【问题标题】:How to properly add a Event Handler to the EventHandlerList of a Control?如何正确地将事件处理程序添加到控件的 EventHandlerList 中?
【发布时间】:2026-02-10 22:00:03
【问题描述】:

我正在尝试编写一个类,其构造函数需要对控件/组件的引用,以及控件类中的事件名称。目的是通过在运行时添加事件处理程序,从被引用控件的实例中动态订阅指定事件:

Public NotInheritable Class ExampleType(Of T As Component)

    Public ReadOnly Property Target As T

    Public Sub New(target As T, eventName As String)
        Me.Target = target

        Dim eventsProperty As PropertyInfo = 
            GetType(Component).GetProperty("Events", 
                                           BindingFlags.DeclaredOnly Or 
                                           BindingFlags.ExactBinding Or 
                                           BindingFlags.Instance Or 
                                           BindingFlags.NonPublic, 
                                           Type.DefaultBinder, 
                                           GetType(EventHandlerList), 
                                           Type.EmptyTypes, Nothing)
      
        Dim eventHandlerList As EventHandlerList = 
            DirectCast(eventsProperty.GetValue(target, BindingFlags.Default, 
                                               Type.DefaultBinder, Nothing, 
                                               CultureInfo.InvariantCulture), 
                                               EventHandlerList)

        Dim eventHandler As New EventHandler(AddressOf Me.Target_Handler)
        eventHandlerList.AddHandler(eventName, eventHandler)
    End Sub

    Private Sub Target_Handler(sender As Object, e As EventArgs)
        Console.WriteLine("Test")
    End Sub

End Class

示例用法:

Dim target As NumericUpDown = Me.NumericUpDown1
Dim eventName As String = NameOf(NumericUpDown.ValueChanged)

Dim example As New ExampleType(Of NumericUpDown)(target, eventName)

问题在于,在上面的示例中,当在这种情况下引发 Me.NumericUpDown1.ValueChanged 事件时,永远不会到达 Target_Handler 方法,除非我从代码中调用事件处理程序方法(使用:eventHandlerList(eventName).DynamicInvoke(target, Nothing)

我做错了什么?,如何解决我的问题?提前致谢。

【问题讨论】:

    标签: .net vb.net winforms events reflection


    【解决方案1】:

    我认为这个方法可以简化为从Component实例Type中获取EventInfo对象,使用GetEvent()方法,然后添加一个新的Delegate,使用EventInfo.AddEventHandler()方法,传递返回的委托类型EventInfo 对象本身,在 EventInfo.EventHandlerType 属性中(定义了此事件使用的事件处理程序类型)。

    AddEventHandler() 方法需要添加新事件委托的类型实例和委托对象:可以使用Delegate.CreateDelegate 方法创建此委托,该方法接受处理程序方法作为字符串。
    Target 类型是定义了 Delegate 的类 Instance,因此是 ExampleType 类的当前 Instance。

    应该这样做:
    这里不处理异常:至少验证.GetEvent(eventName)是否返回null

    Imports System.ComponentModel
    Imports System.Reflection
    
    Public NotInheritable Class ExampleType(Of T As Component)
    
        Public ReadOnly Property Target As T
    
        Public Sub New(target As T, eventName As String)
            Me.Target = target
    
            Dim eventNfo = target.GetType().GetEvent(eventName)
            ' Or
            ' Dim eventNfo = GetType(T).GetEvent(eventName)
    
            eventNfo.AddEventHandler(target, [Delegate].CreateDelegate(
                eventNfo.EventHandlerType, Me, NameOf(Me.Target_Handler))
           ' Or
           ' eventNfo.AddEventHandler(Target, New EventHandler(AddressOf Target_Handler))
        End Sub
    
        Private Sub Target_Handler(sender As Object, e As EventArgs)
            Console.WriteLine("Test")
        End Sub
    End Class
    

    处理本地(静态)EventHandlerList 的替代方法:

    Public NotInheritable Class ExampleType(Of T As Component)
        Implements IDisposable
    
        Private Shared m_EventList As EventHandlerList = New EventHandlerList()
        Private m_Delegate As [Delegate] = Nothing
        Private m_Event As Object = Nothing
    
        Public Sub New(target As T, eventName As String)
            Me.Target = target
            AddEventHandler(eventName)
        End Sub
    
        Public ReadOnly Property Target As T
    
        Private Sub AddEventHandler(eventName As String)
            m_Event = eventName
            Dim eventNfo = Target.GetType().GetEvent(eventName)
            m_Delegate = [Delegate].CreateDelegate(eventNfo.EventHandlerType, Me, NameOf(Me.Target_Handler))
            m_EventList.AddHandler(m_Event, m_Delegate)
            eventNfo.AddEventHandler(Target, m_EventList(m_Event))
        End Sub
    
        Private Sub RemoveEventHandler()
            Dim eventNfo = Target.GetType().GetEvent(m_Event.ToString())
            eventNfo?.RemoveEventHandler(Target, m_EventList(m_Event))
            m_EventList.RemoveHandler(m_Event, m_Delegate)
            m_Delegate = Nothing
        End Sub
    
        Private Sub Target_Handler(sender As Object, e As EventArgs)
            Console.WriteLine("Test")
        End Sub
    
        Public Sub Dispose() Implements IDisposable.Dispose
            Dispose(True)
            GC.SuppressFinalize(Me)
        End Sub
        Public Sub Dispose(disposing As Boolean)
            If disposing AndAlso m_Delegate IsNot Nothing Then
                RemoveEventHandler()
            End If
        End Sub
    End Class
    

    你可以拥有:

    Private example As ExampleType(Of Component)
    Private example2 As ExampleType(Of Component)
    
    ' [...]
    
    example = New ExampleType(Of Component)(Me.NumericUpDown1, NameOf(NumericUpDown.ValueChanged))
    example2 = New ExampleType(Of Component)(Me.TextBox1, NameOf(TextBox.TextChanged))
    

    然后在每个对象(或这些对象的 List)上调用 Dispose() 以移除处理程序并清理本地 EventHandlerList。可以通过反射访问相对于每个组件的 EventHandlerList,但如前所述,它不会包含 Control 的所有事件委托的 ListEntry 对象。

    example.Dispose()
    example2.Dispose()
    

    【讨论】:

    • 非常感谢,尽管您的解决方案并没有解决我对为什么我的方法不能按预期工作的疑问,而事实是我非常想知道这个问题的答案,因为有在其他项目中我想检索 EventHandlerList 并能够为其添加事件处理程序的一点,因此如果您可以考虑为此添加答案,那将有很大帮助,但我不能要求您更多,因为您的方法比我的更简单、更干净,它按预期工作并解决了我的问题。 +1
    • 例如,ValueChanged 事件不属于基类,它由 NumericUpDown 控件定义,并且从未添加到 Events (EventHandlerList) 对象:本地 @改为使用 987654340@,因此您不会在 Property 的 Handler 中找到它。之后,您还必须引发事件,而不仅仅是添加订阅。您必须知道哪个事件属于哪个类以及在这种情况下是否使用了EventHandlerList。或者做类似this 的事情。它会在 .Net 5 中工作吗?
    • 顺便说一句,使用BindingFlags.DeclaredOnly Or BindingFlags.ExactBinding,您将找不到Events 属性。
    【解决方案2】:

    我只想分享这些我能够编写的方法扩展,并根据@Jimi 回答关于EventInfo 用法的建议,使其适用于System.ComponentModel.Component 类,以及他的回答:

    • Component.GetEvent(String, Boolean) As EventInfo

    • Component.TryGetEvent(String, Boolean) As EventInfo

    • Component.GetEvents(Boolean) As IReadOnlyCollection(Of EventInfo)

    • Component.GetSubscribedEvents(Boolean) As IReadOnlyCollection(Of EventInfo)


    ''' ----------------------------------------------------------------------------------------------------
    ''' <summary>
    ''' Gets all the events declared in the source <see cref="Component"/>.
    ''' </summary>
    ''' ----------------------------------------------------------------------------------------------------
    ''' <example> This is a code example.
    ''' <code language="VB.NET">
    ''' Dim ctrl As New Button()
    ''' Dim events As IReadOnlyCollection(Of EventInfo) = ctrl.GetEvents(declaredOnly:=True)
    ''' 
    ''' For Each ev As EventInfo In events
    '''     Console.WriteLine($"Event Name: {ev.Name}")
    ''' Next
    ''' </code>
    ''' </example>
    ''' ----------------------------------------------------------------------------------------------------
    ''' <param name="component">
    ''' The source <see cref="Component"/>.
    ''' </param>
    ''' 
    ''' <param name="declaredOnly">
    ''' If <see langword="True"/>, only events declared at the 
    ''' level of the supplied type's hierarchy should be considered. 
    ''' Inherited events are not considered.
    ''' </param>
    ''' ----------------------------------------------------------------------------------------------------
    ''' <returns>
    ''' All the events declared in the source <see cref="Component"/>
    ''' </returns>
    ''' ----------------------------------------------------------------------------------------------------
    <DebuggerStepThrough>
    <Extension>
    <EditorBrowsable(EditorBrowsableState.Always)>
    Public Function GetEvents(component As Component, declaredOnly As Boolean) As IReadOnlyCollection(Of EventInfo)
    
        If declaredOnly Then
            Const flags As BindingFlags = BindingFlags.DeclaredOnly Or
                                          BindingFlags.Instance Or
                                          BindingFlags.Public Or
                                          BindingFlags.NonPublic
    
            Return (From ev As EventInfo In component.GetType().GetEvents(flags)
                    Order By ev.Name Ascending).ToList()
    
        Else
            Return (From ev As EventInfo In component.GetType().GetEvents()
                    Order By ev.Name Ascending).ToList()
        End If
    
    End Function
    
    ''' ----------------------------------------------------------------------------------------------------
    ''' <summary>
    ''' Gets a list of events declared in the source <see cref="Component"/>  
    ''' that are subscribed to a event-handler.
    ''' </summary>
    ''' ----------------------------------------------------------------------------------------------------
    ''' <example> This is a code example.
    ''' <code language="VB.NET">
    ''' Dim ctrl As New Button()
    ''' AddHandler ctrl.Click, Sub() Console.WriteLine("Test")
    ''' AddHandler ctrl.DoubleClick, Sub() Console.WriteLine("Test") ' declaredOnly:=True
    ''' 
    ''' Dim subscribedEvents As IReadOnlyCollection(Of EventInfo) = ctrl.GetSubscribedEvents(declaredOnly:=True)
    ''' For Each ev As EventInfo In subscribedEvents
    '''     Console.WriteLine($"Event Name: {ev.Name}")
    ''' Next ev
    ''' </code>
    ''' </example>
    ''' ----------------------------------------------------------------------------------------------------
    ''' <param name="component">
    ''' The source <see cref="Component"/>.
    ''' </param>
    ''' 
    ''' <param name="declaredOnly">
    ''' If <see langword="True"/>, only events declared at the 
    ''' level of the supplied type's hierarchy should be considered. 
    ''' Inherited events are not considered.
    ''' </param>
    ''' ----------------------------------------------------------------------------------------------------
    ''' <returns>
    ''' A list of events declared in the source <see cref="Component"/> 
    ''' that are subscribed to a event-handler.
    ''' </returns>
    ''' ----------------------------------------------------------------------------------------------------
    <DebuggerStepThrough>
    <Extension>
    <EditorBrowsable(EditorBrowsableState.Always)>
    Public Function GetSubscribedEvents(component As Component, declaredOnly As Boolean) As IReadOnlyCollection(Of EventInfo)
    
        Dim events As IReadOnlyCollection(Of EventInfo) =
            GetEvents(component, declaredOnly)
    
        Dim subscribedEvents As New List(Of EventInfo)
        For Each ev As EventInfo In events
            If component.GetEventHandlers(ev.Name).Any() Then
                subscribedEvents.Add(ev)
            End If
        Next ev
    
        Return subscribedEvents
    
    End Function
    
    ''' ----------------------------------------------------------------------------------------------------
    ''' <summary>
    ''' Gets a <see cref="EventInfo"/> that match the specified event name 
    ''' declared in the source <see cref="Component"/>.
    ''' </summary>
    ''' ----------------------------------------------------------------------------------------------------
    ''' <example> This is a code example.
    ''' <code language="VB.NET">
    ''' Dim ctrl As New Button()
    ''' 
    ''' Dim ev As EventInfo
    ''' Try
    '''     ev = ctrl.GetEvent(NameOf(Button.MouseDoubleClick), declaredOnly:=True)
    '''     Console.WriteLine($"Event Name: {ev.Name}")
    ''' 
    ''' Catch ex As ArgumentException When ex.ParamName = "eventName"
    '''     Console.WriteLine($"No event found matching the supplied name: {ev.Name}")
    ''' 
    ''' End Try
    ''' </code>
    ''' </example>
    ''' ----------------------------------------------------------------------------------------------------
    ''' <param name="component">
    ''' The source <see cref="Component"/>.
    ''' </param>
    ''' 
    ''' <param name="eventName">
    ''' The name of the event.
    ''' </param>
    ''' 
    ''' <param name="declaredOnly">
    ''' If <see langword="True"/>, only events declared at the 
    ''' level of the supplied type's hierarchy should be considered. 
    ''' Inherited events are not considered.
    ''' </param>
    ''' ----------------------------------------------------------------------------------------------------
    ''' <exception cref="ArgumentException">
    ''' No event found matching the supplied name: {eventName},
    ''' </exception>
    ''' ----------------------------------------------------------------------------------------------------
    ''' <returns>
    ''' The resulting <see cref="EventInfo"/>
    ''' </returns>
    ''' ----------------------------------------------------------------------------------------------------
    <DebuggerStepThrough>
    <Extension>
    <EditorBrowsable(EditorBrowsableState.Always)>
    Public Function GetEvent(component As Component, eventName As String, declaredOnly As Boolean) As EventInfo
    
        Dim ev As EventInfo = TryGetEvent(component, eventName, declaredOnly)
        If ev Is Nothing Then
            Throw New ArgumentException($"No event found matching the supplied name: {eventName}", paramName:=NameOf(eventName))
        End If
    
        Return ev
    
    End Function
    
    ''' ----------------------------------------------------------------------------------------------------
    ''' <summary>
    ''' Tries to get a <see cref="EventInfo"/> that match the specified event name 
    ''' declared in the source <see cref="Component"/>.
    ''' </summary>
    ''' ----------------------------------------------------------------------------------------------------
    ''' <example> This is a code example.
    ''' <code language="VB.NET">
    ''' Dim ctrl As New Button()
    ''' Dim ev As EventInfo = ctrl.TryGetEvent(NameOf(Button.MouseDoubleClick), declaredOnly:=True)
    ''' If ev IsNot Nothing Then
    '''     Console.WriteLine($"Event Name: {ev.Name}")
    ''' Else
    '''     Console.WriteLine($"No event found matching the supplied name: {ev.Name}")
    ''' End If
    ''' </code>
    ''' </example>
    ''' ----------------------------------------------------------------------------------------------------
    ''' <param name="component">
    ''' The source <see cref="Component"/>.
    ''' </param>
    ''' 
    ''' <param name="eventName">
    ''' The name of the event.
    ''' </param>
    ''' 
    ''' <param name="declaredOnly">
    ''' If <see langword="True"/>, only events declared at the 
    ''' level of the supplied type's hierarchy should be considered. 
    ''' Inherited events are not considered.
    ''' </param>
    ''' ----------------------------------------------------------------------------------------------------
    ''' <returns>
    ''' The resulting <see cref="EventInfo"/>, 
    ''' or <see langword="Nothing"/> (null) if no event found matching the supplied name.
    ''' </returns>
    ''' ----------------------------------------------------------------------------------------------------
    <DebuggerStepThrough>
    <Extension>
    <EditorBrowsable(EditorBrowsableState.Always)>
    Public Function TryGetEvent(component As Component, eventName As String, declaredOnly As Boolean) As EventInfo
    
        Dim events As IReadOnlyCollection(Of EventInfo) =
            GetEvents(component, declaredOnly)
    
        Return (From ev As EventInfo In events
                Where ev.Name.Equals(eventName, StringComparison.OrdinalIgnoreCase)
               ).SingleOrDefault()
    
    End Function
    

    另外,我编写了一个辅助函数来获取事件字段名称的可能变体列表(如果与此结合使用,这是必不可少的:Get all the event-handlers of a event declared in a custom user-control

    ''' ----------------------------------------------------------------------------------------------------
    ''' <summary>
    ''' Gets a list of possible variants for an event field name.
    ''' <para></para>
    ''' Example event name: ValueChanged
    ''' <para></para>
    ''' Result field names: EventValueChanged, EventValue, EVENT_VALUECHANGED, EVENT_VALUE, ValueChangedEvent, ValueEvent
    ''' </summary>
    ''' ----------------------------------------------------------------------------------------------------
    ''' <param name="eventName">
    ''' The name of the event.
    ''' <para></para>
    ''' Note: the name is case-insensitive.
    ''' </param>
    ''' 
    ''' <returns>
    ''' A list of possible variants for an event field name.
    ''' </returns>
    ''' ----------------------------------------------------------------------------------------------------
    <DebuggerStepThrough>
    Private Function GetEventFieldNameVariants(eventName As String) As IReadOnlyCollection(Of String)
    
        ' Example input event name: 
        '   ValueChanged
        '
        ' Resulting field names: 
        '   EventValueChanged, EventValue   (Fields declared in 'System.Windows.Forms.Control' class.)
        '   EVENT_VALUECHANGED, EVENT_VALUE (Fields declared in 'System.Windows.Forms.Form' class.)
        '   ValueChangedEvent, ValueEvent   (Fields (auto-generated) declared in other classes.)
    
        Dim names As New List(Of String) From {
            $"Event{eventName}",            ' EventName
            $"EVENT_{eventName.ToUpper()}", ' EVENT_NAME
            $"{eventName}Event"             ' NameEvent
        }
    
        If eventName.EndsWith("Changed", StringComparison.OrdinalIgnoreCase) Then
            names.Add($"Event{eventName.RemoveEnd(0, 7)}")            ' EventName
            names.Add($"EVENT_{eventName.RemoveEnd(0, 7).ToUpper()}") ' EVENT_NAME
            names.Add($"{eventName.RemoveEnd(0, 7)}Event")            ' NameEvent
        End If
    
        Return names
    
    End Function
    
    Public Function RemoveEnd(input As String, startIndex As Integer, length As Integer) As String
    
        Return source.Remove((input.Length - startIndex - length), length)
    
    End Function
    

    【讨论】: