Reputation: 20464
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
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
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