在除錯、診斷、動態移除事件、防止重複註冊等情境下,需要知道某個事件目前綁定了哪些委派方法。C# 事件對外僅暴露 +=/-=,內部委派欄位為私有,需透過反射取得。
private Delegate _eventField; + public event EventHandler EventName { add { _eventField = Delegate.Combine(_eventField, value); } remove { ... } }EventHandlerList(鍵值對集合),鍵為 static readonly object EVENT_KEY。EventHandlerList 與 EVENT_KEY,即可取出委派鏈,再用 GetInvocationList() 拆解每個方法。using System;
using System.ComponentModel;
using System.Reflection;
using System.Collections.Generic;
public static class EventInspector
{
///
/// 取得物件指定事件目前註冊的所有處理器方法名稱
///
public static IEnumerable GetEventHandlers(object obj, string eventName)
{
if (obj == null) throw new ArgumentNullException(nameof(obj));
if (string.IsNullOrEmpty(eventName)) throw new ArgumentException("事件名稱不可為空");
// 1. 取得 EventHandlerList 屬性(非公開實例屬性)
PropertyInfo eventsProp = obj.GetType().GetProperty("Events",
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);
if (eventsProp == null)
throw new InvalidOperationException($"型別 {obj.GetType()} 不包含 Events 屬性");
EventHandlerList list = (EventHandlerList)eventsProp.GetValue(obj);
if (list == null) yield break;
// 2. 取得事件對應的鍵欄位(通常命名為 EVENT_事件名)
FieldInfo keyField = obj.GetType().GetField($"EVENT_{eventName.ToUpper()}",
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);
if (keyField == null)
throw new InvalidOperationException($"找不到事件鍵欄位 EVENT_{eventName.ToUpper()}");
object key = keyField.GetValue(null);
Delegate d = list[key];
if (d == null) yield break;
// 3. 拆解委派鏈
foreach (Delegate handler in d.GetInvocationList())
{
yield return $"{handler.Method.DeclaringType?.FullName}.{handler.Method.Name}";
}
}
///
/// 判斷事件是否已註冊特定處理器
///
public static bool IsHandlerRegistered(object obj, string eventName, Delegate targetHandler)
{
foreach (var handler in GetEventHandlers(obj, eventName))
{
// 這裡只比對方法名稱,嚴格比對需取得 Delegate 實例比較
if (handler.EndsWith(targetHandler.Method.Name))
return true;
}
return false;
}
}
public partial class DemoForm : Form
{
public DemoForm()
{
InitializeComponent();
Load += DemoForm_Load1;
Load += DemoForm_Load2;
Load += DemoForm_Load1; // 故意重複註冊
// 檢查 Load 事件註冊情況
var handlers = EventInspector.GetEventHandlers(this, "Load").ToList();
foreach (var h in handlers)
Console.WriteLine($"Load 事件處理器: {h}");
// 輸出範例:
// Load 事件處理器: DemoForm.DemoForm_Load1
// Load 事件處理器: DemoForm.DemoForm_Load2
// Load 事件處理器: DemoForm.DemoForm_Load1
}
private void DemoForm_Load1(object sender, EventArgs e) { }
private void DemoForm_Load2(object sender, EventArgs e) { }
}
若類別未繼承 Component,事件通常直接用欄位儲存:
public class Publisher
{
public event Action OnMessage;
public void Raise(string msg) => OnMessage?.Invoke(msg);
}
public static class CustomEventInspector
{
public static Delegate[] GetInvocationList(object publisher, string eventName)
{
FieldInfo field = publisher.GetType().GetField(eventName,
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
return (field?.GetValue(publisher) as Delegate)?.GetInvocationList() ?? Array.Empty();
}
}
| 限制 | 說明 |
|---|---|
| 僅能檢測實例事件 | 靜態事件鍵欄位通常為 static,需調整 BindingFlags |
| 匿名方法/Lambda 顯示為編譯器生成名稱 | 如 <Main>b__0_0,無法直接對應原始碼行號 |
| 效能開銷 | 反射較慢,避免在熱路徑/高頻迴圈中使用 |
| 非 WinForms 類別需自行尋找欄位名 | 慣例為事件名同名欄位,但非強制 |
透過 EventHandlerList + 事件鍵欄位 + GetInvocationList() 三步驟,即可完整列出 WinForms 控件任一事件的處理器清單。封裝成輔助類別後,可用於單元測試驗證事件綁定、執行期診斷記憶體洩漏(未移除處理器)、防止重複註冊等實務場景。