Windows窗体中的通用事件处理程序的解决方法

时间:2015-02-15 14:57:52

标签: c# winforms generics visual-studio-2013 event-handling

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

这样的事件
public event EventHandler<ListEventArgs<int>> MyStrangeEvent { add { ... } remove { ... } }

,其中

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

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

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

请注意,无法更改基础Plane类(由_Plane实例表示,它是受保护的字段)。它的DrawingError事件及其EventArgs类型在Plane类中声明如下:

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

当然,Visual Studio的Windows窗体编辑器不会显示我的用户控件的任何事件。我一直在寻找一些解决方法来让它们再次显示,但是却找不到真正有效的解决方法。以下是我尝试过的一些事情:

  1. 创建了一个继承自Plane的MyPlane类,并使用它代替:public event EventHandler<MyPlane.DrawingErrorEventArgs> DrawingError ...。由于我不知道的原因,事件仍未显示在编辑器中。也许这是由于事件的参数,其中一些仍然是通用的。找到下面的最小工作示例。
  2. 创建了一个辅助类,它定义了EventHandler<Plane<GDISurface>.DrawingErrorEventArgs>EventHandler<GDIPlane.DrawingErrorEventArgs>之间的隐式转换运算符,其中GDIPlane只是一个继承自Plane<GDISurface>的虚拟类。这在某种程度上有效,但重复事件调用,因为转换会创建新的事件处理程序,这些处理程序将传递给_Plane,无法正确删除/取消注册。
  3. 试图从EventHandler<Plane<GDISurface>.DrawingErrorEventArgs>继承,但由于EventHandler<T>被封存,这显然无效。
  4. 是否还有其他方法可以在Windows窗体编辑器中再次显示我的事件?

    祝你好运 安德烈亚斯

    编辑: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; } }
    }
    
    单击TestControl实例时,

    DrawingError不会显示在属性管理器的事件列表中。

    EDIT2:这是TestControl的DrawingError事件没有出现的原始问题(没有任何变通方法):

    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; } }
    }
    

1 个答案:

答案 0 :(得分:2)

这是Visual Studio特有的行为,其根源在于EventHandler&lt;&gt;没有在其TEventArgs&#39;上指定协方差。 (它会施加看似愚蠢的限制)并且这些工具没有对你的代码进行足够的内省以得出合适的类型(即使你在构造控件时留下了类型数据的痕迹。)因此,它似乎是虽然VS不支持通用事件属性。您可以考虑在Microsoft Connect上提交功能请求,我不建议将其归档为错误,因为他们可能会将其标记为“#34;设计&#34;并关闭它。

作为一般规则,如果您需要您的事件的泛型类型参数,并且您需要设计时支持它们(这是不同的实现问题),您&#39 ;重新考虑将它们包装在特定于演示文稿的外观中(例如,#34;额外的代码层以方便设计时间需要&#34;。)

就个人而言,我会减少你现在玩的通用打字,看起来有点过分,如果你不理解通用类型中的协方差/逆变,它可能会让你在某个时刻陷入困境,比如就像现在一样。

但是,要解决您的问题:

考虑使用自定义事件args类,它可以在非泛型属性中传输数据,并且还使用非泛型EventHandler事件/属性。理解&#39;类型&#39;然后,事件将从泛型类型参数转移,并使您的非泛型事件args负责。如果&#39;班级&#39;如果事件args不足,你可以添加一个属性来传达事件类型(或数据类型),以便接收代码可以正确地解释它(当然,假设它还没有通过其他方式知道。):< / p>

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

这通常仅用于通过事件链传送数据,通常按如下方式实现:

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

不幸的是,这也有一个协方差问题,为了解决这个问题你真的想要更像这样的东西:

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的限制,这只是您已经向我们展示的更合适的替代形式。

更新:根据要求,这是一个&#34;目的建立的外观&#34;在最基本的意义上可能看起来像。请注意,在这种情况下,usercontrol充当外观层,因为它将委托暴露给底层对象模型。用户控件(从消费者/设计者的角度来看)无法直接访问底层对象模型。

请注意,除非您在应用程序的整个生命周期内处理这些用户控件,否则不必对事件处理程序进行引用跟踪(这样做只是为了确保根据提供的委托删除正确的委托,该委托包含在一个/代表,如下所示。)

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

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);
                }
            }
        }
    }
}

为了补充上述内容,以下是非通用事件处理程序现在的样子:

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

请注意,此处的消费者必须了解e执行转换的类型。在假设转换成功的情况下,使用as运算符将绕过祖先检查。

像这样的东西就像你要去的那样近。是的,我们的大多数标准都很难看,但如果你绝对需要&#39;在这些组件之上的设计时支持,你不能改变Plane<T>(这将更合适)然后这个,或接近这个,是唯一可行的解​​决方法。

HTH