65.9K
CodeProject 正在变化。 阅读更多。
Home

从事件订阅中获取委托

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (8投票s)

2009年4月5日

CPOL

2分钟阅读

viewsIcon

42799

downloadIcon

380

这段代码允许你获取订阅到控件事件的委托。所使用的技术适用于通用事件。

引言

这段代码允许你获取订阅到控件事件的委托。所使用的技术适用于通用事件。

背景

最近我遇到一个问题。任务是扩展某个第三方控件的功能。没有源代码可用,所有 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 日:初始发布
© . All rights reserved.