ElektroStudios
ElektroStudios

Reputation: 20474

How to check if a Sub is handling a NON-custom event?

For custom events I can check the handler like this:

 If Object.EventNameEvent Is Nothing Then
    MsgBox("Is not handling it.")
 End If

...But how I can do the same, for example, to check a ".click" event of a button which is generated in the designer? This does not work:

If Button1.ClickEvent Is Nothing Then
   MsgBox("Is not handling it.")
End If

UPDATE

Example of my requeriments:

    MsgBox(HasAttachedHandler(MySub, Button1.Click))  ' Expected result: True
    MsgBox(HasAttachedHandler(MyFunc, Button1.Click)) ' Expected result: False

Private Sub MySub() Handles Button1.Click, Button2.Click
    ' bla bla bla
End Sub

Private Function MyFunc() Handles Button2.Click
    ' bla bla bla
End Function

UPDATE 2:

I'm trying to use the @varocarbas solution, but is not doing exactly what I need, so I've tried to make the necessary modifications to get it work. The problem is the event "FontChaged" is not returning the desired result as you can see here:

    Public Class Form1

    Private WithEvents Button1 As New Button

    Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
        ' This is working (Result True):
        MsgBox(HasAttachedHandler(Button1, "Click", "Button1_Click")) ' Result: True

        ' This is not working (Result False):
        MsgBox(HasAttachedHandler(Button1, "FontChanged", "Button1_Click")) ' Expected result: True
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles _
                Button1.Click, _
                Button1.MouseHover, _
                Button1.GotFocus, _
                Button1.Enter, _
                Button1.FontChanged, _
                Button1.AutoSizeChanged

    End Sub

    Private Function HasAttachedHandler(ByVal ctl As Control, ByVal eventname As String, ByVal targetMethod As String) As Boolean

        For Each evnt In ctl.GetType().GetEvents()

            ' Get secret key for the current event:
            Dim curEvent As Reflection.FieldInfo = GetType(Control).GetField("Event" & evnt.Name, Reflection.BindingFlags.NonPublic Or Reflection.BindingFlags.Static)

            If (curEvent IsNot Nothing) Then
                Dim secret As Object = curEvent.GetValue(Nothing)

                ' Retrieve the current event:
                Dim eventsProp As Reflection.PropertyInfo = GetType(System.ComponentModel.Component).GetProperty("Events", Reflection.BindingFlags.NonPublic Or Reflection.BindingFlags.Instance)
                Dim events As System.ComponentModel.EventHandlerList = DirectCast(eventsProp.GetValue(ctl, Nothing), System.ComponentModel.EventHandlerList)

                If (Not IsNothing(events(secret))) AndAlso curEvent.Name.ToLower = "event" & eventname.ToLower Then
                    Dim handler As [Delegate] = events(secret)
                    Dim method As Reflection.MethodInfo = handler.Method
                    If (targetMethod = method.Name) Then Return True
                End If
            End If
        Next

        Return False
    End Function

End Class

Upvotes: 2

Views: 295

Answers (4)

ains
ains

Reputation: 1446

I'll present my unorthodox ways of dealing with this sort of things in .NET framework. From your question...

..."click" event of a button which is generated in the designer...

So I'm going to assume this only applies to only to subclasses of System.Windows.Forms.Control. As many others have demonstrated, the Control manages its event system a bit differently by using an EventHandlerList. For example, with the KeyDown event, it is implemented as follow...

Public Custom Event KeyDown As KeyEventHandler
    AddHandler(ByVal value As KeyEventHandler)
        MyBase.Events.AddHandler(Control.EventKeyDown, value)
    End AddHandler
    RemoveHandler(ByVal value As KeyEventHandler)
        MyBase.Events.RemoveHandler(Control.EventKeyDown, value)
    End RemoveHandler
End Event

Where EventKeyDown is defined as a "key" object used later for retrieving the handlers

Private Shared ReadOnly EventKeyDown As Object

Okay, so far, so good! Idle_Mind and varocarbas both nailed the solution, but you mentioned the FontChanged event does not seem to work with the proposed method. Why? Because both methods would assume there would be a EventFontChanged field object, whereas in reality, it is defined as...

Private Shared ReadOnly EventFont As Object

Now it's clear that assuming...

GetField("Event" + eventName)

...would not work at all.

Since all events are defined in the Control class, we can directly reflect it from there. In order to find the object key, we need to locate the "field reference" from the event handlers (refer to the KeyDown example implementation above). We get the CIL byte codes, loop through it, until we find the opcode which loads a static field reference (ldsfld). The operand of ldsfld is a 32-bit integer that can be resolved into a type. The rest is straightforward as both Idle_Mind and varocarbas have already demonstrated.

Imports System.Reflection
Imports System.ComponentModel
Imports System.Reflection.Emit

Public Class Form1

    Private Shared Sub OnFooEvent(sender As Object, e As EventArgs) Handles Foo.Click, _
                Foo.MouseHover, _
                Foo.GotFocus, _
                Foo.Enter, _
                Foo.FontChanged, _
                Foo.AutoSizeChanged

    End Sub

    Private Sub OnWindowLoad(sender As Object, e As EventArgs) Handles MyBase.Load
        MsgBox(HasHandlers(Foo, "AutoSizeChanged"))
    End Sub

    Private Function HasHandlers(ByVal instance As Object, ByVal arg As String) As Boolean
        Dim baseType = GetType(Control)
        Dim eventInfo = baseType.GetEvent(arg)

        Dim method = eventInfo.AddMethod.GetMethodBody().GetILAsByteArray()
        Dim id As Integer

        For i As Integer = 0 To method.Length - 1
            If method(i) <> OpCodes.Ldsfld.Value Then Continue For
            i += 1
            Dim operand(3) As Byte
            Buffer.BlockCopy(method, i, operand, 0, 4)
            id = BitConverter.ToInt32(operand, 0)
            Exit For
        Next

        If id = 0 Then Return False

        Dim key = baseType.Module.ResolveField(id).GetValue(instance)
        Dim listInfo = GetType(Component).GetProperty("Events", BindingFlags.NonPublic Or BindingFlags.Instance)

        Dim list = listInfo.GetValue(instance)
        Return list(key) IsNot Nothing
    End Function
End Class

This code is not safe, because we have yet to take into consideration of each byte code's operand size! Therefore, the loop might accidentally pick up non-opcode bytes and mistake it for the ldsfld instruction.

Upvotes: 1

user2480047
user2480047

Reputation:

After doing some research, testing and having depleted my knowledge and the freely-available information on this front, I have to give up on the requested fixing. Here are my conclusions:

  • The limitation of the original approach (from Hans Passant) is relying on FieldInfo by assuming that every event has an associated Field, what does not seem to be the case.
  • On the other hand, EventHandlerList seems to do include all the events. Unfortunately, it is a special dictionary where the values can only be retrieved by relying on an Object which defines the given Event (in the (converted) Hans Passant code: Dim secret As Object = curEvent.GetValue(Nothing)).
  • By assuming that the two aforementioned points are right (not too much information on internet), the events shouldn’t be got as FieldInfo, but as EventInfo (Dim curEvent As EventInfo = GetType(Control).GetEvent(evnt.Name)). All the code would be fine with this change except for the calculation of the secret variable and thus the required index for events. Surprisingly, the solution for this problem is not too easy: apparently, only secret can provide the right index (?!).
  • I have found what seem to be potential alternatives: either extracting the information from EventHandlerList without the secret object (code on this line); or replacing secret object/EventHandlerList with other variables, like: PropertyDescriptorCollection, EventDescriptorCollection or MethodInfo.

It would be excellent if someone could correct/extend this post to help (me or others) to clear this issue out.

Upvotes: 0

user2480047
user2480047

Reputation:

Firstly, I want to highlight that the Hans Passant's code is brilliant and that with a so good starting point most of the work is done. I have taken part of the Idle_Mind code and done a bit of research and here you have what you are looking for:

Imports System.Reflection
Imports System.ComponentModel
Public Class Form1

    Private WithEvents Button1 As New Button
    Private Sub Button1_Click(sender As Object, e As System.EventArgs) Handles Button1.Click

    End Sub

     Private Function HasAttachedHandler2(ByVal ctl As Control, ByVal targetMethod As String) As Boolean

        Dim allEvents0 = ctl.GetType().GetEvents()

        For Each evnt In allEvents0
            ' Get secret key for the current event
            Dim curEvent As FieldInfo = GetType(Control).GetField("Event" & evnt.Name, BindingFlags.NonPublic Or BindingFlags.Static)
            If (curEvent IsNot Nothing) Then
                Dim secret As Object = curEvent.GetValue(Nothing)
                ' Retrieve the current event
                Dim eventsProp As PropertyInfo = GetType(Component).GetProperty("Events", BindingFlags.NonPublic Or BindingFlags.Instance)
                Dim events As EventHandlerList = DirectCast(eventsProp.GetValue(ctl, Nothing), EventHandlerList)

                If (Not IsNothing(events(secret))) Then
                    Dim handler As [Delegate] = events(secret)
                    Dim method As MethodInfo = handler.Method
                    If (targetMethod = method.Name) Then
                        Return True
                    End If
                End If
            End If
        Next

   End Function

   Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
          Dim isTheMethodUsed As Boolean = HasAttachedHandler2(Button1, "Button1_Click")
   End Sub

End Class

Upvotes: 0

Idle_Mind
Idle_Mind

Reputation: 39142

Here's my attempt at a conversion and modification of the code posted by @HansPassant:

Imports System.Reflection
Imports System.ComponentModel
Public Class Form1

    Private WithEvents btnA As New Button
    Private WithEvents btnB As New Button

    Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
        btnA.AutoSize = True
        btnA.Text = "Handler Attached"
        btnA.Location = New Point(10, 10)
        Me.Controls.Add(btnA)

        btnB.AutoSize = True
        btnB.Text = "No Handlers Attached"
        Dim pt As Point = btnA.Location
        pt.Offset(btnA.Width, 0)
        btnB.Location = pt
        Me.Controls.Add(btnB)
    End Sub

    Private Sub btnA_Click(sender As Object, e As System.EventArgs) Handles btnA.Click
        Dim btnA_Handled As Boolean = HasAttachedHandler("Click", btnA)
        Dim btnB_Handled As Boolean = HasAttachedHandler("Click", btnB)

        Debug.Print("btnA_Handled = " & btnA_Handled)
        Debug.Print("btnB_Handled = " & btnB_Handled)
    End Sub

    Private Function HasAttachedHandler(ByVal EventName As String, ByVal ctl As Control) As Boolean
        ' Get secret click event key
        Dim eventClick As FieldInfo = GetType(Control).GetField("Event" & EventName, BindingFlags.NonPublic Or BindingFlags.Static)
        Dim secret As Object = eventClick.GetValue(Nothing)
        ' Retrieve the click event
        Dim eventsProp As PropertyInfo = GetType(Component).GetProperty("Events", BindingFlags.NonPublic Or BindingFlags.Instance)
        Dim events As EventHandlerList = DirectCast(eventsProp.GetValue(ctl, Nothing), EventHandlerList)
        Return Not IsNothing(events(secret))
    End Function

End Class

Upvotes: 2

Related Questions