mardi 25 juin 2019

Get all the event-handlers of a event declared in a custom user-control

I'm trying to write a universal function that, given a reference to a control/component and the name of an event declared on its class, it should be able to retrieve (through Reflection) all the event-handlers currently registered for the specified event name..

The first and main problem I had, is that all the solutions (mostly written in C#) that I found in StackOverflow are limited in the meaning that the authors only look for the event-field declaration in the System.Windows.Forms.Control class, and for that reason will fail for example when trying to retrieve the event-handlers of System.Windows.Forms.ToolStripMenuItem.MouseEnter event (since the event-field is declared in System.Windows.Forms.ToolStripItem class), and also does not take into account event-fields naming of System.Windows.Forms.Form class, which have a underscore. So I covered all this, and currently my solution works (or I think it works) for any class that inherits from System.ComponentModel.Component.

The only problem I'm having now is when I declare a custom type (that inherits from Contol / UserControl / Component / Form class) and I pass that type to my function. In this circumstance I get a null-reference exception. Not sure what I'm doing wrong here...

Public Shared Function GetEventHandlers(component As IComponent, eventName As String) As IReadOnlyCollection(Of [Delegate])

    Dim componentType As Type
    Dim declaringType As Type ' The type on which the event is declared.
    Dim eventInfo As EventInfo
    Dim eventField As FieldInfo = Nothing
    Dim eventFieldValue As Object
    Dim eventsProp As PropertyInfo
    Dim eventsPropValue As EventHandlerList
    Dim eventDelegate As [Delegate]
    Dim invocationList As [Delegate]()

    ' Possible namings for an event field.
    Dim eventFieldNames As String() =
            {
                $"Event{eventName}",            ' Fields declared in 'System.Windows.Forms.Control' class.
                $"EVENT_{eventName.ToUpper()}", ' Fields declared in 'System.Windows.Forms.Form' class.
                $"{eventName}Event"             ' Fields auto-generated.
            }

    Const bindingFlagsEventInfo As BindingFlags =
              BindingFlags.ExactBinding Or
              BindingFlags.Instance Or
              BindingFlags.NonPublic Or
              BindingFlags.Public Or
              BindingFlags.Static

    Const bindingFlagsEventField As BindingFlags =
              BindingFlags.DeclaredOnly Or
              BindingFlags.ExactBinding Or
              BindingFlags.IgnoreCase Or
              BindingFlags.Instance Or
              BindingFlags.NonPublic Or
              BindingFlags.Static

    Const bindingFlagsEventsProp As BindingFlags =
              BindingFlags.DeclaredOnly Or
              BindingFlags.ExactBinding Or
              BindingFlags.Instance Or
              BindingFlags.NonPublic

    Const bindingFlagsEventsPropValue As BindingFlags =
              BindingFlags.Default

    componentType = component.GetType()
    eventInfo = componentType.GetEvent(eventName, bindingFlagsEventInfo)
    If (eventInfo Is Nothing) Then
        Throw New ArgumentException($"Event with name '{eventName}' not found in type '{componentType.FullName}'.", NameOf(eventName))
    End If

    declaringType = eventInfo.DeclaringType

    For Each name As String In eventFieldNames
        eventField = declaringType.GetField(name, bindingFlagsEventField)
        If (eventField IsNot Nothing) Then
            Exit For
        End If
    Next name

    If (eventField Is Nothing) Then
        Throw New ArgumentException($"Field with name 'Event{eventName}', 'EVENT_{eventName.ToUpper()}' or '{eventName}Event' not found in type '{componentType.FullName}'.", NameOf(eventName))
    End If

#If DEBUG Then
    Debug.WriteLine($"Field with name '{eventField.Name}' found in type '{declaringType.FullName}'")
#End If

    eventFieldValue = eventField.GetValue(component)
    eventsProp = GetType(Component).GetProperty("Events", bindingFlagsEventsProp, Type.DefaultBinder, GetType(EventHandlerList), Type.EmptyTypes, Nothing)
    eventsPropValue = DirectCast(eventsProp.GetValue(component, bindingFlagsEventsPropValue, Type.DefaultBinder, Nothing, CultureInfo.InvariantCulture), EventHandlerList)
    eventDelegate = eventsPropValue.Item(eventFieldValue)
    invocationList = eventDelegate.GetInvocationList()

    If (invocationList Is Nothing) Then ' There is no event-handler registered for the specified event.
        Return Enumerable.Empty(Of [Delegate]).ToList()
    End If

    Return invocationList

End Function

The exception occurs at this line:

invocationList = eventDelegate.GetInvocationList()

because eventDelegate is null.


To test the exception, you can take this class as example:

Public Class TestUserControl : Inherits UserControl

    Event TestEvent As EventHandler(Of EventArgs)

    Overridable Sub OnTestEvent()
        If (Me.TestEventEvent Is Nothing) Then
            ' ...
        End If
    End Sub

End Class

And a example usage like this:

Dim ctrl As New TestUserControl()
AddHandler ctrl.TestEvent, Sub()

                           End Sub

Dim handlers As IReadOnlyCollection(Of [Delegate]) = GetEventHandlers(ctrl, NameOf(TestUserControl.TestEvent))
For Each handler As [Delegate] In handlers
    Console.WriteLine(handler.Method.Name)
Next

Not sure if it is an issue related to the binding flags, or the event field naming... but I don't have this problem when trying the same with any built-in control/component class that expose events, instead of that TestUserControl class.

What I'm doing wrong, and how do I fix it?. Please note that this function should still be universal.





Aucun commentaire:

Enregistrer un commentaire