Click here to Skip to main content
15,881,757 members
Articles / General Programming

Yet Another Universal Event Handler

Rate me:
Please Sign up or sign in to vote.
4.94/5 (43 votes)
2 Jan 2012CPOL7 min read 64.1K   802   96   29
A universal Event Handler with a difference: No MSIL required.

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.

output1.png

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:

C#
/// <summary>
/// default implementation of InternalEventRouter for standard events.
/// The signature of the HandleEvent method matches that of the
/// EventHandler delegate.
/// </summary>
public class DefaultEventRouter : InternalEventRouter
{
    /// <summary>
    /// construct and pass the event-info to the base constructor.
    /// </summary>
    /// <param name="info"></param>
    public DefaultEventRouter(EventInfo info)
        : base(info)
    {
    }
    /// <summary>
    /// handles EventHandler type events:
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    public virtual void HandleEvent(object sender, EventArgs e)
    {
        // capture the event parameters:
        Object[] args = new Object[2];
        args[0] = sender;
        args[1] = e;
        // submit the event data to the common event broker.
        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.

C#
 // submit the event data to the common event broker. 
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:

C#
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:

C#
 // define the class template: spots are marked for replacement (ie %NAME%)
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:

C#
/// <summary>
/// subscribes the specified event from the specified object to the universal event broker.
/// when the event is raised, it will be rerouted to the CommonEventBroker.CommonEvent
/// </summary>
/// <param name="obj"></param>
/// <param name="eventName"></param>
public static void Subscribe(object obj, string eventName)
{
    // get the event-info:
    EventInfo info = obj.GetType().GetEvent(eventName);

    // get the event-handler:
    var eventHandler = CreateEventHandler(info);

    // attach the event handler to the source object:
    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:

C#
/// <summary>
/// delegate for a universal event handler. passes the source object,
/// the event-info, and all the parameters of the event.
/// </summary>
/// <param name="source"></param>
/// <param name="eventInfo"></param>
/// <param name="arguments"></param>
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:

C#
CommonEventBroker.CommonEvent += new CommonEventHandler(CommonEventBroker_CommonEvent);
CommonEventBroker.SubscribeAll(this);
CommonEventBroker.SubscribeAll(this.webBrowser1);

testform.png

The handler for the common event just writes out the sender, event name, and event signature:

C#
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:

C#
public partial class Test : Form
{
    /// <summary>
    /// instance event broker for this form.
    /// </summary>
    private EventBroker myEventBroker = new EventBroker();

    public Test()
    {
        // initialise designer components
        InitializeComponent();

        // attach to the common event:
        myEventBroker.CommonEvent += 
           new CommonEventHandler(myEventBroker_CommonEvent);

        // subscribe to this form and the web-browser control's events.
        // note: the web-browser offers several un-subscribable events.
        myEventBroker.SubscribeAll(this);
        myEventBroker.SubscribeAll(this.webBrowser1);

        // handle the form-closed event:
        this.FormClosed += new FormClosedEventHandler(Test_FormClosed);
    }

    void myEventBroker_CommonEvent(object source, CommonEventArgs e)
    {
        // write out the event signature:
        Console.WriteLine(source.ToString() + " fired: " + e.ToString());
    }

    void Test_FormClosed(object sender, FormClosedEventArgs e)
    {
        // stop the event broker.
        myEventBroker.Stop();
    }

    private void btnGo_Click(object sender, EventArgs e)
    {
        // navigate to the selected url:
        this.webBrowser1.Navigate(this.txtAddress.Text);
    }
}

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Decipha
Australia Australia
Wrote his first computer game in Microsoft Basic, on a Dragon 32 at age 7. It wasn't very good.
Has been working as a consultant and developer for the last 15 years,
Discovered C# shortly after it was created, and hasn't looked back.
Feels weird talking about himself in the third person.

Comments and Discussions

 
GeneralMy vote of 5 Pin
gdpaul5-Jan-12 10:58
gdpaul5-Jan-12 10:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.