Windows 表单中通用事件处理程序的解决方法
Workaround for generic event handler in Windows Forms
很久以前,我注意到 Windows 表单编辑器 Visual Studio 不支持包含通用类型参数的事件。例如,类似
的事件
public event EventHandler<ListEventArgs<int>> MyStrangeEvent { add { ... } remove { ... } }
哪里
public class ListEventArgs<T> : EventArgs { List<T> args; }
甚至没有出现在 Visual Studio 的 属性 管理器的事件列表中。现在,这是一个有点人为的示例,可以通过重写 classes 及其事件轻松修改以在 Visual Studio 中工作。但是,我目前正在从事一个项目,出于兼容性原因,我无法更改某些 classes。我唯一能做的就是更改我的用户控件的事件。该控件的事件当前如下所示:
public event EventHandler<Plane<GDISurface>.DrawingErrorEventArgs> DrawingError { add { _Plane.DrawingError += value; } remove { _Plane.DrawingError -= value; } }
请注意,底层的 Plane class(由 _Plane 实例表示,它是一个受保护的字段)无法更改。它的 DrawingError 事件及其 EventArgs 类型在 Plane class 中声明如下:
public class Plane<T> where T : ISurface
{
...
public event EventHandler<DrawingErrorEventArgs> DrawingError = null;
...
public class DrawingErrorEventArgs : EventArgs { ... /* Uses T */ ... }
}
当然,Visual Studio 的 Windows 表单编辑器不显示我的用户控件的任何事件。我一直在寻找一些解决方法来让它们再次显示,但一直无法找到真正有效的解决方法。以下是我尝试过的一些方法:
- 创建了一个 MyPlane class,它继承自 Plane 并改为使用它:
public event EventHandler<MyPlane.DrawingErrorEventArgs> DrawingError ...
。由于我不知道的原因,事件仍然没有出现在编辑器中。也许这是由于事件的参数,其中一些仍然是通用的。在下面找到一个最小的工作示例。
- 创建了一个助手 class,它定义了
EventHandler<Plane<GDISurface>.DrawingErrorEventArgs>
和 EventHandler<GDIPlane.DrawingErrorEventArgs>
之间的隐式转换运算符,其中 GDIPlane 只是一个从 Plane<GDISurface>
继承的虚拟 class。这在某种程度上确实有效,但会重复事件调用,因为转换会创建新的事件处理程序,这些处理程序会传递给 _Plane,而不能正确地 removed/unregistered。
- 试图从
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' 上指定协方差(它会施加看似愚蠢的限制)和工具不要对您的代码执行足够的内省以得出适当的类型(即使您在构造控件时留下了类型数据的踪迹。)因此,VS 似乎不支持通用事件属性。您可以考虑在 Microsoft Connect 上提交功能请求,我不建议将其作为错误提交,因为他们可能会将其标记为 "by design" 并关闭它。
作为一般规则,如果您需要 事件的通用类型参数并且您需要 设计时支持它们(它们是不同的实现问题),您正在考虑将它们包装在特定于演示文稿的外观中(例如 "extra layer of code to facilitate design-time needs"。)
就我个人而言,我会减少你现在使用的通用类型,这似乎有点过分,如果你不理解 covariance/contravariance 在通用类型中,它可能会让你在某些时候陷入困境,比如现在。
但是,要解决您的问题:
考虑使用自定义事件参数 class,它可以在非通用 属性 中传输数据,并且还可以使用非通用 EventHandler
event/property。了解事件的 'type' 然后从泛型类型参数转移到非泛型事件参数的责任。如果事件参数的 '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 的限制,这只是您已经向我们展示的更合适的替代形式.
更新:根据要求,这是 "purpose built facade" 在最基本的意义上可能看起来像 的样子。请注意,在这种情况下,用户控件充当外观层,作为事件处理程序,它公开委托给底层对象模型。无法从用户控件直接访问底层对象模型(从 consumer/designer 角度来看。)
请注意,事件处理程序的引用跟踪不是必需的,除非您在应用程序的整个生命周期内处理这些用户控件(这样做只是为了确保根据提供的委托正确删除委托,委托包含在closure/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
运算符将在转换应该成功的假设下绕过祖先检查。
像这样的东西是你将要得到的最接近的东西。是的,按照我们的大多数标准,它是丑陋的,但是如果你绝对 'need' 在这些组件之上提供设计时支持,并且你不能更改 Plane<T>
(这会更合适),那么这个,或者类似的东西对此,这是唯一可行的解决方法。
HTH
很久以前,我注意到 Windows 表单编辑器 Visual Studio 不支持包含通用类型参数的事件。例如,类似
的事件public event EventHandler<ListEventArgs<int>> MyStrangeEvent { add { ... } remove { ... } }
哪里
public class ListEventArgs<T> : EventArgs { List<T> args; }
甚至没有出现在 Visual Studio 的 属性 管理器的事件列表中。现在,这是一个有点人为的示例,可以通过重写 classes 及其事件轻松修改以在 Visual Studio 中工作。但是,我目前正在从事一个项目,出于兼容性原因,我无法更改某些 classes。我唯一能做的就是更改我的用户控件的事件。该控件的事件当前如下所示:
public event EventHandler<Plane<GDISurface>.DrawingErrorEventArgs> DrawingError { add { _Plane.DrawingError += value; } remove { _Plane.DrawingError -= value; } }
请注意,底层的 Plane class(由 _Plane 实例表示,它是一个受保护的字段)无法更改。它的 DrawingError 事件及其 EventArgs 类型在 Plane class 中声明如下:
public class Plane<T> where T : ISurface
{
...
public event EventHandler<DrawingErrorEventArgs> DrawingError = null;
...
public class DrawingErrorEventArgs : EventArgs { ... /* Uses T */ ... }
}
当然,Visual Studio 的 Windows 表单编辑器不显示我的用户控件的任何事件。我一直在寻找一些解决方法来让它们再次显示,但一直无法找到真正有效的解决方法。以下是我尝试过的一些方法:
- 创建了一个 MyPlane class,它继承自 Plane 并改为使用它:
public event EventHandler<MyPlane.DrawingErrorEventArgs> DrawingError ...
。由于我不知道的原因,事件仍然没有出现在编辑器中。也许这是由于事件的参数,其中一些仍然是通用的。在下面找到一个最小的工作示例。 - 创建了一个助手 class,它定义了
EventHandler<Plane<GDISurface>.DrawingErrorEventArgs>
和EventHandler<GDIPlane.DrawingErrorEventArgs>
之间的隐式转换运算符,其中 GDIPlane 只是一个从Plane<GDISurface>
继承的虚拟 class。这在某种程度上确实有效,但会重复事件调用,因为转换会创建新的事件处理程序,这些处理程序会传递给 _Plane,而不能正确地 removed/unregistered。 - 试图从
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' 上指定协方差(它会施加看似愚蠢的限制)和工具不要对您的代码执行足够的内省以得出适当的类型(即使您在构造控件时留下了类型数据的踪迹。)因此,VS 似乎不支持通用事件属性。您可以考虑在 Microsoft Connect 上提交功能请求,我不建议将其作为错误提交,因为他们可能会将其标记为 "by design" 并关闭它。
作为一般规则,如果您需要 事件的通用类型参数并且您需要 设计时支持它们(它们是不同的实现问题),您正在考虑将它们包装在特定于演示文稿的外观中(例如 "extra layer of code to facilitate design-time needs"。)
就我个人而言,我会减少你现在使用的通用类型,这似乎有点过分,如果你不理解 covariance/contravariance 在通用类型中,它可能会让你在某些时候陷入困境,比如现在。
但是,要解决您的问题:
考虑使用自定义事件参数 class,它可以在非通用 属性 中传输数据,并且还可以使用非通用 EventHandler
event/property。了解事件的 'type' 然后从泛型类型参数转移到非泛型事件参数的责任。如果事件参数的 '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 的限制,这只是您已经向我们展示的更合适的替代形式.
更新:根据要求,这是 "purpose built facade" 在最基本的意义上可能看起来像 的样子。请注意,在这种情况下,用户控件充当外观层,作为事件处理程序,它公开委托给底层对象模型。无法从用户控件直接访问底层对象模型(从 consumer/designer 角度来看。)
请注意,事件处理程序的引用跟踪不是必需的,除非您在应用程序的整个生命周期内处理这些用户控件(这样做只是为了确保根据提供的委托正确删除委托,委托包含在closure/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
运算符将在转换应该成功的假设下绕过祖先检查。
像这样的东西是你将要得到的最接近的东西。是的,按照我们的大多数标准,它是丑陋的,但是如果你绝对 'need' 在这些组件之上提供设计时支持,并且你不能更改 Plane<T>
(这会更合适),那么这个,或者类似的东西对此,这是唯一可行的解决方法。
HTH