ElektroStudios
ElektroStudios

Reputation: 20464

How to properly add a Event Handler to the EventHandlerList of a Control?

I'm trying to write a class whose constructor expects a reference to a control / component, and the name of an event within the control class. The purpose is to dynamically subscribe to the specified event from the instance of the referenced control by adding a event handler at run-time:

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

Example usage:

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

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

The problem is that in the example above the Target_Handler method is never reached when in this case the Me.NumericUpDown1.ValueChanged event is raised, unless I invoke the event handler method from code (with: eventHandlerList(eventName).DynamicInvoke(target, Nothing))

What I'm doing wrong?, how to solve my problem?. Thanks in advance.

Upvotes: 1

Views: 678

Answers (2)

ElektroStudios
ElektroStudios

Reputation: 20464

I just would like to share these method extensions I was able to write and make it work for System.ComponentModel.Component class from the suggestions on @Jimi answer about EventInfo usage, and in addition to his answer:

  • 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

Also, I wrote an helper function to get a list of possible variants for an event field name (which is essential if combined with this: 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

Upvotes: 0

Jimi
Jimi

Reputation: 32248

I think this method can be simplified getting the EventInfo object from the Component instance Type, using the GetEvent() method, then adding a new Delegate, using the EventInfo.AddEventHandler() method, passing the delegate type returned by the the EventInfo object itself, in the EventInfo.EventHandlerType property (defined the Event Handler Type this event uses).

The AddEventHandler() method wants the Type Instance to which the new Event delegate is added and a Delegate object: this Delegate can be created using the Delegate.CreateDelegate method, which accepts a handler method as a string.
The Target type is the class Instance where the Delegate is defined, so the current Instance of your ExampleType class.

Something like this should do:
No Exceptions are handled here: verify at least whether .GetEvent(eventName) returns 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

Alternative method to handle a local (static) 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

The you can have:

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))

Then call Dispose() on each object (or a List of these objects) to remove the handler and clean up the local EventHandlerList. The EventHandlerList relative to each Component can be accessed via reflection, but, as mentioned, won't contain ListEntry objects of all event delegates of a Control.

example.Dispose()
example2.Dispose()

Upvotes: 1

Related Questions