Получить все обработчики событий, объявленные в пользовательском элементе управления - PullRequest
2 голосов
/ 26 июня 2019

Я пытаюсь написать универсальную функцию, которая, учитывая ссылку на элемент управления / компонент и имя события, объявленного в его классе, должна иметь возможность извлекать (через Reflection ) все обработчики событий, зарегистрированные на данный момент для указанного имени события.

Первая и главная проблема, с которой я столкнулся (которая решена, поэтому вы можете игнорировать этот параграф), заключается в том, что все решения (в основном написанные на C #), которые я обнаружил в StackOverflow, ограничены в том смысле, что авторы ищут только объявление поля события в классе System.Windows.Forms.Control, и по этой причине произойдет сбой, например, при попытке получить обработчики события System.Windows.Forms.ToolStripMenuItem.MouseEnter (так как поле события объявлено в классе System.Windows.Forms.ToolStripItem), а также не учитывает именование полей событий класса System.Windows.Forms.Form с подчеркиванием. Итак, я рассмотрел все это, и в настоящее время мое решение работает (или я думаю, что оно работает) для любого класса, который наследуется от System.ComponentModel.Component.

Единственная проблема, с которой я сейчас сталкиваюсь - это когда я объявляю пользовательский тип (который наследуется от Control / UserControl / Компонент / Форма и т. Д.), И я передаю этот тип своей функции. В этом случае я получаю исключение с нулевой ссылкой. Не уверен, что я тут не так делаю ...

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

Исключение возникает в этой строке:

invocationList = eventDelegate.GetInvocationList()

потому что eventDelegate равно нулю.


Чтобы проверить исключение, вы можете взять этот класс в качестве примера:

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

И пример использования, подобный этому:

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

Не уверен, что, может быть, это проблема, связанная с флагами привязки, или, возможно, именование поля события ... но у меня нет этой проблемы с нулевой ссылкой при попытке выполнить то же самое с любым встроенным элементом управления / компонентом класс, который выставляет события, вместо этого TestUserControl класс.

Что я делаю не так? И как мне это исправить ?. Обратите внимание, что эта функция все еще должна быть универсальной.

1 Ответ

2 голосов
/ 28 июня 2019

Благодаря тому, что @ Ханс Пассант предложил в своем комментарии к основному вопросу, это работает, как и ожидалось:

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:
        ' /9593575/poluchit-vse-obrabotchiki-sobytii-obyavlennye-v-polzovatelskom-elemente-upravleniyacomment100177090_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

Также мы можем определить следующее расширение метода для EventInfo тип, действующий как перегрузка метода для 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
...