2

Is there any static event that is triggered when a handle for a Winforms control is created or when it is loaded for the first time?

This is a contrived example:

using (Form f1 = new Form())
{
    f1.HandleCreated += (sender, args) => { MessageBox.Show("hi"); };
    f1.ShowDialog();
}
using (Form f2 = new Form())
{
    f2.HandleCreated += (sender, args) => { MessageBox.Show("hi"); };
    f2.ShowDialog();
}

And this is what I want:

Form.StaticHandleCreated += (sender, args) => { MessageBox.Show("hi"); };
using (Form f1 = new Form())
{
    f1.ShowDialog();
}
using (Form f2 = new Form())
{
    f2.ShowDialog();
}

(I want this because I have several hundred controls, and I need to customize the default behavior of a third party control, and the vendor does not provide a better way. They specifically say to handle the OnLoad event to do the customization, but this is a massive workload.)


Thanks to xtu, here is the working version that doesn't hold onto the forms longer than necessary, to avoid memory leaks.

public class WinFormMonitor : IDisposable
{
    private readonly IntPtr _eventHook;
    private List<Form> _detectedForms = new List<Form>();

    public event Action<Form> NewFormDetected;

    private WinEventDelegate _winEventDelegate;

    public WinFormMonitor()
    {
        _winEventDelegate = WinEventProc;

        _eventHook = SetWinEventHook(
            EVENT_OBJECT_CREATE,
            EVENT_OBJECT_CREATE,
            IntPtr.Zero,
            _winEventDelegate,
            0,
            0,
            WINEVENT_OUTOFCONTEXT);
    }

    public void Dispose()
    {
        _detectedForms.Clear();
        UnhookWinEvent(_eventHook);
    }

    private void WinEventProc(IntPtr hWinEventHook, uint eventType,
        IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
    {
        if (idObject != 0 || idChild != 0) return;
        var currentForms = Application.OpenForms.OfType<Form>().ToList();
        var newForms = currentForms.Except(_detectedForms);
        foreach (var f in newForms)
        {
            NewFormDetected?.Invoke(f);
        }
        _detectedForms = currentForms;
    }


    private const uint EVENT_OBJECT_CREATE = 0x8000;
    private const uint WINEVENT_OUTOFCONTEXT = 0;

    private delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType,
        IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);

    [DllImport("user32.dll")]
    private static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr
            hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess,
        uint idThread, uint dwFlags);

    [DllImport("user32.dll")]
    private static extern bool UnhookWinEvent(IntPtr hWinEventHook);
}
MineR
  • 2,144
  • 12
  • 18
  • What about implement this in a base Form class? then any Form inherits the base Form class will behavior this way. – kennyzx Nov 06 '18 at 07:41
  • That is an option - a find and replace across the solution would work, but I'm hoping there is a more maintainable option, as when when we make a new UserControl we would need to remember to use the other class. – MineR Nov 06 '18 at 07:43
  • No, there is no static event for that, but there are two different solutions to this problem - One is to create a base form where every form that has these control inherits from it, and the other is to have all the customization code encapsulated in a different class, and have a reference to it in all the forms that contains these controls. Without more details, I prefer the second option. – Zohar Peled Nov 06 '18 at 07:45
  • @ZoharPeled, yeah we use the latter quite a bit, but as we add new controls, we have to always remember to add the decorator. I estimate I've spent 8 hours over the past year finding controls which are missing the decorator and creating tickets for them. I'm hoping for something which just "works". – MineR Nov 06 '18 at 07:48
  • 8 hours in the span of an entire year doesn't seem that much.... – Zohar Peled Nov 06 '18 at 07:50
  • I posted something like this (VB.Net here, but it should give the idea): [Add an event to all Forms in a Project](https://stackoverflow.com/questions/51491566/add-an-event-to-all-forms-in-a-project?answertab=active#tab-top). Using UIAutomation, an event handler (static/internal) is added to every Form the moment it's created. Then removed when the Form is diposed. Maybe it's a viable solution in this case. Even though, I haven't tested `OnHandleCreated`, which is raised quite early. But Automation, with objects created inside the same process, should get it right after the instance is built. – Jimi Nov 06 '18 at 09:23
  • A similar C# implementation here: [Run the current application as Single Instance](https://stackoverflow.com/questions/50552592/run-the-current-application-as-single-instance-and-show-the-previous-instance?answertab=active#tab-top). It doesn't register any event handler, but it's the same procedure. It detects when a Window is created and acts on it. The UIAutomation pattern is the same. – Jimi Nov 06 '18 at 09:36
  • This question has the smell of an XY problem. The fact that you specifically requested "static event that is triggered when a handle for a Winforms control is created or when it is loaded for the first time" indicates that you need to configure static properties on the target type before it used. Is this a valid assessment of your true need? – TnTinMn Nov 06 '18 at 15:08

1 Answers1

3

I was inspired by this answer and used SetWinEventHook API to create a new WinForm monitor. Basically, it monitors the EVENT_OBJECT_CREATE event and fires an event whenever a new window is found.

The usage is very simple. You create a new instance and then attach a handler to the NewFormCreated event. Here is an example:

static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    using (var monitor = new WinFormMonitor())
    {
        monitor.NewFormCreated += (sender, form) => { MessageBox.Show($"hi {form.Text}"); };

        Application.Run(new Form1());
    }
}

And here is the source of the WinFormMonitor:

public class WinFormMonitor : IDisposable
{
    private readonly IntPtr _eventHook;
    private readonly IList<int> _detectedFormHashes = new List<int>();

    public event EventHandler<Form> NewFormCreated = (sender, form) => { };

    public WinFormMonitor()
    {
        _eventHook = SetWinEventHook(
            EVENT_OBJECT_CREATE,
            EVENT_OBJECT_CREATE,
            IntPtr.Zero,
            WinEventProc,
            0,
            0,
            WINEVENT_OUTOFCONTEXT);
    }

    public void Dispose()
    {
        _detectedFormHashes.Clear();
        UnhookWinEvent(_eventHook);
    }

    private void WinEventProc(IntPtr hWinEventHook, uint eventType,
        IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
    {
        // filter out non-HWND namechanges... (eg. items within a listbox)
        if (idObject != 0 || idChild != 0) return;
        if (!TryFindForm(hwnd, out var foundForm)) return;

        RaiseIfNewFormFound(foundForm);
    }

    private void RaiseIfNewFormFound(Form foundForm)
    {
        var formHash = foundForm.GetHashCode();
        if (_detectedFormHashes.Contains(formHash)) return;

        NewFormCreated(this, foundForm);
        _detectedFormHashes.Add(formHash);
    }

    private static bool TryFindForm(IntPtr handle, out Form foundForm)
    {
        foreach (Form openForm in Application.OpenForms)
        {
            if (openForm.Handle != handle) continue;
            foundForm = openForm;
            return true;
        }

        foundForm = null;
        return false;
    }

    private const uint EVENT_OBJECT_CREATE = 0x8000;
    private const uint WINEVENT_OUTOFCONTEXT = 0;

    private delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType,
        IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);

    [DllImport("user32.dll")]
    private static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr
            hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess,
        uint idThread, uint dwFlags);

    [DllImport("user32.dll")]
    private static extern bool UnhookWinEvent(IntPtr hWinEventHook);
}
xtu
  • 417
  • 3
  • 12
  • 1
    Thanks, this works great - you've saved me a lot of time. I just made some adjustments to not hold onto all the forms so they can be GCed, and made a member variable for the WinEventProc callback to avoid the exception that the debugger throws. – MineR Nov 07 '18 at 01:09
  • @MineR your sharing your modifications would be appreciated ! – BillW Nov 23 '18 at 12:16
  • 1
    @BillW, I have added the version I'm using in the question. – MineR Nov 26 '18 at 04:21
  • @MineR Thanks for the hint! I have updated the code in my answer so that it now does not cache a list of forms but a list of hash code of previously seen forms. – xtu Nov 28 '18 at 14:15