LightMessageBus: A Standalone Lightweight Event Aggregator
In the field of software architecture the terms publish-subscribe pattern, event aggregator, and message bus are closely related to each other. All of them revolve around a loosely coupled messaging paradigm, in which message publishers and message receivers are mutually independent. Their only common point is the communication contract in the form of messages, which are being published and received.
Publish-subscribe denotes the name of the architectural pattern. Event aggregator is a common name for its in-process implementation, i.e. a class which receives all the published messages and propagates them to the subscribed receivers. In contrast, message bus is usually a larger software component enabling a similar messaging pattern between multiple applications. The term enterprise service bus (ESB) is also often used in this context. The meaning of terms is not that strictly defined and they are often used interchangeably in different sources.
This post focuses on the concept, most often named event aggregator, i.e. in-process pub-sub communication between components. Lately, event aggregators are often mentioned in connection with MVVM (model - view - view model) UI architectural pattern. Most MVVM frameworks today include their own event aggregator implementation: Messenger
in MVVM Light, MvxMessenger
in Mvvm Cross, EventAggregator
in Prism for the Windows Runtime, etc.
As long as you're actually using the event aggregator within the frames of the UI, developed using the MVVM pattern, you're of course also going to use the event aggregator distributed with your MVVM framework of choice. The APIs and functionalities of different event aggregator implementations are similar enough, therefore it shouldn't really matter which one you are using.
But what if you want to use an event aggregator in non-MVVM applications? You probably don't want or even can't reference a MVVM framework just to get to the event aggregator implementation. In this case you suddenly have a much smaller choice. My coworker and friend Goran Šiška recently published the first version of LightMessageBus, his own minimalistic event aggregator. I decided to spread some word about it by giving it a proper introduction.
To do that, we need a feasible use case. After all, samples usually don't go past composite UIs, where event aggregators are used for communication between different view models. I'm sure you've already encountered a shopping cart example:
- There's a shopping cart with a number of items displayed in the corner of the application.
- Other parts of the UI publish messages whenever an item is added to the cart.
- The shopping cart is subscribed to this messages.
- This way other view models don't have to know about the shopping cart directly, but can still notify it of new items added to it.
Of course, you'll usually have a MVVM framework available in such a scenario. So, let's venture into the game development for a change. You're typically not going to use MVVM or XAML for game development, so no MVVM framework, as well. This should be a good opportunity for LightMessageBus to prove itself. Here's the scenario:
- We want to implement achievements in our game.
- The achievement tracker needs to know about many different in-game events to track their statistics. It's going to subscribe to all messages it needs to track.
- Once conditions are met for an achievement to unlock, it will publish a corresponding message.
- The notification center will listen to this message (as well as others) and show a pop-up notifying the player about the unlocked achievement.
The first step in implementing such a system should be the definition of the messaging contract: the messages that are going to be published. In LightMessageBus all messages must implement IMessage
interface. Since there's no base class implementing it in the library, you'll probably want to write one yourself to avoid reimplementing the same required property in every message:
public abstract class MessageBase : IMessage
{
public object Source { get; set; }
protected MessageBase(object source)
{
Source = source;
}
}
Both our messages will derive from this base class:
public class LevelEndedMessage : MessageBase
{
public bool Success { get; set; }
public LevelEndedMessage(object source, bool success)
: base(source)
{
Success = success;
}
}
public class AchievementUnlockedMessage : MessageBase
{
public AchievementType AchievementType { get; set; }
public AchievementUnlockedMessage(object source, AchievementType achievementType)
: base(source)
{
AchievementType = achievementType;
}
}
The AchievementTracker
class is subscribed to the LevelEndedMessage
from all publishers:
public AchievementTracker()
{
MessageBus.Default.FromAny().Where<LevelEndedMessage>().Notify(this);
}
To be able to handle it, it must implement IMessageHandler<LevelEndedMessage>
:
public class AchievementTracker : IMessageHandler<LevelEndedMessage>
{
void IMessageHandler<LevelEndedMessage>.Handle(LevelEndedMessage message)
{
if (message.Success)
{
Achievements[AchievementType.LevelsWonInARow].IncrementCount();
}
else
{
Achievements[AchievementType.LevelsWonInARow].ResetCount();
}
}
}
It makes sense to implement the interface explicitly. We don't want to pollute its public interface with a bunch of Handle
methods. The message handler processes the message and updates the corresponding AchievementStatus
instance. Once the achievement is unlocked, the class publishes a new AchievementUnlockedMessage
:
public class AchievementStatus
{
public AchievementType AchievementType { get; set; }
public bool Completed { get; set; }
public int CurrentCount { get; set; }
public int RequiredCount { get; private set; }
private void CheckCompletion()
{
if (CurrentCount >= RequiredCount && !Completed)
{
MessageBus.Default.Publish(
new AchievementUnlockedMessage(this, AchievementType));
Completed = true;
}
}
public void IncrementCount(int delta = 1)
{
CurrentCount += delta;
CheckCompletion();
}
public void ResetCount()
{
CurrentCount = 0;
}
}
Other classes follow the same pattern. When the player completes the level or fails to complete it, the game level will publish the LevelEndedMessage
, which will be delivered to the AchievementTracker
. Similarly, the notification center is subscribed to AchievementUnlockedMessage
to notify the user, whenever it is published. With such architecture, it is easy to extend the game with additional messages, publishers and subscribers, without affecting the existing code.
If you have never done it before, try using an event aggregator in your code, when an opportunity arises: either in an MVVM application or elsewhere. Publish-subscribe is an architectural pattern, worth learning.