Introduction
How often have you coded up a simple Windows Form application to clear up the difference between similar events? Where does TextChanged get fired in the sequence KeyDown, KeyPress, KeyUp. When exactly does Load get fired in relation to other events on a complex control?
ControlInspector is designed to answer these and many other questions by hooking all events on an arbitrary windows form control; in a user control; or in a complete windows form. It will recurse through the controls collection and hook events on every sub control, and has special handling for context menus and main menus on forms to make sure that these don't get excluded.
In summary; Control Inspector is like a native .net version of Spy++ for .net events
This article is intended to accompany ContronInspector and give some insight into the techniques it uses. If you just want to use ControlInspector to diagnose your own applications, or to understand Windows Form events better then just download the compiled version of the software, and don't worry about the source!
Using Control Inspector
When you first open Control Inspector you will be presented with a blank screen. You can use the File/Open option to open an arbitrary assembly (.net exe or dll file); you will then be presented with a list of Windows Forms Controls and Forms that are part of the assembly. The entry you select will be loaded into memory and instantiated either by hosting it in a form, or if it is a form it will be constructed directly. You can also use the File/Windows Forms option to display a list of available controls from the System.Windows.Forms namespace. Note that you won't be able to construct some of these controls (eg ButtonBase) because although they derive from Control they are not directly usable.
If the control is hosted in a form, you will see the ControlHostForm above. It has been written to display a red grid around your control so you can see where the control you are analysing ends.
The first tab of the event viewer, "All Events" shows a complete list of events trapped by ControlInspector in the that they sequence. You can look at individual controls by clicking on their individual tab. When you have a particular control in focus, you are able to use the property editor to make on the fly changes to the control (useful to see what effect events are fired by this, and in what order). For example, if you enable anchoring you then can see the resize events generated by resizing the hosting form.
The events are hooked before the control is displayed, so you will get all the initialisation events; and if you close the hosting form, then you will see the events fired until the control dies.
You are able to uncheck particular events to handle either by focusing on the control and using the checked list box, or by right clicking on a particular event in the event view and selecting "stop tracking this event". This option will only stop tracking events for this individual control. There are options to stop tracking groups of events for all controls (for example, all mouse movement related events) to stop your event list getting over populated.
If you aren't interested in ControlInspector under the covers, I suggest you stop reading now!
Background
Last week I attended a Guerilla .NET course hosted by DevelopMentor. I can highly recommend this training company as the whole week was inspiring, and the instructors highly knowledgeable: You know who you are guys!
The instructors issued a challenge for the the class to come up with the best program that they could using any of the techniques that they had learnt during the week. The challenge would be judged on Thursday, so I had just a few days to get busy.
One of the topics covered on the course was Reflection and I decided to use this to discover information about Windows Forms controls and hook on to their events.
ControlInspector also has to use Reflection.Emit to generate a function and delegate that exactly corresponds to the event type to allow it to hook on to arbitrary events. It can only hook on to events that following the function prototype:
void eventName(object sender, eventargstype x)
where eventargstype derives from the EventArgs type. All the standard WindowsForms events do; and your events should also use this structure so there should be no problems with this approach.
About the code
The main part of the code is split between MainForm.cs which contains the UI and code for hooking on to events, and GenerateEventAssembly.cs which generates the IL for a function which matches a given delegate. Lets talk about the way the events are hooked in the first place:
void HookEvents(object o, string name) {
Type t = o.GetType();
...
Using Reflection, we step trhough all the events on a particular type. The EventHandlerType will be the type of the delegate required to hook on to this event; eg: void EventHandler(object sender, EventArgs e)
To generate a function with this prototype, we need to get the parameters of the function, since all delegates are classes that implement the Invoke function with the required parameters it is this that we find.
foreach(EventInfo ei in t.GetEvents())
{
Type eventHandlerType = ei.EventHandlerType;
MethodInfo mi = eventHandlerType.GetMethod("Invoke");
ParameterInfo[] pi = mi.GetParameters();
Now comes the magic. The function GetEventConsumerType
generates an a class dynamically that has a method "HandleEvent
" of exactly the right types. This class is derived from ControlEvent, which contains a function void GenericHandleEvent(object sender, object eventargs)
so the generated code is kept to a minimum (I can't write IL for toffee: I wrote a class in C# which did the required type conversion, ran ILDASM on it, then used that as a basis to automatically generate these arbitrary types).
ControlEvent ce
= GenerateEventAssembly.Instance.GetEventConsumerType(pi[1].ParameterType);
Now we have a function of the right type, we just need to hook it up. We also hook up the EventFired event on "ControlEvent" to our generic event handler which does all the display work
ce.ControlName = name;
ce.EventName = ei.Name;
ce.EventTrackInfo = trackInfo;
ce.EventFired += new EventHandler(eventFired);
controlEventList.Add(ce);
Delegate d = Delegate.CreateDelegate(eventHandlerType, ce, "HandleEvent");
ei.AddEventHandler(o, d);
}
...
Finally, if this is a control type, we recurse through sub-controls
if (o is Control) {
Control c = (Control) o;
if (c.Controls != null) {
foreach(Control subControl in c.Controls) {
HookEvents(subControl, name + "/" + ControlName(subControl));
}
}
...
}
The code to do the IL generation is quite well commented, so I won't go into greater detail about that here.
The function
void AddEventsToTreeView(ControlEvent ce, TreeView treeView,
bool includeControlName)
is the routine which adds the events to the tree list. It uses reflection (again!) to display any information contained in the EventArgs (eg key pressed in KeyPressedEventArgs).
Because of the generic way in which events are hooked up, there is a test user control contained in the ControlInspector.exe - UserControlTest. This contains a button which fires off a user-defined event just to prove that everything is working correctly.
Points of Interest
Removing the tab pages from the form when a new control is loaded exhibits a strange bug in the 1.0 framework which I have tried my best to work around. My thanks to instructor Ian Griffiths who helped me with this issue.
I didn't win the Thursday Challenge!
The current project has been tested under VS.NET 2002 and VS.NET 2003, and it works fine on both of them. The downloadable file is a VS.NET 2002 project, but works fine if you upgrade it. I will be releasing a new version with some more changes soon.
History
1.0 Initial release