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



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

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

其中

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

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

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 */ ... }
}

当然,VisualStudio的WindowsForms编辑器不会显示我的用户控件的任何事件。我一直在寻找一些变通方法来再次展示它们,但一直未能找到一个真正有效的变通方法。以下是我尝试过的一些东西:

  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窗体编辑器中再次可见?

致以最良好的问候Andreas

编辑: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不会显示在属性管理器的事件列表中。

编辑2:这是最初的问题(没有任何解决方法),其中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"上指定协变性(这会施加看似愚蠢的限制),并且工具没有对代码执行足够的内省以找出合适的类型(即使在构造控件时留下了类型数据的痕迹)。因此,VS似乎不支持泛型事件属性。你可以考虑在Microsoft Connect上提交功能请求,我不建议将其作为bug提交,因为他们可能会将其标记为"设计"并关闭它。

一般来说,如果您的事件需要通用类型参数,并且您需要对它们的设计时支持(这是不同的实现问题),则您需要将它们封装在特定于演示的外观中(例如"促进设计时需求的额外代码层")

就我个人而言,我会减少你现在使用的泛型类型,这似乎有点过分,如果你不了解泛型类型中的协方差/反方差,它可能会在某个时候让你陷入困境,比如现在。

但是,要解决您的问题:

考虑使用自定义事件args类,该类可以在非通用属性中传输数据,也可以使用非通用EventHandler事件/属性。然后,了解事件的"类型"将从泛型类型参数转移到非泛型事件参数。如果事件参数的"class"不足,您可以添加一个属性来传达事件类型(或数据类型),以便接收代码能够正确地解释它(当然,假设它还不知道其他方式。):

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

更新:根据要求,以下是"专门建造的立面"在最基本的意义上可能是什么样子。请注意,在本例中,用户控件充当门面层,作为事件处理程序,它向底层对象模型公开委托。没有从用户控制(从消费者/设计者的角度)直接访问底层对象模型

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

同样值得注意的是,除了验证设计器在放到表单上时在属性网格中显示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

相关内容

  • 没有找到相关文章

最新更新