Testing

Beyond the Event Horizon: WinForms Event System

OLYMPUS DIGITAL CAMERAYou remember WinForms don’t you? WinForms applications are still around, in huge quantities. Some of us still need to maintain or rehabilitate WinForms applications, and testing events is particularly important when working with WinForms. The WinForms designer takes responsibility for wiring up many events for you, then discourages you from thinking too hard about what it did. Not only does it put the event wiring code in a semi-hidden Designer file, it also encloses the code it doesn’t want you touch in a region and adds a large comment explaining that you really should slowly back away before you hurt yourself.

Even if you heed all of the warnings and stay out of that code, its still far too easy to add useless empty handlers from within the designer, or worse—accidentally unhook your events.  Unit testing your handler implementations wont help, nothing in the internal implementation changes when a handler is wired (or unwired) from an event.  You need something like the event tests I’ve been working on to make sure that the proper code is invoked when you perform an action on the GUI (for example, clicking a button).

To build a system for inventorying events on WinForms controls, you should have a good idea how events work elsewhere in the .NET Framework.  Here’s a short list of posts on this blog which should get you up to speed:

  1. Beyond the Event Horizon: Delegate Basics” — Explores the useful Delegate.GetInvocationListmethod.
  2. Beyond the Event Horizon: Event Basics” — Explains the relationship between Delegates and Events, and how the compiler implements simple events.
  3. Beyond the Event Horizon: Events You Don’t Own” — Shows how to use reflection to retrieve delegates for events declared on classes you can’t or won’t change.
  4. Beyond the Event Horizon: Event Complications” — Completes the toolset introduced in part 3 by handling inherited events and events of any delegate type.

I’ve made the code associated with these articles available on GitHub.  The code described in this article is a reimplementation of features available in a free, open source testing library called ApprovalTests, which you can download from SourceForge or NuGet and start using immediately.  By using ApprovalTests, you can save yourself the bother of cut-and-paste, and the maintenance headache of keeping your own copy of the code.  Need help getting started with ApprovalTests?  Check out Llewellyn Falco’s fantastic series on YouTube and you will be up to speed in no time.

So, will my event testing system work on WinForms? On the surface it seems like it will, but lets give it a try and see what happens.

Start With A Failing Test

First I’ll create a simple Form.  I won’t be using the designer.  Some of you might have never ventured into the “forbidden zone” inside the Designer file.  I’ll implement the Form by hand, and this will give you your first bit of insight into how events work in WinForms.

First I need to add a reference to System.Windows.Forms and import the namespace having the same name.

using System.Windows.Forms;

public class DemoForm : Form
{
}

While I design, I’ll use WinFormsApprovals.Verify to get feedback as I work.

[TestClass]
public class WinFormsDemo
{
    [TestMethod]
    public void VerifyDemoFormView()
    {
        WinFormsApprovals.Verify(new DemoForm());
    }
}

This test will continue to fail until I approve it, so each time I run it I’ll see what DemoForm looks like.  Now that I can see what I’m doing I can quickly add a few controls to the form. For example, here I add a Button.

private Button button1;

public DemoForm()
{
    this.button1 = new Button();
    this.button1.Text = "Click Me!";
    this.Controls.Add(this.button1);
}

And I can see what this looks like in TortoiseIDiff:

WinFormsEvents

And here’s a CheckBox:

private CheckBox checkBox1;

public DemoForm()
{
    this.button1 = new Button();
    this.button1.Text = "Click Me!";
    this.Controls.Add(this.button1);

    this.checkBox1 = new CheckBox();
    this.checkBox1.Text = "Check Me!";
    this.checkBox1.Location = new Point(
        this.button1.Location.X + this.button1.Width + 10,
        this.button1.Location.Y);
    this.Controls.Add(this.checkBox1);
}

The layout code is ugly, but it gets the job done.

WinFormsEvents2

Finally, I’ll have a ListBox too:

public DemoForm()
{
    this.button1 = new Button();
    this.button1.Text = "Click Me!";
    this.Controls.Add(this.button1);

    this.checkBox1 = new CheckBox();
    this.checkBox1.Text = "Check Me!";
    this.checkBox1.Location = new Point(
        this.button1.Location.X + this.button1.Width + 10,
        this.button1.Location.Y);
    this.Controls.Add(this.checkBox1);

    this.listBox1 = new ListBox();
    this.listBox1.Location = new Point(
        10,
        this.button1.Location.Y + this.button1.Height + 10);
    this.listBox1.Size = new Size(
        this.Width - 40,
        this.Height - this.button1.Height - 40);
    this.Controls.Add(this.listBox1);
}

And here’s my final Form.  Fantastic design if you ask me.  I’ll apply a ClipboardReporterAttribute to the test method and approve the result.  Doing this locks down the look and feel.

WinFormsEvents3

Now I want to add another failing test to lock down the event handlers for the form.

[TestMethod]
public void VerifyDemoFormEvents()
{
    EventUtility.VerifyEventCallbacks(new DemoForm());
}

And here are the results:

Event callbacks for DemoForm

Throughout these posts I’ve been implying that this code base wont work with WinForms but we haven’t proved that yet.  Before I can say that its not working, I need to wire up some events!

public DemoForm()
{
    // ...

    this.button1.Click += this.ButtonClick;
    this.checkBox1.CheckedChanged += this.HandleCheckedChanged;
    this.Load += this.HandleFormLoad;
}

I’ve wired up three events but none of them show up in the results.  Right away I can see that there might be a problem with the Button and CheckBox, those events are wired up to child objects, and I’ve never tried to do anything other than get events off the top level object.  However, the HandleFormLoad method is wired up to the Form.Load event, which is part of the top-level object and isn’t showing up either.

Now I’ve got a TODO list:

  1. Figure out why the handler attached to Load is missing.
  2. Detect events wired up to child controls.

WinForms Event Implementation

In my last post, I thought I had discovered the end-all-be-all technique for finding events of any type, but the missing Load event proves me wrong.  According to MSDN, Form.Load has this signature:

public event EventHandler Load

That doesn’t seem very exotic.  When I debug my test and query the type in the Immediate window, I get the following output:

typeof(DemoForm).GetEvents()
{System.Reflection.EventInfo[91]}
    [0]: {System.EventHandler AutoSizeChanged}
    [1]: {System.EventHandler AutoValidateChanged}
    [2]: {System.ComponentModel.CancelEventHandler HelpButtonClicked}
    [3]: {System.EventHandler MaximizedBoundsChanged}
    [4]: {System.EventHandler MaximumSizeChanged}
    [5]: {System.EventHandler MarginChanged}
    [6]: {System.EventHandler MinimumSizeChanged}
    [7]: {System.EventHandler TabIndexChanged}
    [8]: {System.EventHandler TabStopChanged}
    [9]: {System.EventHandler Activated}
    [10]: {System.ComponentModel.CancelEventHandler Closing}
    [11]: {System.EventHandler Closed}
    [12]: {System.EventHandler Deactivate}
    [13]: {System.Windows.Forms.FormClosingEventHandler FormClosing}
    [14]: {System.Windows.Forms.FormClosedEventHandler FormClosed}
    [15]: {System.EventHandler Load}
    // Many more events...

Maybe something is wrong with the way I’m building my type collection in GetEventCallbacks. I’ll use Extract Method to break out the type collection query so that I can test it in isolation.

public static IEnumerable<EventCallback> GetEventCallbacks(
    this object value)
{
    return value.GetEventsForTypes(GetEventTypes(value).ToArray());
}

public static IEnumerable<Type> GetEventTypes(object value)
{
    return GetType(value).GetEvents().Select(ei => ei.EventHandlerType).Distinct();
}

This change doesn’t break any existing tests.  I’ll add a test to see what happens when this method queries DemoForm.

[TestMethod]
public void GetEventTypeForDemoForm()
{
    ApprovalTests.Approvals.VerifyAll(
        ReflectionUtility.GetEventTypes(new DemoForm()), string.Empty);
}

The results look and EventHandler, the delegate type backing Form.Load, is right there in slot 0.

[0] = System.EventHandler
[1] = System.ComponentModel.CancelEventHandler
[2] = System.Windows.Forms.FormClosingEventHandler
[3] = System.Windows.Forms.FormClosedEventHandler
[4] = System.Windows.Forms.InputLanguageChangedEventHandler
[5] = System.Windows.Forms.InputLanguageChangingEventHandler
[6] = System.Windows.Forms.ScrollEventHandler
[7] = System.Windows.Forms.ControlEventHandler
[8] = System.Windows.Forms.DragEventHandler
[9] = System.Windows.Forms.GiveFeedbackEventHandler
[10] = System.Windows.Forms.HelpEventHandler
[11] = System.Windows.Forms.InvalidateEventHandler
[12] = System.Windows.Forms.PaintEventHandler
[13] = System.Windows.Forms.QueryContinueDragEventHandler
[14] = System.Windows.Forms.QueryAccessibilityHelpEventHandler
[15] = System.Windows.Forms.KeyEventHandler
[16] = System.Windows.Forms.KeyPressEventHandler
[17] = System.Windows.Forms.LayoutEventHandler
[18] = System.Windows.Forms.MouseEventHandler
[19] = System.Windows.Forms.PreviewKeyDownEventHandler
[20] = System.Windows.Forms.UICuesEventHandler

The next logical problem could be that the GetEventsForTypes method is not finding any fields assignable to EventHandler on Form or any of its ancestors.  I’ll write another test to focus in on that possibility.

[TestMethod]
public void GetEventsForDemoFormEventHandlers()
{
    ApprovalTests.Approvals.VerifyAll(
        new DemoForm().GetEventsForTypes(typeof(EventHandler)),
        string.Empty);
}

ApprovalTests reports that the result set is empty.  That’s a problem.  Why isn’t it finding the field backing Form.LoadIs there a field backing Form.Load?  My testing assumes that every event is implemented by the compiler, and as a result, every event should have a delegate backing the event, declared as a private field.  What if WinForms uses custom add/remove methods instead of compiler implemented events?

I can use ILSpy to figure out what’s going on.  Sure enough, ILSpy shows a custom add/remove implementation.

public event EventHandler Load
{
    add
    {
        base.Events.AddHandler(Form.EVENT_LOAD, value);
    }
    remove
    {
        base.Events.RemoveHandler(Form.EVENT_LOAD, value);
    }
}

Instead of adding and removing delegates from a private instance field on Form, these add and remove methods make calls to a protected property called Events. ILSpy tells me that Events is an instance of EventHandlerList. I don’t know what that is yet, but before I try to figure it out I want to spend a little more time inside Form. The purpose of value is easy to understand, it is the delegate to add or remove, but what about Form.EVENT_LOAD?

EVENT_LOAD refers to a private static read-only object, initialized to new object(). This argument it is just a reference to some unique chunk of memory on the managed heap, it can’t be changed, and every instance of Form has access to the same unique reference. I notice that there are many more static objects like this on Form. For example, Form has EVENT_MENUCOMPLETE, EVENT_MENUSTART, EVENT_RESIZEBEGIN, and so on. Presumably there is a static object which corresponds to each event implemented with custom add/remove methods similar to Form.Load.

If I navigate to the Events property declaration I find it declared on System.ComponentModel.Component:

protected EventHandlerList Events
{
    get
    {
        if (this.events == null)
        {
            this.events = new EventHandlerList(this);
        }
        return this.events;
    }
}

The property lazily instantiates an EventHandlerList when needed, and that’s about it. EventHandlerList also lives in the System.ComponentModel namespace. Despite it’s name, it does not derive from List, nor does it implement any list or collection interfaces.  I’ll look at AddHandler to see what it does when it receives the static object and handler delegate.

public void AddHandler(object key, Delegate value)
{
    EventHandlerList.ListEntry listEntry = this.Find(key);
    if (listEntry != null)
    {
        listEntry.handler = Delegate.Combine(listEntry.handler, value);
        return;
    }
    this.head = new EventHandlerList.ListEntry(key, value, this.head);
}

From this method’s point of view, the static object is called key and the delegate is called value. So, EventHandlerList is logically closer to a dictionary than a list.  When AddHandler executes, the dictionary attempts to find an existing value with that key. When found, the new delegate is combined with the existing delegates, otherwise the method creates a new dictionary entry. The last line of the method might give some insight into how EventHandlerList stores its data, if I knew how EventHandlerList.ListEntry was implemented.

Here is ListEntry‘s entire implementation:

private sealed class ListEntry
{
    internal EventHandlerList.ListEntry next;
    internal object key;
    internal Delegate handler;
    public ListEntry(object key, Delegate handler, EventHandlerList.ListEntry next)
    {
        this.next = next;
        this.key = key;
        this.handler = handler;
    }
}

In a typical .NET dictionary, each entry provides a key and a value. ListEntry provides these members, but also a reference to the next entry. So, EventHandlerList is a hybrid dictionary/linked-list.

Missing Pieces

After finding ListEntry I don’t need to look any further. Since WinForms has taken us deep into a rabbit hole, I’ll restate my goals:

Given a Form to examine, make one call that will inventory the event handlers attached to the Form and each of the Form’s controls. Display the inventory where each invocation list is associated with the event it’s attached to, and each event is associated with the type it is declared on.

Let’s compare how I solved each part of this problem in the Poco example with how I will need to solve it in the WinForms example.

  • Find Invocation List
    • Poco: Find the delegate field backing the event; call GetInvocationList
    • WinForms: Find the ListEntry instance in Events; access the handler field and call GetInvocationList
  • Associate Invocation List with Event
    • Poco: Use the delegate field name.
    • WinForms: Use the name of the static object used as the ListEntry key.
  • Associate Event with Object
    • Poco: Use the Type name.
    • WinForms: Use the Form type name for Form Events.  Use the Control type name for each child control.

I think the biggest difference while constructing a query will be the way the invocation list is associated with an event name.  With POCO events I could get both the invocation list and the name from the same delegate reference.  By the time a Component stores an event delegate in a ListEntry the event name is lost and everything is called key. The CLR doesn’t care about the name because it checks the key with a reference comparison, it just needs the pointer.

Besides the query, I’m going to need a lot of plumbing just to make the data structure queryable. ListEntry is private, nested and sealed. The compiler wont even acknowledge it’s existence:

EventHandlerList.ListEntry entry;  // Won't compile

Error:

Error   1   The type name 'ListEntry' does not exist in the type 'System.ComponentModel.EventHandlerList'

And since EventHandlerList doesn’t implement IEnumerable or IEnumerable<T>, it doesn’t play nice with LINQ:

EventHandlerList ehl = new EventHandlerList();
var q = from e in ehl select e; // Wont compile

Error:

Error   1   Could not find an implementation of the query pattern for source type 'System.ComponentModel.EventHandlerList'.  'Select' not found.

So, before I can write my queries, I’ll need to get under there and do some plumbing.

Relationship With EventApprovals

This article has been a bit of a tease.  While I hope I’ve provided some useful information, I haven’t really produced any solutions.  The relationship with EventApprovals is that all of these problems are already solved in ApprovalTests!  When you call ApprovalTests.Events.EventApprovals.VerifyEvents, WinForms events are supported and will appear in your inventory.

Up Next

In this article’s next installment: “Beyond the Event Horizon: WinForms Plumbing”, I will use what I’ve learned about WinForms to build up the plumbing pieces necessary to make a usable wrapper for ListEntry and an enumerable adapter for EventHandlerList.  With those pieces of plumbing out of the way then I can return to my TODO list, find the missing Form.Load event, and figure out how I want to capture the events declared on child controls.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s