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

标签 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 实例表示,它是一个 protected 字段)不能更改。它的 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>是密封的。

是否有任何其他方法可以使我的事件在 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; } }
}

最佳答案

这是特定于 Visual Studio 的行为,其根源在于 EventHandler<> 没有在其“TEventArgs”上指定协方差(它会施加看似愚蠢的限制)并且工具没有对您的应用程序执行足够的自省(introspection)代码来确定适当的类型(即使您在构造控件时留下了类型数据的踪迹。)因此,VS 似乎不支持通用事件属性。您可以考虑在 Microsoft Connect 上提交功能请求,我不建议将其作为错误提交,因为他们可能会将其标记为“按设计”并将其关闭。

作为一般规则,如果您需要您的事件的通用类型参数并且您需要对它们的设计时支持(这是不同的实现问题),您是考虑将它们包装在特定于表示的外观中(例如“额外的代码层以促进设计时的需求”。)

就个人而言,我会减少你现在使用的泛型类型,它看起来有点过分,如果你不理解泛型类型中的协变/逆变,它可能会让你在某些时候陷入困境,比如现在.

但是,要解决您的问题:

考虑使用可以在非通用属性中传输数据的自定义事件参数类,并使用非通用 EventHandler事件/属性(property)。然后,了解事件的“类型”不再是泛型类型参数,而是让您的非泛型事件参数负责。如果事件参数的“类”不够,您可以添加一个属性来传达事件类型(或数据类型),以便接收代码可以正确解释它(当然,假设它还没有被其他人知道)意味着。):

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

更新:根据要求,这是最基本意义上的“专用立面”的外观。请注意,在这种情况下,用户控件充当外观层,作为事件处理程序,它公开委托(delegate)给底层对象模型。用户控件无法直接访问底层对象模型(从消费者/设计者的角度来看。)

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

同样值得注意的是,除了验证设计器显示 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的使用运算符将在转换应该成功的假设下绕过祖先检查。

像这样的东西是你将要得到的最接近的东西。是的,按照我们的大多数标准,它是丑陋的,但是如果您绝对“需要”这些组件之上的设计时支持并且您不能更改 Plane<T> (这会更合适)那么这个或接近这个的东西是唯一可行的解​​决方法。

HTH

关于c# - Windows 窗体中通用事件处理程序的解决方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28527350/

相关文章:

c# - Winforms - 在面板内填充用户控件

c++ - Visual C++6.0中CEdit的消息

c# - 禁用 Alt+F4 但允许通过代码关闭表单,CloseReason.UserClosing 没有帮助

java - 将泛型 T 转换为 Object[]

c# - 释放 C# 中的非托管代码

c# - 改变下拉菜单列表项正方形

c# - 如何从 Action<T> 获取方法的自定义属性?

vb.net - 如何将 DataGridView 绑定(bind)到自定义类列表

c# - 组合多个元素集合的优雅方式?

c# - 使用通用抽象类型参数和链接的 .net 单元测试