C#如何查找事件是否已连接?

42

我想要判断一个事件是否已经被挂钩。我查找了一些资料,但是发现很多解决方案都需要修改包含事件的对象内部,而我不想这样做。

以下是我尝试使用的测试代码:

// Create a new event handler that takes in the function I want to execute when the event fires
EventHandler myEventHandler = new EventHandler(myObject_SomeEvent);
// Get "p1" number events that got hooked up to myEventHandler
int p1 = myEventHandler.GetInvocationList().Length;
// Now actually hook an event up
myObject.SomeEvent += m_myEventHandler;
// Re check "p2" number of events hooked up to myEventHandler
int p2 = myEventHandler.GetInvocationList().Length;

很遗憾,以上内容是完全错误的。我曾认为将事件附加到myEventHandler时,“invocationList”会自动更新。但实际上并非如此。该列表的长度始终返回为一。

有没有办法从包含事件的对象外部确定其长度?

5个回答

68
如果相关对象已经指定了事件关键字,那么你只能添加(+=)和移除(-=)处理程序,不能做更多的事情。
我认为比较调用列表长度是可行的,但你需要在对象内部操作才能得到它。
此外,请记住,+=-=运算符返回一个新的事件对象;它们不修改现有对象。
你为什么想知道特定的事件是否被连接?是为了避免多次注册吗?
如果是这样,诀窍是先删除处理程序(-=),因为删除不存在的处理程序是合法的,并且不会产生任何影响。例如:
// Ensure we don't end up being triggered multiple times by the event
myObject.KeyEvent -= KeyEventHandler;
myObject.KeyEvent += KeyEventHandler;

如果您多次执行 "+= KeyEventHandler",那么 "-= KeyEventHandler" 会删除所有的事件处理程序还是只删除最后一个或第一个? - vbp13
“-=” 将减去一个;鉴于它们都相等,我不知道如何确定要减去哪一个。 - Bevan

54

C#中的event关键字存在一个微妙的错觉,即事件具有调用列表。

如果你使用C# event关键字声明事件,编译器将在你的类中生成一个私有委托,并为您进行管理。每当您订阅事件时,编译器生成的add方法被调用,它将事件处理程序附加到委托的调用列表中。事件并没有显式的调用列表。

因此,唯一访问委托调用列表的方法是优先考虑:

  • 使用反射访问编译器生成的委托,或者
  • 创建一个非私有委托(可能是internal),并手动实现事件的add/remove方法(这可以防止编译器生成事件的默认实现)

以下是一个演示后一种技术的示例。

class MyType
{
    internal EventHandler<int> _delegate;
    public event EventHandler<int> MyEvent;
    {
        add { _delegate += value; }
        remove { _delegate -= value; }
    }
}

之前我不确定的一个澄清是,编译器生成的委托与您在代码中声明的事件具有相同的名称(或者至少我的是这样的)。 - Matt Zappitello

16
可以做到,但需要一些技巧......如前所述,编译器会生成事件的实现,包括其支持字段。使用反射可以通过名称检索支持字段,在获得访问权限后,即使你在类本身之外,也可以调用GetInvocationList()
由于您要求使用反射按名称获取事件,我假设您也正在使用反射按名称获取类型 - 我正在制作一个示例,以展示如何完成此操作。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Reflection;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            string typeName = "ConsoleApplication1.SomeClass, ConsoleApplication1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";
            string eventName = "SomeEvent";

            Type declaringType = Type.GetType(typeName);
            object target = Activator.CreateInstance(declaringType);

            EventHandler eventDelegate;
            eventDelegate = GetEventHandler(target, eventName);
            if (eventDelegate == null) { Console.WriteLine("No listeners"); }

            // attach a listener
            SomeClass bleh = (SomeClass)target;
            bleh.SomeEvent += delegate { };
            //

            eventDelegate = GetEventHandler(target, eventName);
            if (eventDelegate == null)
            { 
                Console.WriteLine("No listeners"); 
            }
            else
            { 
                Console.WriteLine("Listeners: " + eventDelegate.GetInvocationList().Length); 
            }

            Console.ReadKey();

        }

        static EventHandler GetEventHandler(object classInstance, string eventName)
        {
            Type classType = classInstance.GetType();
            FieldInfo eventField = classType.GetField(eventName, BindingFlags.GetField
                                                               | BindingFlags.NonPublic
                                                               | BindingFlags.Instance);

            EventHandler eventDelegate = (EventHandler)eventField.GetValue(classInstance);

            // eventDelegate will be null if no listeners are attached to the event
            if (eventDelegate == null)
            {
                return null;
            }

            return eventDelegate;
        }
    }

    class SomeClass
    {
        public event EventHandler SomeEvent;
    }
}

尽管GetEventHandler()方法返回了一个空的eventField,但我发现这个答案非常有帮助。(我怀疑这与我将基于Castle的动态代理传递到程序中而不是代理对象有关。)我正在/已经演示如何使用动态代理“自动”实现INotifyPropertyChanged。 - Wonderbird
1
你可以直接从 GetEventHandler 方法中返回 eventDelegate,而不必进行最后的 null 检查。 - Chris Lees
我收到一个错误,提示“无法将类型为'CallStatechanged'的对象转换为类型'System.EventHandler'” CallStateChanged是我的事件名称。 - Avdhut Vaidya
GetField返回null。GetEvent返回所需的事件,但不允许调用GetValue,因为EventInfo不包含此方法。 - Otto Abnormalverbraucher
将类型转换为“EventHandler”并不够通用,建议使用“return (Delegate)eventField.GetValue(classInstance)” - Ziriax

6

您应该能够通过“事件”获取调用列表。大致上,它将是这样的..

public delegate void MyHandler;
public event MyHandler _MyEvent
public int GetInvocationListLength()
{
   var d = this._MyEvent.GetInvocationList(); //Delegate[]
   return d.Length;
}

9
这只能在声明事件的类内部起作用;他试图在外部这样做。 - Pavel Minaev

0

我使用了你的示例并进行了一些修改。注册事件处理程序会增加调用次数,即使使用两个不同的回调方法(如此处所示)或使用相同的回调方法。

private void SomeMethod()
{
    // Create a new event handler that takes in the function I want to execute when the event fires
    var myEventHandler = new EventHandler(OnPropertyChanged);

    // Get "p1" number events that got hooked up to myEventHandler
    int p1 = myEventHandler.GetInvocationList().Length; // 1

    // Now actually hook an event up
    myEventHandler += OnPropertyChanged2;

    // Re check "p2" number of events hooked up to myEventHandler
    int p2 = myEventHandler.GetInvocationList().Length; // 2

    myEventHandler.Invoke(null, null); 
// each of the registered callback methods are executed once. 
// or if the same callback is used, then twice.
}

private void OnPropertyChanged2(object? sender, EventArgs e)
{}
private void OnPropertyChanged(object? sender, EventArgs e)
{}

正如其他人已经提到的那样,对于eventhandler.GetInvocationList的访问仅限于类本身,您需要公开一个属性或方法来检索委托列表。

像这样:

protected Delegate[]? GetInvocations() => PropertyChanged?.GetInvocationList();

根据您的使用情况将其设置为受保护、内部或两者兼备。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接