Skip to content


Simple message-based Event Aggregator

This blog post is more about implementation than in-depth description or background information. It’s covering an implementation of a simple Event Aggregator that I’ve developed and used in a non-trivial Silverlight project and which I found quite useful. A word of warning: It’s not fully fledged and not suited for all scenarios… and it’s not intended to be.

(Little) background

Event broker linkage, Copyright: Matthias JauernigThe first time I’ve seen an implementation of the event aggregator pattern was in the Prism framework. I like the idea behind this pattern. It’s decoupling event publishers and subscribers. It’s representing a kind of Hub. Each time a publisher publishs an event it gets into the hub and then it’s redirected to the subscribers. The publishers/subscribers don’t have to know each other, they just have to know the concrete event aggregator which is a mediator and mediates between both parties. Thus the event aggregator is realizing a useful indirection mechanism.

The event aggregator can be used in many scenarios and situations. I’ve mainly used it in UI-related scenarios, but it’s not limited to that. In the UI situation it helped me out e.g. at view synchronization and indirect communication: between multiple user controls/views, views and view models (both directions) or views and controllers. They don’t have to know each other and thus can be loosely coupled.

While I came across with the Prism event aggregator at first, after some investigation I didn’t like the implementation very much in view of the usage from a client’s perspective. You first have to get an event from the event aggregator and then you can subscribe to this event or publish the event with some event arguments. This is kind of duplicated work. When I know the type of the event arguments why do I have to know the event anymore (under the assumption that there’s a 1:1 relationship between both)? Others came across with this issue as well. A better approach in my opinion is a solely message-based event aggregator. A system that doesn’t distinguish between events and event arguments, but is based on messages people can subscribe to and publish.

Implementation

Let’s come to the bits’n'bytes of my implementatoin. My message-based event aggregator should be able to handle messages of type IMessage. This is just an empty interface for type-correctness in the other components. Additionally it makes the message type explicit which I like because a message should be a very specific type of your application domain:

public interface IMessage { }

Another important component is the ISubscription<TMessage> interface and its default implementation Subscription<TMessage>. A subscription is something the event aggregator stores internally when an action should be subscribed to a message. This subscription process results in an ISubscription<TMessage> object, which is returned to the caller. The caller will be able to unsubscribe from the event aggregator with this subscription object – there’s no need to reference the subscribed action, which is handy e.g. in situations where you want to use anonymous methods (via delegates or lambdas). Furthermore when the subscription disposes it’s unsubscribed from the event aggregator, which I found quite useful:

public interface ISubscription<TMessage> : IDisposable
    where TMessage : IMessage
{
    Action<TMessage> Action { get; }
    IEventAggregator EventAggregator { get; }
}

public class Subscription<TMessage> : ISubscription<TMessage>
    where TMessage : IMessage
{
    public Action<TMessage> Action { get; private set; }
    public IEventAggregator EventAggregator { get; private set; }

    public Subscription(IEventAggregator eventAggregator, Action<TMessage> action)
    {
        if(eventAggregator == null) throw new ArgumentNullException("eventAggregator");
        if(action == null) throw new ArgumentNullException("action");

        EventAggregator = eventAggregator;
        Action = action;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if(disposing)
            EventAggregator.UnSubscribe(this);
    }
}

Note the reference to EventAggregator in the interface and its use in the implementation. This is necessary due to the disposing functionality. Of course if you don’t want this behavior in your scenario you can adapt the implementation.

At the end the IEventAggregator interface and its default implementation EventAggregator handle the whole message publish/subscribe mechanism. Clients can Subscribe() to specific types of messages with custom actions that are stored in an ISubscription<TMessage> object. Those clients can UnSubscribe() if they’re owning the ISubscription<TMessage> object. Other clients can Publish() concrete messages, which gets the subscribers of the message notified:

public interface IEventAggregator
{
    void Publish<TMessage>(TMessage message)
        where TMessage : IMessage;

    ISubscription<TMessage> Subscribe<TMessage>(Action<TMessage> action)
        where TMessage : IMessage;

    void UnSubscribe<TMessage>(ISubscription<TMessage> subscription)
        where TMessage : IMessage;

    void ClearAllSubscriptions();
    void ClearAllSubscriptions(Type[] exceptMessages);
}

public class EventAggregator : IEventAggregator
{
    private readonly IDictionary<Type, IList> _subscriptions = new Dictionary<Type, IList>();

    public void Publish<TMessage>(TMessage message)
        where TMessage : IMessage
    {
        if(message == null) throw new ArgumentNullException("message");

        Type messageType = typeof(TMessage);
        if(_subscriptions.ContainsKey(messageType))
        {
            var subscriptionList = new List<ISubscription<TMessage>>(
                _subscriptions[messageType].Cast<ISubscription<TMessage>>());
            foreach(var subscription in subscriptionList)
                subscription.Action(message);
        }
    }

    public ISubscription<TMessage> Subscribe<TMessage>(Action<TMessage> action)
        where TMessage : IMessage
    {
        Type messageType = typeof(TMessage);
        var subscription = new Subscription<TMessage>(this, action);

        if(_subscriptions.ContainsKey(messageType))
            _subscriptions[messageType].Add(subscription);
        else
            _subscriptions.Add(messageType, new List<ISubscription<TMessage>>{subscription});

        return subscription;
    }

    public void UnSubscribe<TMessage>(ISubscription<TMessage> subscription)
        where TMessage : IMessage
    {
        Type messageType = typeof(TMessage);
        if (_subscriptions.ContainsKey(messageType))
            _subscriptions[messageType].Remove(subscription);
    }

    public void ClearAllSubscriptions()
    {
        ClearAllSubscriptions(null);
    }

    public void ClearAllSubscriptions(Type[] exceptMessages)
    {
        foreach (var messageSubscriptions in new Dictionary<Type, IList>(_subscriptions))
        {
            bool canDelete = true;
            if (exceptMessages != null)
                canDelete = !exceptMessages.Contains(messageSubscriptions.Key);

            if (canDelete)
                _subscriptions.Remove(messageSubscriptions);
        }
    }
}

Usage

The usage of this event aggregator implementation is simple and straight forward. Clients can subscribe to messages they’re interested in:

// Option 1: Explicit action subscription
Action<MyMessage> someAction = message => { /*...*/ };
var subscription1 = eventAggregator.Subscribe(someAction);

// Option 2: Subscription via lambda
var subscription2 = eventAggregator.Subscribe<MyMessage>(message => { /*...*/ });

The clients get an ISubscription<TMessage> in return, from which they’re able to unsubscribe:

// Option 1: Unsubscribe by calling the event aggregator method
eventAggregator.UnSubscribe(subscription);

// Option 2: Unsubscribe by calling Dispose() on the subscription object
subscription.Dispose();

Other clients now are able to publish concrete messages and subscribers get informed about those messages:

eventAggregator.Publish(new MyMessage{ /*...*/ });

How to get an instance of IEventAggregator, you may ask? Well, that’s your decision! Implement a singleton for accessing an instance, use your favorite DI container, whatever…

Conclusion

That’s it. A simple message-based event aggregator implementation that can be used in a variety of situations. And which can be replaced by other implementations as well. Perhaps you want to persist or log messages, enable detached subscribers, allow async event processing or even further functionality like load balancing… It’s up to you to provide your own implementation. And feel free to connect the Latch Pattern ;-)

While the presented EventAggregator perfectly fitted my needs, it’s not intended to be universally applicable. For example I know that Prism uses WeakReferences to simplify garbage collection. I say it again: feel free to do that in your own implementation. Besides there are many more syntactic ways to implement event aggregators/brokers. Paste your comments if you have further suggestions – you’re welcome!

kick it on DotNetKicks.com

Posted in Design patterns. Tagged with , , , , .

6 Responses

Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.

  1. I don’t understand why you want to make all Message-Types implement IMessage. It’s not necessary.

    If I just delete all the “where TMessage : IMessage” statements, everything works just as well, but now I can do things like

    var aggregator = new EventAggregator();

    using (var subscription = aggregator.Subscribe(s => Console.WriteLine(s)))
    {
    aggregator.Publish(“Hello!”);
    aggregator.Publish(“World!”);
    }

    aggregator.Publish(“just kidding…”);

  2. Gope said

    @DanielT: How would you subscribe then to a specific message like CustomerChangedMessage? Your solution defeats the whole purpose of a Pub/Sub System.

    Regards
    Gope

  3. Hello Daniel,

    Introducing IMessage was an explicit decision! Of course it limits the flexibility to use arbitrary datatypes, but this is by design.

    Imagine you have two components in your application, which don’t know of each other, but share the same event aggregator. Now imagine both components are handling “string” datatypes as in your example. Component 1 publishs a string and component 2 reacts on it, but this wasn’t intended. You see: arbitrary datatypes can lead to odd side effects.

    Being explicit in the message type helps out. A message implementing IMessage is a very specific part of you domain and bound to it. It’s encapsulating a certain part of logic and named like UserLoginFailedMessage. Thus the whole event system becomes explicit.

    Of course if you want to use arbitrary datatypes, feel free to use your own IEventAggregator implementation :-)

    Regards, Matthias

  4. Daniel said

    There is literally next to no useful code for someone wishing to develop their own Event Aggregator from scratch using WinForms. You have saved my life, thank you very much.

  5. Daniel said

    By the way, I thought of a question. The Event Aggregator seems quite useful when you need to publish/subscribe when certain pieces of the code occur. If, however, I am in a piece of code and I need some data to process, how do I instantly call, receive data and proceed?

    It seems to me that you would have to first fire an event saying you need an update in the data/state/whatever, and then the update would occur. Then the code could continue its execution.

    Suppose that at any given moment, I need a list of directories that are being stored in my domain model. At some time T, I need that list in order to perform operations on the directories. This means that I have to keep a state of the directories, which would periodically get updated. This, at first glance, doesn’t seem like a desirable solution.

    What can be done in these kinds of circumstances?

  6. Sab said

    Nice Article. It helped me understanding the pattern

Some HTML is OK

(required)

(required, but never shared)

or, reply to this post via trackback.