Introduction
If you wanted to record every event generated by any object, how would you do that? This article describes what I think is a new (if only slightly different) method
to allow one common point to handle any event.
Why?
Why would you want to handle multiple events from a single point? Lots of reasons, mine being that I was writing an application to test the field strength
of the 3G and 4G wireless mobile networks at certain locations.
Three separate modules generate events indicating changes in location (from GPS), changes in radio-field-strength (from the 3G/4G modem),
and changes in data download rate (from another class repeatedly downloading the same file).
A single point was needed to consolidate the data from each source and record it within a common data structure. Because of the nature of real-world programming,
each module had been developed previously for different purposes, and I had very little time to implement the application,
so re-writing each module to use a common event structure was not practical.
Background
I must begin this with a tip of the hat to the following article that served as inspiration: commoneventhandler.aspx.
This article covers an excellent method for attaching to any event without first knowing the signature of that event.
The reason I have created my own method, and subsequent article was that the above method did not quite serve my purposes, and I believe I have come up with
perhaps a new (if only slightly different) solution to this problem.
The crux of the issue: How can I have a single point in my code that can handle events from any class?
The main problem is that while Microsoft has tried to instill a common base for all event handlers with the signature
of (Object sender, EventArgs e)
- this is not by any means mandatory. To bind a delegate to an event, no matter what,
the parameters of the Invoke
method of the delegate must match those of the event handler delegate.
Another issue arises - suppose you do manage to bind multiple events to the same handler (to record or re-route those events) - the event handler doesn't necessarily
include any information about the event. In normal practices, you should know exactly what event is being raised, because you are only handling
the one event in each handler. However, using a common event means that one event handler is dealing with multiple events and event sources,
and it would really help if you knew what that event was.
An event handler's method signature must match the event-handler delegate, so there isn't any scope to include the event information in the method
signature (unless you can dictate the signature of the event handler, which we can't).
Also, I carefully investigated Reflection, and while there seems to be a way to get the calling method signature, there isn't any functionality to find the calling event.
The solution turned out to be found in putting an event-handler method on its own class, and setting the event-information as a property of that class.
This is the default implementation of an event-router class for the "EventHandler
" standard delegate:
public class DefaultEventRouter : InternalEventRouter
{
public DefaultEventRouter(EventInfo info)
: base(info)
{
}
public virtual void HandleEvent(object sender, EventArgs e)
{
Object[] args = new Object[2];
args[0] = sender;
args[1] = e;
CommonEventBroker.SubmitEvent(sender, Info, args);
}
}
The event-info is passed in to the class with its constructor. The HandleEvent
method can now pass that event-info structure on to the common event broker, along with
the sender of the event, and an array of objects containing the parameters.
CommonEventBroker.SubmitEvent(sender, Info, args);
By generating a new instance of this class for each event, then having that class' event-handler method raise a common event, passing in the event-info,
and arguments of the source event, we get the ability to distinguish between event types in the common event handler.
At this point you might be thinking, Yes that's great but it isn't very universal, is it?
And you'd be right. Time for the next part of the issue: how do we get the one event-handler method to bind to any event?
Other solutions all seem to fall back to using MSIL and Reflection.Emit
to generate an event-handler signature at run time.
Effectively, this writes a new class and method to handle each type of event. My problem with this has been that the MSIL generated for these
event handlers is completely unintelligible - it is only one step away from assembly really.
Not only do I not want to deal with IL directly (it was never meant to be human coded), I don't want to burden future programmers that might have
to deal with my code, with having to read and understand that stuff.
What would be cool, would be if the event handler could be written
in C# at runtime, and compiled and attached to events as necessary. Only the method signature, class-name, and a couple of lines of code need to differ from a common template.
Turns out this is more than possible. The CodeDomProvider
within the System.CodeDom
namespace has the method:
CompilerResults CompileAssemblyFromSource(CompilerParameters options, params string[] sources);
This generates a new assembly from the source code and grants access to the types within the assembly. You can even tell the process to generate an in-memory only assembly.
So now I just need to be able to procedurally write an event-handler class.
I came up with the following template:
string baseCode = @"using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
namespace Utility.Events
{
public class %NAME%EventRouter : InternalEventRouter
{
public %NAME%EventRouter(EventInfo info) : base(info)
{
}
public virtual void HandleEvent(%PARAMETERS%)
{
Object[] args = new Object[%PARAMCOUNT%];
%PARAMASSIGNMENT%
// submit the event to the broker
CommonEventBroker.SubmitEvent(sender, Info, args);
}
}
}";
I marked the spots where the code needs to be adjusted using %%, and used the EventHandlerType
property of the EventInfo
structure
to build the replacement code sections.
The generated code is then compiled to an in-memory assembly, and stored in a dictionary against the event-handler-type.
A new instance of this class can be used for each event that uses the same event handler type.
Binding an Event:
public static void Subscribe(object obj, string eventName)
{
EventInfo info = obj.GetType().GetEvent(eventName);
var eventHandler = CreateEventHandler(info);
eventHandler.AttachToEventOn(obj);
}
The subscribe method of the CommonEventBroker
class subscribes to a named event on that class.
From that point, whenever that event is raised, the common event in the CommonEventBroker
class will also be raised. The common event will have the event source,
event info, and parameters associated with it.
This is the delegate for the common event:
public delegate void CommonEventHandler(object source, EventInfo eventInfo,
CommonEventParameter[] parameters);
Using the Code
In the example project, the form "Test" has a web-browser control hosted on it, and all events from the form and the browser are handled
by the common-event broker, with the following lines of code:
CommonEventBroker.CommonEvent += new CommonEventHandler(CommonEventBroker_CommonEvent);
CommonEventBroker.SubscribeAll(this);
CommonEventBroker.SubscribeAll(this.webBrowser1);
The handler for the common event just writes out the sender, event name, and event signature:
void CommonEventBroker_CommonEvent(object source,
System.Reflection.EventInfo eventInfo, CommonEventParameter[] parameters)
{
Console.Write(source.ToString() + " fired: " + eventInfo.Name + " (");
foreach (var p in parameters)
Console.Write(p.ParameterType.Name + " " + p.Name + " ");
Console.WriteLine(")");
}
Points of Interest
When an event is raised by any class, the thread that raises the event is also the thread that must execute the event handler method.
Thus, attaching complex event handlers may introduce delays, or make an application unresponsive. If you are recording lots of events with
a single event handler, this problem is exacerbated.
To get around this I am using a separate thread to process events. The SubmitEvent
method of CommonEventBroker
adds
the event details to a FIFO queue, sets an AutoResetEvent
, then returns control back to the source of the event. The consumer thread is signaled
by the AutoResetEvent
then de-queues the event data and raises the CommonEvent
. The code within the common event handler is then executed
by the consumer thread, and not the thread that raised the event. This decouples the event and its processing, meaning that you can write very heavy processing code
for the event without sacrificing responsiveness.
Stuff I Could Have Done Better
I'm sure there is heaps, I didn't have time to use best practices all the way through. Mainly, the CommonEventHandler
delegate should use only two arguments,
and the second argument should be a derivation of EventArgs
.
Also I made the CommonEventBroker
static. This limits you to one common event. I could have used the Singleton pattern instead.
Latest Updates
Firstly, my thanks to those that have left positive comments about this article. I hope it has been useful.
I have made a few upgrades to the code that may offer increased usability. Mainly, I have changed the event-broker from a static class called CommonEventBroker
to an instance class called EventBroker
. This class now has a static property called CommonEventBroker
, much like the Singleton pattern, but unlike Singleton,
the constructor is still public so you can create instances of the class. I feel this gives the best of both worlds: there is a truly common, static instance of the broker,
as well as specific instances.
The test form has been updated to utilize its own instance of the event-broker.
In addition, I have changed the signature of the common-event delegate to match best-practices, it now has two parameters: the first being the sender,
and the second being a derivation of EventArgs
.
I have also set the event-broker class to implement the IDisposable
interface, which will stop the event processing thread in the Dispose()
method.
The following code is the updated Test
form:
public partial class Test : Form
{
private EventBroker myEventBroker = new EventBroker();
public Test()
{
InitializeComponent();
myEventBroker.CommonEvent +=
new CommonEventHandler(myEventBroker_CommonEvent);
myEventBroker.SubscribeAll(this);
myEventBroker.SubscribeAll(this.webBrowser1);
this.FormClosed += new FormClosedEventHandler(Test_FormClosed);
}
void myEventBroker_CommonEvent(object source, CommonEventArgs e)
{
Console.WriteLine(source.ToString() + " fired: " + e.ToString());
}
void Test_FormClosed(object sender, FormClosedEventArgs e)
{
myEventBroker.Stop();
}
private void btnGo_Click(object sender, EventArgs e)
{
this.webBrowser1.Navigate(this.txtAddress.Text);
}
}