Windows窗体中的通用事件处理程序的解决方法 [英] Workaround for generic event handler in Windows Forms

查看:133
本文介绍了Windows窗体中的通用事件处理程序的解决方法的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

很久以前,我注意到Visual Studio的Windows Forms编辑器不支持包含通用类型参数的事件。例如,像

  public event EventHandler< ListEventArgs< int>> MyStrangeEvent {add {...}删除{...}} 

其中

  public class ListEventArgs< T> :EventArgs {List< T> ARGS; } 

甚至不会显示在Visual Studio的属性管理器中的事件列表中。现在,这是一个有趣的例子,可以通过重写类及其事件,轻松地修改为在Visual Studio中工作。但是,目前我正在从事一个项目,因为兼容性原因我无法修改一些类。我唯一能做的就是改变用户控制的事件。此控件的事件现在如下所示:

 公共事件EventHandler< Plane< GDISurface> .DrawingErrorEventArgs> DrawingError {add {_Plane.DrawingError + = value; }删除{_Plane.DrawingError  -  = value;请注意,底层的Plane类(由保护字段的_Plane实例表示)不能改变。它的DrawingError事件及其EventArgs类型在Plane类中声明如下:

  public class Plane< T>其中T:ISurface 
{
...
public event EventHandler< DrawingErrorEventArgs> DrawingError = null;
...
public class DrawingErrorEventArgs:EventArgs {... / *使用T * / ...}
}

当然,Visual Studio的Windows窗体编辑器不会显示我的用户控件的任何事件。我一直在寻找一些解决方法来重新显示它们,但是无法找到实际工作的解决方法。以下是我尝试的一些事情:


  1. 创建了一个从Plane继承的MyPlane类,并改为使用: public事件EventHandler< MyPlane.DrawingErrorEventArgs> DrawingError ... 。由于我不知道的原因,事件仍然没有显示在编辑器中。也许这是由于事件的参数,其中一些仍然是通用的。

  2. 创建了一个帮助器类,它定义了 EventHandler< Plane< GDISurface> .DrawingErrorEventArgs> 之间的隐式转换运算符和 EventHandler< GDIPlane.DrawingErrorEventArgs> 其中GDIPlane只是一个继承自 Plane< GDISurface> 的虚拟类。这在某种程度上起作用,但重复事件调用,因为转换创建新的事件处理程序,传递给_Plane,不能正确删除/取消注册。

  3. 尝试继承自 EventHandler< Plane< GDISurface> .DrawingErrorEventArgs> ,这显然不起作用,因为 EventHandler< T> 已被密封。

有没有其他方法可以在Windows窗体编辑器中再次显示我的活动?



最好的问候
Andreas



编辑:1的最小工作示例:

 code> public interface ISurface {} 

public class GDISurface:ISurface {}

public class Plane< T>其中T:ISurface
{
public event EventHandler< DrawingErrorEventArgs> DrawingError = null;
public class DrawingErrorEventArgs:EventArgs {T stuff;
}

public class TestControl:UserControl
{
public class GDIPlane:Plane< GDISurface> {}
GDIPlane _Plane = null;
public event EventHandler< GDIPlane.DrawingErrorEventArgs> DrawingError {add {_Plane.DrawingError + = value; }删除{_Plane.DrawingError - = value; }}
}

DrawingError不会显示在属性管理器中的事件列表中当您点击TestControl实例时。



EDIT2:这是原始的问题(没有任何解决方法)TestControl的DrawingError事件不显示:

  public interface ISurface {} 

public class GDISurface:ISurface {}

public类平面< T>其中T:ISurface
{
public event EventHandler< DrawingErrorEventArgs> DrawingError = null;
public class DrawingErrorEventArgs:EventArgs {T stuff; }
}

public class TestControl:UserControl
{
平面< GDISurface> _Plane = null;
public event EventHandler< Plane< GDISurface> .DrawingErrorEventArgs> DrawingError {add {_Plane.DrawingError + = value; }删除{_Plane.DrawingError - = value; }}
}


解决方案

到Visual Studio,其原因源于事件,即EventHandler<>并没有在其'TEventArgs'上指定协方差(它会强加看似愚蠢的限制),并且这些工具不能对你的代码进行足够的反思,以排除适当的类型(即使您在构建控件时已经留下了类型数据的踪迹)。因此,似乎VS似乎不支持泛型事件属性。您可以考虑在 Microsoft Connect 上提交功能请求,我不会建议将其作为错误提交,因为它们可以通过设计标注它,并关闭它。



作为一般规则,如果您需要您的事件的通用类型参数,而需要设计时间支持(这是不同的实现问题),您正在将其包装在一个特定于演示文稿的外观中(例如额外的代码层以便于设计时需要)。 / p>

个人来说,我会减少你现在玩的泛型打字,看起来有点过分,如果你不明白通用类型的协方差/逆向,那么它可能会让你



但是,要解决您的问题:



考虑使用可以在非泛型属性中传输数据的自定义事件args类,并使用非ge事件/属性 EventHandler 了解事件的类型然后将其从泛型类型参数转移出去,从而使您的非泛型事件的参数变为args。如果事件的类引用不足,您可以添加一个属性来传达事件类型(或数据类型),以便接收代码可以正确解释(假设当然不知道某个事件类型意味着。):

  public class DataEventArgs:EventArgs 
{
// public string EventTypeOrPurpose {get;组; }
public object Data {get;组; }
}

这最常用于通过事件链来传送数据,通常实现如下:

  public class DataEventArgs&T; :EventArgs 
{
public T Data {get;组; }
}

不幸的是,这也有一个协方差问题,要解决它,想要一些这样的东西:

  public interface IDataArgs< out T> 
{
T Data {get; }
}

public class DataEventArgs< T> :EventArgs,IDataArgs< T>
{
public DataEventArgs< T>(T data)
{
_data = data;
}
private T _data;
public T Data {get {return _data; }}
}

即使如此,这些通用版本仍然无效围绕Visual Studio的局限性,这只是您已经向我们展示的更为恰当的替代形式。



更新:根据要求,这里是最根本的意义上,专用立面可能看起来像。请注意,在这种情况下,usercontrol用作外观层,因为事件处理程序会将代理公开给底层对象模型。没有从用户控件(从消费者/设计师的角度)直接访问底层对象模型。



请注意,事件处理程序的参考跟踪不需要,除非您处理这些用户控件在整个应用程序的整个生命周期中(只有确保根据提供的代理进行正确的委托删除,这将被封装/委托包装),如下所示。)



另外值得注意的是,我没有测试运行此代码,除非验证设计者在删除到表单上时在属性网格中显示 DrawingError

 命名空间SampleCase3 
{
public interface ISurface {}

public class GDISurface:ISurface {}

public class Plane< T>其中T:ISurface
{
public event EventHandler< DrawingErrorEventArgs> DrawingError;
public class DrawingErrorEventArgs:EventArgs {T stuff; }
}

public class TestControl:UserControl
{
private Plane< GDISurface> _Plane = new Plane< GDISurface>(); //需要初始化为我自己的测试

public TestControl()
{
}

//我添加这个地图*只*事件处理程序的删除可以正确地完成
private Dictionary< EventHandler,EventHandler< Plane< GDISurface> .DrawingErrorEventArgs>> _cleanupMap =新词典< EventHandler,EventHandler< Plane< GDISurface> .DrawingErrorEventArgs>>();

public event EventHandler DrawingError
{
add
{
var nonGenericHandler = value;
var genericHandler =(EventHandler< Plane< GDISurface> .DrawingErrorEventArgs>)delegate(object sender,Plane< GDISurface> .DrawingErrorEventArgs e)
{
nonGenericHandler(sender,e);
};
_Plane.DrawingError + = genericHandler;
_cleanupMap [nonGenericHandler] = genericHandler;
}
删除
{
var nonGenericHandler = value;
var genericHandler = default(EventHandler< Plane< GDISurface> .DrawingErrorEventArgs>);
if(_cleanupMap.TryGetValue(nonGenericHandler,out genericHandler))
{
_Plane.DrawingError - = genericHandler;
_cleanupMap.Remove(nonGenericHandler);
}
}
}
}
}

为了补充上面的内容,这里是一个非泛型事件处理程序现在看起来像:

  private void testControl1_DrawingError (object sender,EventArgs e)
{
var genericDrawingErrorEventArgs = e as Plane< GDISurface> .DrawingErrorEventArgs;
if(genericDrawingErrorEventArgs!= null)
{
// TODO:
}
}
e 的类型才能执行转换。使用作为运算符将绕过祖先检查,假设转换应该成功。



这样的事情就像你要得到的一样接近。是的,我们的大部分标准都是丑陋的,但是如果您绝对需要在这些组件之上进行设计时支持,并且您无法更改 Plane< T> 更合适),那么这个或者接近这个的东西是唯一可行的解​​决方法。



HTH


Quite some time ago, I noticed that the Windows Forms editor of Visual Studio does not support events which contain generic type parameters. For example, an event like

public event EventHandler<ListEventArgs<int>> MyStrangeEvent { add { ... } remove { ... } }

where

public class ListEventArgs<T> : EventArgs { List<T> args; }

does not even show up in the event list in the property manager of Visual Studio. Now, this is a somewhat artificial example that could easily be modified to work in Visual Studio by rewriting the classes and their events. However, I am currently working on a project where I cannot change some classes for compatibility reasons. The only thing I can do is to change the events of my user control. The events of this control currently look like this:

public event EventHandler<Plane<GDISurface>.DrawingErrorEventArgs> DrawingError { add { _Plane.DrawingError += value; } remove { _Plane.DrawingError -= value; } }

Note that the underlying Plane class (represented by the _Plane instance which is a protected field) cannot be changed. Its DrawingError event and its EventArgs type are declared in the Plane class like this:

public class Plane<T> where T : ISurface
{
    ...
    public event EventHandler<DrawingErrorEventArgs> DrawingError = null;
    ...
    public class DrawingErrorEventArgs : EventArgs { ... /* Uses T */ ... }
}

Of course, the Windows Forms editor of Visual Studio does not show any of the events of my user control. I have been looking for a number of workarounds to get them shown again, but have not been able to find a workaround that actually works. Here are some things that I tried:

  1. Created a MyPlane class which inherits from Plane and used that instead: public event EventHandler<MyPlane.DrawingErrorEventArgs> DrawingError .... For reasons unknown to me, the events still don't show up in the editor. Perhaps this is due to the parameters of the event, some of which still are generic. Find a minimal working example below.
  2. Created a helper class which defines implicit conversion operators between EventHandler<Plane<GDISurface>.DrawingErrorEventArgs> and EventHandler<GDIPlane.DrawingErrorEventArgs> where GDIPlane is just a dummy class which inherits from Plane<GDISurface>. This does work to some extent, but duplicates event calls since the conversion creates new event handlers which are passed down to _Plane which cannot be removed/unregistered properly.
  3. Tried to inherit from EventHandler<Plane<GDISurface>.DrawingErrorEventArgs>, which obviously does not work since EventHandler<T> is sealed.

Are there any other ways to make my events visible again in the Windows Forms editor?

Best regards Andreas

EDIT: Minimal working example for 1:

public interface ISurface { }

public class GDISurface : ISurface { }

public class Plane<T> where T : ISurface
{
    public event EventHandler<DrawingErrorEventArgs> DrawingError = null;
    public class DrawingErrorEventArgs : EventArgs { T stuff; }
}

public class TestControl : UserControl
{
    public class GDIPlane : Plane<GDISurface>  { }
    GDIPlane _Plane = null;
    public event EventHandler<GDIPlane.DrawingErrorEventArgs> DrawingError { add { _Plane.DrawingError += value; } remove { _Plane.DrawingError -= value; } }
}

DrawingError does not show up in the list of events in the property manager when clicking on a TestControl instance.

EDIT2: This is the original problem (without any workarounds) where the DrawingError event does of TestControl does not show up either:

public interface ISurface { }

public class GDISurface : ISurface { }

public class Plane<T> where T : ISurface
{
    public event EventHandler<DrawingErrorEventArgs> DrawingError = null;
    public class DrawingErrorEventArgs : EventArgs { T stuff; }
}

public class TestControl : UserControl
{
    Plane<GDISurface> _Plane = null;
    public event EventHandler<Plane<GDISurface>.DrawingErrorEventArgs> DrawingError { add { _Plane.DrawingError += value; } remove { _Plane.DrawingError -= value; } }
}

解决方案

This is behavior specific to Visual Studio, and the cause is rooted in the fact that EventHandler<> does not specify covariance on its 'TEventArgs' (it would impose seemingly silly restrictions) and the tools do not perform enough introspection of your code to suss out an appropriate type (even though you've left a trail of type data in constructing the control.) Thus, it seems as though VS does not support generic event properties. You may consider filing a feature request on Microsoft Connect, I wouldn't suggest filing it as a bug as they may label it "by design" and close it.

As a general rule, if you need generic type parameters on your events and you need design time support for them (which are different implementation concerns), you're looking at wrapping them in a presentation-specific facade (e.g. "extra layer of code to facilitate design-time needs".)

Personally, I would reduce the generic typing you have in play now, it seems a bit excessive and if you don't understand covariance/contravariance in generic types it might put you in a tight spot at some point, such as now.

However, to work around your problem:

Consider using a custom event args class which could transport data in a non-generic property, and also use a non-generic EventHandler event/property. Understanding the 'type' of the event is then shifted away from generic type parameters and made the responsibility of your non-generic event args instead. If the 'class' of the event args is insufficient, you can add a property to convey the event type (or data type) so that receiving code can properly interpret it (assuming, of course, that it does not already know by some other means.):

public class DataEventArgs : EventArgs
{
    //public string EventTypeOrPurpose { get; set; }
    public object Data { get; set; }
}

This is most often only used to ferry data through an event chain, and it is usually implemented as follows:

public class DataEventArgs<T> : EventArgs
{
    public T Data { get; set; }
}

Unfortunately, this also has a covariance problem, to resolve it you would actually want something more like this:

public interface IDataArgs<out T>
{
    T Data { get; }
}

public class DataEventArgs<T> : EventArgs, IDataArgs<T>
{
    public DataEventArgs<T>(T data) 
    {
        _data = data;
    }
    private T _data;
    public T Data { get { return _data; } }
}

Even so, these generic versions still don't work around Visual Studio's limitations, this is merely more proper alternative forms of what you already have shown us.

UPDATE: As requested, here is what a "purpose built facade" might look like in the most basic sense. Note that the usercontrol functions as a facade layer in this case as the eventhandler it exposes delegates to the underlying object model. There is no direct access to underlying object model from the user control (from consumer/designer perspective.)

Please note the reference tracking for event handlers is not necessary unless you dispose of these user controls throughout the lifetime of the app (it is only done to ensure proper delegate removal based on the delegate provided, which is wrapped in a closure/delegate, as you see below.)

Also worth noting I did not test-run this code beyond verifying that the designer shows DrawingError in the property grid when dropped onto a form.

namespace SampleCase3
{
    public interface ISurface { }

    public class GDISurface : ISurface { }

    public class Plane<T> where T : ISurface
    {
        public event EventHandler<DrawingErrorEventArgs> DrawingError;
        public class DrawingErrorEventArgs : EventArgs { T stuff; }
    }

    public class TestControl : UserControl
    {
        private Plane<GDISurface> _Plane = new Plane<GDISurface>(); // requires initialization for my own testing

        public TestControl()
        {
        }

        // i am adding this map *only* so that the removal of an event handler can be done properly
        private Dictionary<EventHandler, EventHandler<Plane<GDISurface>.DrawingErrorEventArgs>> _cleanupMap = new Dictionary<EventHandler, EventHandler<Plane<GDISurface>.DrawingErrorEventArgs>>();

        public event EventHandler DrawingError
        {
            add
            {
                var nonGenericHandler = value;
                var genericHandler = (EventHandler<Plane<GDISurface>.DrawingErrorEventArgs>)delegate(object sender, Plane<GDISurface>.DrawingErrorEventArgs e)
                {
                    nonGenericHandler(sender, e);
                };
                _Plane.DrawingError += genericHandler;
                _cleanupMap[nonGenericHandler] = genericHandler;
            }
            remove
            {
                var nonGenericHandler = value;
                var genericHandler = default(EventHandler<Plane<GDISurface>.DrawingErrorEventArgs>);
                if (_cleanupMap.TryGetValue(nonGenericHandler, out genericHandler))
                {
                    _Plane.DrawingError -= genericHandler;
                    _cleanupMap.Remove(nonGenericHandler);
                }
            }
        }
    }
}

To complement the above, here is what a non-generic event handler would now look like:

private void testControl1_DrawingError(object sender, EventArgs e)
{
    var genericDrawingErrorEventArgs = e as Plane<GDISurface>.DrawingErrorEventArgs;
    if (genericDrawingErrorEventArgs != null)
    {
        // TODO:
    }
}

Note that the consumer here has to have knowledge of the type for e to perform conversion. The use of the as operator will bypass ancestry checks under the assumption that the conversion should succeed.

Something like this is as close as you're going to get. Yes it is ugly by most of our standards, but if you absolutely 'need' design-time support on top of these components and you cannot change Plane<T> (which would be more appropriate) then this, or something close to this, is the only viable workaround.

HTH

这篇关于Windows窗体中的通用事件处理程序的解决方法的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆