16 November 2009

Writing an EventBroker

On a project I'm working on with my friend Michael, we need a web page to be build from independent modules - independent in the sense that they have no direct dependency on any other module and that they may operate autonomously. A lose coupling strategy for the various UI elements you could call it.

We need this kind of flexibility because the users of the system must be able to pick and choose any module, or any number of modules, for any given page, dynamically - that is, without the need for a developer to wire everything up in the code behind. They should be able to select the modules they want and the modules should function autonomous. The modules should also be able to send messages to other modules when needed, without knowledge of each other.

The way I went about achieving this was by writing an EventBroker. An EventBroker can be considered an implementation of the Publisher/Subscriber pattern or a sub-pattern or derivative thereof.

It is obvious, that to achieve loosely coupled modules that knows nothing of the other modules, yet are able to send messages to each other, we must move the coupling or change its direction to an intermediate component. This is the role of the EventBroker.

An example is in place. I have two modules. When the UpdateContent method in the first module is executed, I want the second module to be notified and do something about it.

I use well named interfaces to express the type of message send.

public interface UpdateContentEvent {}

The SubscriberModule subscribe to messages of type UpdateContentEvent and assigns a messagehandler, UpdateContentHandler, to handle the message when or if it occur.

public class SubscriberModule
{
    public SubscriberModule()
    {
        EventBroker.Subscribe<UpdateContentEvent>(UpdateContentHandler);
    }
 
    private void UpdateContentHandler(object sender, EventArgs e)
    {
        var args = e as UpdateContentEventArgs;
        if(args != null)
        {
            string content = args.Content;
            // perform some update with content
        }
    }
}

The PublisherModule first builds some event arguments, then publishes the event, UpdateContentEvent, and supplies a reference to itself as well as the arguments.

public class PublisherModule
{
    public void DoUpdateContent()
    {
        EventArgs args = new UpdateContentEventArgs {Content = "Some content to be updated"};
        EventBroker.Publish<UpdateContentEvent>(this, args);
    }
}

That is basically it. The two modules know nothing of each other, but are able to send and receive messages from and to each other through the EventBroker.

Some considerations:

This EventBroker implementation is static and as such represent the strongest coupling one can have between components. In this case I find it acceptable and consider it part of the infrastructure of the system we are building. It is possible to change the implementation slightly and inject the EventBroker into the classes that use it, or hide it behind a factory, but at this point I'm satisfied with the coupling it represents.

The implementation only allow modules to send and receive messages in a synchronous manner. This is all I need at the moment, but at some point it may need to support asynchronous messaging. If this need emerge, I'll consider a true message bus instead.

The implementation allow for any number of publishers and subscribers. I've wrapped the event handling Action delegate in a WeakReference so that the EventBroker does not become a greedy memory-clutching hub.

In truth the EventBroker mediates messages, not events in the sense of .NET events. In this implementation I switch between the terms event and message. This can be cause for misunderstandings. In the next version I'll remedy that...

Lastly, the EventBroker is thread safe.

And here is the EventBroker code:

public static class EventBroker
{
    static readonly IList<KeyValuePair<Type, WeakReference>> eventsSubscribedTo = new List<KeyValuePair<Type, WeakReference>>();
 
    public static void Subscribe<TEventType>(Action<object, EventArgs> eventHandler)
    {
        lock (eventsSubscribedTo)
            eventsSubscribedTo.Add(new KeyValuePair<Type, WeakReference>(typeof(TEventType), new WeakReference(eventHandler)));
    }
 
    public static void Publish<TEventType>(object sender, EventArgs e)
    {
        lock (eventsSubscribedTo)
        {
            RemoveGarbageCollectedEventHandlers();
 
            for (int i = 0; i < eventsSubscribedTo.Count; i++)
            {
                if (eventsSubscribedTo[i].Key == typeof(TEventType))
                {
                    var eventHandler = eventsSubscribedTo[i].Value.Target as Action<object, EventArgs>;
 
                    if(eventHandler != null)
                        eventHandler(sender, e);
                }
            }
        }
    }
 
    private static void RemoveGarbageCollectedEventHandlers()
    {
        for (int i = 0; i < eventsSubscribedTo.Count; i++)
        {
            KeyValuePair<Type, WeakReference> pair = eventsSubscribedTo[i];
            WeakReference value = pair.Value;
            var eventHandler = value.Target as Action<object, EventArgs>;
 
            if (eventHandler == null)
                eventsSubscribedTo.Remove(pair);
        }
    }
 
    public static void Unsubscribe<TEventType>(Action<object, EventArgs> eventHandler)
    {
        lock (eventsSubscribedTo)
            for (int i = 0; i < eventsSubscribedTo.Count; i++)
            {
                KeyValuePair<Type, WeakReference> pair = eventsSubscribedTo[i];
                Type eventType = pair.Key;
                WeakReference weakReference = pair.Value;
                var handler = weakReference.Target as Action<object, EventArgs>;
 
                if (eventType == typeof(TEventType) && handler == eventHandler)
                    eventsSubscribedTo.Remove(pair);
            }
    }
 
    public static void Clear()
    {
        lock (eventsSubscribedTo)
            eventsSubscribedTo.Clear();
    }
}

Please feel free to suggest improvement or errors I have not detected.

Labels: ,


Comments: Post a Comment



Links to this post:

Create a Link



<< Home

This page is powered by Blogger. Isn't yours?

Subscribe to Posts [Atom]