Add an event to all Forms in a Project

If I want to display the size of every Form in my Project in the Form's Title what will be the best approach?
I don't want to manually put a event handler in every Form.
I want the process to be automatic.
Something like a overloaded Load() event that adds a handler on the resize event.


Solution 1:

Here is an attempt to implement an Automation solution to the problem.

The problem:
Attach one or more Event Handlers to each existing Form in a Project (or a subset of them), without editing/modifying these classes existing code.

A possible solution comes from UIAutomation, which provides means to detect when a new Window is opened and reports the event to the subscribers of its own Automation.AddAutomationEventHandler, when the EventId of its AutomationEvent is set to a WindowPattern pattern.
The AutomationElement member must be set to AutomationElement.RootElement and the Scope member to TreeScope.SubTree.

Automation, for each AutomationElement that raises the AutomationEvent, reports:

  • the Element.Name (corresponding to the Windows Title)
  • the Process ID
  • the Window Handle (as an Integer value)

These values are quite enough to identify a Window that belongs to the current process; the Window handle allows to identify the opened Form instance, testing the Application.OpenForms() collection.

When the Form is singled out, a new Event Handler can be attached to an Event of choice.

By expanding this concept, it's possible to create a predefined List of Events and a List of Forms to attach these events to.
Possibly, with a class file to include in a Project when required.

As a note, some events will not be meaningful in this scenario, because the Automation reports the opening of a Window when it is already shown, thus the Load() and Shown() events belong to the past.


I've tested this with a couple of events (Form.Resize() and Form.Activate()), but in the code here I'm using just .Resize() for simplicity.

This is a graphics representation of the process.
Starting the application, the Event Handler is not attached to the .Resize() event.
It's just because a Boolean fields is set to False.
Clicking a Button, the Boolean field is set to True, enabling the registration of the Event Handler.
When the .Resize() event is registered, all Forms' Title will report the current size of the Window.

Global Handlers

Test environment:
Visual Studio 2017 pro 15.7.5
.Net FrameWork 4.7.1

Imported Namespaces:
System.Windows.Automation

Reference Assemblies:
UIAutomationClient
UIAutomationTypes

MainForm code:

Imports System.Diagnostics
Imports System.Windows
Imports System.Windows.Automation

Public Class MainForm

    Friend GlobalHandlerEnabled As Boolean = False
    Protected Friend FormsHandler As List(Of Form) = New List(Of Form)
    Protected Friend ResizeHandler As EventHandler

    Public Sub New()

        InitializeComponent()

        ResizeHandler =
                Sub(obj, args)
                    Dim CurrentForm As Form = TryCast(obj, Form)
                    CurrentForm.Text = CurrentForm.Text.Split({" ("}, StringSplitOptions.None)(0) &
                                                               $" ({CurrentForm.Width}, {CurrentForm.Height})"
                End Sub

        Automation.AddAutomationEventHandler(WindowPattern.WindowOpenedEvent,
            AutomationElement.RootElement,
                TreeScope.Subtree,
                    Sub(UIElm, evt)
                        If Not GlobalHandlerEnabled Then Return
                        Dim element As AutomationElement = TryCast(UIElm, AutomationElement)
                        If element Is Nothing Then Return

                        Dim NativeHandle As IntPtr = CType(element.Current.NativeWindowHandle, IntPtr)
                        Dim ProcessId As Integer = element.Current.ProcessId
                        If ProcessId = Process.GetCurrentProcess().Id Then
                            Dim CurrentForm As Form = Nothing
                            Invoke(New MethodInvoker(
                                Sub()
                                    CurrentForm = Application.OpenForms.
                                           OfType(Of Form)().
                                           FirstOrDefault(Function(f) f.Handle = NativeHandle)
                                End Sub))

                            If CurrentForm IsNot Nothing Then
                                Dim FormName As String = FormsHandler.FirstOrDefault(Function(f) f?.Name = CurrentForm.Name)?.Name
                                If Not String.IsNullOrEmpty(FormName) Then
                                    RemoveHandler CurrentForm.Resize, ResizeHandler
                                    FormsHandler.Remove(FormsHandler.Where(Function(fn) fn.Name = FormName).First())
                                End If
                                Invoke(New MethodInvoker(
                                Sub()
                                    CurrentForm.Text = CurrentForm.Text & $" ({CurrentForm.Width}, {CurrentForm.Height})"
                                End Sub))

                                AddHandler CurrentForm.Resize, ResizeHandler
                                FormsHandler.Add(CurrentForm)
                            End If
                        End If
                    End Sub)
    End Sub


    Private Sub btnOpenForm_Click(sender As Object, e As EventArgs) Handles btnOpenForm.Click
        Form2.Show(Me)
    End Sub

    Private Sub btnEnableHandlers_Click(sender As Object, e As EventArgs) Handles btnEnableHandlers.Click
        GlobalHandlerEnabled = True
        Me.Hide()
        Me.Show()
    End Sub

    Private Sub btnDisableHandlers_Click(sender As Object, e As EventArgs) Handles btnDisableHandlers.Click
        GlobalHandlerEnabled = False
        If FormsHandler IsNot Nothing Then
            For Each Item As Form In FormsHandler
                RemoveHandler Item.Resize, ResizeHandler
                Item = Nothing
            Next
        End If
        FormsHandler = New List(Of Form)
        Me.Text = Me.Text.Split({" ("}, StringSplitOptions.RemoveEmptyEntries)(0)
    End Sub
End Class

Note:
This previous code is placed inside the app Starting Form (for testing), but it might be preferable to have a Module to include in the Project when needed, without touching the current code.

To get this to work, add a new Module (named Program) which contains a Public Sub Main(), and change the Project properties to start the application from Sub Main() instead of a Form.
Remove the check mark on Use Application Framework and choose Sub Main from the Startup object Combo.

All the code can be transferred to the Sub Main proc with a couple of modifications:

Imports System
Imports System.Diagnostics
Imports System.Windows
Imports System.Windows.Forms
Imports System.Windows.Automation

Module Program

    Friend GlobalHandlerEnabled As Boolean = True
    Friend FormsHandler As List(Of Form) = New List(Of Form)
    Friend ResizeHandler As EventHandler

    Public Sub Main()

        Application.EnableVisualStyles()
        Application.SetCompatibleTextRenderingDefault(False)

        Dim MyMainForm As MainForm = New MainForm()

        ResizeHandler =
                Sub(obj, args)
                    Dim CurrentForm As Form = TryCast(obj, Form)
                    CurrentForm.Text = CurrentForm.Text.Split({" ("}, StringSplitOptions.None)(0) &
                                                               $" ({CurrentForm.Width}, {CurrentForm.Height})"
                End Sub

        Automation.AddAutomationEventHandler(WindowPattern.WindowOpenedEvent,
            AutomationElement.RootElement,
                TreeScope.Subtree,
                    Sub(UIElm, evt)
                        If Not GlobalHandlerEnabled Then Return
                        Dim element As AutomationElement = TryCast(UIElm, AutomationElement)
                        If element Is Nothing Then Return

                        Dim NativeHandle As IntPtr = CType(element.Current.NativeWindowHandle, IntPtr)
                        Dim ProcessId As Integer = element.Current.ProcessId
                        If ProcessId = Process.GetCurrentProcess().Id Then
                            Dim CurrentForm As Form = Nothing
                            If Not MyMainForm.IsHandleCreated Then Return
                            MyMainForm.Invoke(New MethodInvoker(
                                Sub()
                                    CurrentForm = Application.OpenForms.
                                           OfType(Of Form)().
                                           FirstOrDefault(Function(f) f.Handle = NativeHandle)
                                End Sub))
                            If CurrentForm IsNot Nothing Then
                                Dim FormName As String = FormsHandler.FirstOrDefault(Function(f) f?.Name = CurrentForm.Name)?.Name
                                If Not String.IsNullOrEmpty(FormName) Then
                                    RemoveHandler CurrentForm.Resize, ResizeHandler
                                    FormsHandler.Remove(FormsHandler.Where(Function(fn) fn.Name = FormName).First())
                                End If

                                AddHandler CurrentForm.Resize, ResizeHandler
                                FormsHandler.Add(CurrentForm)

                                CurrentForm.Invoke(New MethodInvoker(
                                Sub()
                                    CurrentForm.Text = CurrentForm.Text & $" ({CurrentForm.Width}, {CurrentForm.Height})"
                                End Sub))
                            End If
                        End If
                    End Sub)

        Application.Run(MyMainForm)

    End Sub

End Module