从事件订阅中获取委托





4.00/5 (8投票s)
这段代码允许你获取订阅到控件事件的委托。所使用的技术适用于通用事件。
引言
这段代码允许你获取订阅到控件事件的委托。所使用的技术适用于通用事件。
背景
最近我遇到一个问题。任务是扩展某个第三方控件的功能。没有源代码可用,所有 public
类都是密封的。我需要更改一个显示 SaveFileDialog
然后使用默认设置渲染报告的按钮的行为。 唯一的方法不是重写整个控件工具栏,而是更改渲染的设置。
但我真正需要做的只是更改控件按钮上的 Click
事件的处理程序。为此,应该取消订阅旧的处理程序 (-=) 并订阅新的处理程序 (+=)。问题是 - 如何在无法访问它时获取旧的处理程序?
仔细想想,当开发人员想要取消订阅匿名委托或 lambda 表达式时,也会出现相同的情况。没有名称可以引用它们(除非事先将其存储在变量中。但匿名委托,你知道……应该保持匿名)。
研究
首先让我们看看事件存储的背景。我需要修改 ToolStripButton
的行为,所以我们将从它开始挖掘。
Reflector 显示 ToolStripButton
类只有两个事件。但在基类 ToolStripItem
中有很多事件。例如,Click
事件的定义如下
public event EventHandler Click
{
add { base.Events.AddHandler(EventClick, value); }
remove { base.Events.RemoveHandler(EventClick, value); }
}
我们可以看到,有一个 Events 集合存储所有订阅的委托。事件的类型由键对象决定 - 在这种情况下是 EventClick
public abstract class ToolStripItem: Component, ...
{
...
internal static readonly object EventClick;
...
}
Events 集合(类型为 EventHandlerList
)存储在 System.ComponentModel.Component
类中,并且有一个有趣的方法 Find
public sealed class EventHandlerList : ...
{
...
private ListEntry Find(object key);
...
private sealed class ListEntry
{
...
internal Delegate handler;
...
}
}
实现
总而言之,要访问订阅到 Click
事件的委托,可以编写如下代码
control.Events.Find( control.EventClick ).handler
但封装不允许这样做。幸运的是,有一种方法可以实现这一点 - 反射。代码将是
var handler = (EventHandler) GetDelegate( toolStripButton, "EventClick" );
private static object GetDelegate( Component issuer, string keyName )
{
// Get key value for a Click Event
var key = issuer
.GetType( )
.GetField( keyName, BindingFlags.Static |
BindingFlags.NonPublic | BindingFlags.FlattenHierarchy )
.GetValue( null );
// Get events value to get access to subscribed delegates list
var events = typeof( Component )
.GetField( "events", BindingFlags.Instance | BindingFlags.NonPublic )
.GetValue( issuer );
// Find the Find method and use it to search up listEntry for corresponding key
var listEntry = typeof( EventHandlerList )
.GetMethod( "Find", BindingFlags.NonPublic | BindingFlags.Instance )
.Invoke( events, new object[] { key } );
// Get handler value from listEntry
var handler = listEntry
.GetType( )
.GetField( "handler", BindingFlags.Instance | BindingFlags.NonPublic )
.GetValue( listEntry );
return handler;
}
可用的事件键(你知道,不仅仅是“EventClick
”……)可以通过一个方法访问
private static IEnumerable<string> GetEventKeysList( Component issuer )
{
return
from key in issuer.GetType( ).GetFields( BindingFlags.Static |
BindingFlags.NonPublic | BindingFlags.FlattenHierarchy )
where key.Name.StartsWith( "Event" )
select key.Name;
}
包含文章的项目包含一个演示所有描述技术的项目。
结论
C# 中的事件操作语法被设计成具有限制性,目的是不破坏事件封装。这是设计使然,也是正确的做法。
需要访问订阅到已知事件的未知委托的情况很少见。但它确实存在。我希望这篇文章能为你节省一些时间,当你遇到这样的问题时。
历史
- 2009 年 4 月 5 日:初始发布