Reputation: 20474
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 (which is solved, so you can ignore this paragraph), 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 Control / UserControl / Component / Form etc. 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 '{declaringType.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(e As EventArgs)
If (Me.TestEventEvent IsNot Nothing) Then
RaiseEvent TestEvent(Me, e)
End If
End Sub
End Class
And a example usage like this:
Dim ctrl As New TestUserControl()
AddHandler ctrl.TestEvent, Sub()
Debug.WriteLine("Hello World!")
End Sub
Dim handlers As IReadOnlyCollection(Of [Delegate]) =
GetEventHandlers(ctrl, NameOf(TestUserControl.TestEvent))
For Each handler As [Delegate] In handlers
Console.WriteLine($"Method Name: {handler.Method.Name}")
Next
Not sure if maybe it is an issue related to the binding flags, or maybe the event field naming... but I don't have this null-reference object issue 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.
Upvotes: 2
Views: 1205
Reputation: 20474
Thanks to what @Hans Passant suggested in his commentary in the main question, this is working as expected:
Public Function GetEventHandlers(component As IComponent, eventName As String) As IReadOnlyCollection(Of [Delegate])
Dim componentType As Type = component.GetType()
' Find event declaration in the source type.
Dim eventInfo As EventInfo = componentType.GetEvent(eventName, BindingFlags.ExactBinding Or BindingFlags.Instance Or BindingFlags.NonPublic Or BindingFlags.Public Or BindingFlags.Static)
If (eventInfo Is Nothing) Then
Throw New ArgumentException($"Event with name '{eventName}' not found in type '{componentType.FullName}'.", NameOf(eventName))
End If
' The type on which the event is declared.
Dim declaringType As Type = eventInfo.DeclaringType
' Find event-field declaration in the declaring type.
Dim eventField As FieldInfo = Nothing
' 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) declared in other classes.
}
For Each name As String In eventFieldNames
eventField = declaringType.GetField(name, BindingFlags.DeclaredOnly Or BindingFlags.ExactBinding Or BindingFlags.IgnoreCase Or BindingFlags.Instance Or BindingFlags.NonPublic Or BindingFlags.Static)
If (eventField IsNot Nothing) Then
Exit For
End If
Next name
If (eventField Is Nothing) Then
Throw New ArgumentException($"Field with name '{String.Join("' or '", eventFieldNames)}' not found in declaring type '{declaringType.FullName}'.", NameOf(eventName))
End If
#If DEBUG Then
Debug.WriteLine($"Field with name '{eventField.Name}' found in declaring type '{declaringType.FullName}'")
#End If
Dim eventFieldValue As object = eventField.GetValue(component)
If TypeOf eventFieldValue Is MulticastDelegate
' See @Hans Passant comment:
' https://stackoverflow.com/questions/56763972/get-all-the-event-handlers-of-a-event-declared-in-a-custom-user-control?noredirect=1#comment100177090_56763972
Return DirectCast(eventFieldValue, MulticastDelegate).GetInvocationList()
End If
Dim eventsProp 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 eventsPropValue As EventHandlerList = DirectCast(eventsProp.GetValue(component, BindingFlags.Default, Type.DefaultBinder, Nothing, CultureInfo.InvariantCulture), EventHandlerList)
Dim eventDelegate As [Delegate] = eventsPropValue.Item(eventFieldValue)
Dim invocationList As [Delegate]() = 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
Also we can define the next method extension for EventInfo type to act as a method overload for EventInfo.RemoveEventHandler(Object, Delegate):
''' ----------------------------------------------------------------------------------------------------
''' <summary>
''' Removes an event handler from an event source.
''' </summary>
''' ----------------------------------------------------------------------------------------------------
''' <example> This is a code example.
''' <code>
''' Public Class Form1
'''
''' Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Shown
''' Dim target As Form = Me
''' Dim eventInfo As EventInfo = target.GetType().GetEvent(NameOf(Form.Click))
''' eventInfo.RemoveEventHandler(target, NameOf(Me.Form1_Click))
''' End Sub
'''
''' Private Sub Form1_Click(sender As Object, e As EventArgs) Handles MyBase.Click
''' MsgBox(MethodBase.GetCurrentMethod().Name)
''' End Sub
'''
''' End Class
''' </code>
''' </example>
''' ----------------------------------------------------------------------------------------------------
''' <param name="eventInfo">
''' The event information.
''' </param>
'''
''' <param name="target">
''' The event source.
''' </param>
'''
''' <param name="handlerName">
''' The name of the delegate to be disassociated from the events raised by <paramref name="target"/>.
''' <para></para>
''' Note that the name is case-sensitive.
''' </param>
''' ----------------------------------------------------------------------------------------------------
<Extension>
Public Sub RemoveEventHandler(eventInfo As EventInfo, target As IComponent, handlerName As String)
If String.IsNullOrWhiteSpace(handlerName)
Throw New ArgumentNullException(NameOf(handlerName))
End If
For each handler As [Delegate] in GetEventHandlers(target, eventInfo.Name)
If handler.Method.Name.Equals(handlerName, StringComparison.Ordinal)
eventInfo.RemoveEventHandler(target, handler)
Exit Sub
End If
Next handler
Throw New ArgumentException($"No delegate was found with the specified name: '{handlerName}'", NameOf(handlerName))
End Sub
Upvotes: 3