不久前,我注意到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编辑器不会显示我的用户控件的任何事件。我一直在寻找一些变通方法来再次展示它们,但一直未能找到一个真正有效的变通方法。以下是我尝试过的一些东西:
- 创建了一个从Plane继承的MyPlane类,并使用了它:
public event EventHandler<MyPlane.DrawingErrorEventArgs> DrawingError ...
。由于我不知道的原因,这些事件仍然没有出现在编辑中。也许这是由于事件的参数,其中一些参数仍然是通用的。在下面找到一个最小的工作示例 - 创建了一个助手类,该类定义了
EventHandler<Plane<GDISurface>.DrawingErrorEventArgs>
和EventHandler<GDIPlane.DrawingErrorEventArgs>
之间的隐式转换运算符,其中GDIPlane只是继承自Plane<GDISurface>
的伪类。这在一定程度上确实有效,但重复了事件调用,因为转换会创建新的事件处理程序,这些事件处理程序会传递到无法正确删除/注销的_Plane - 尝试从
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