Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Centralised Event Dispatcher in C# - Part 3

0.00/5 (No votes)
18 Feb 2016 4  
Centralised Event Dispatcher in C# - Part 3

Introduction

This is part three of a series of posts where I am going to share with you the solution I have used in a recent project for a centralised event dispatcher. All of the source code will be available in my public GitHub repositories. The concept I have tried to realise is a central class that can raise events for any listener to subscribe to.

In part one, we created the event dispatcher class library, in part two, we consumed it from a Windows Forms application. In this part, we are going to look at pooling commonly raised events and implement an 'ApplicationEventPool'.

UPDATE: Please note that following feedback from others, and during the creation of part 4 the event pooling was not found to give much advantage, so you may wish not to read any further of this article.

Assumptions

It is assumed that the reader of this article already has a good understanding of coding with C#, can create projects and solutions, classes and Windows Forms, and can add references to a project and import references into a class. It is also assumed that the reader understands basic inheritance and interface implementation.

So one of the issues with the solution we currently have is as the system grows, the number of events being raised may grow astronomically, and if each event raised creates a new instance, then a memory conscious system may start to "sweat" especially if the event data is quite large.

With this in mind, we can look to minimise this slightly by allowing the system to pool commonly used events.

Dibware.EventDispatcher.Core

So let's move back into the core class library and see if we can implement a system to pool some of the commonly used events.

First, let's define the contract which we think we are going to need for an object that pools Application Events. In the `Contracts` folder, we will create a new interface named `IApplicationEventPool`. We probably need a method to try to add an event, one to try and get an event, one to try and remove an event, and a last to remove all events.

public interface IApplicationEventPool
{
    bool TryAdd<TEvent>(TEvent @event) where TEvent : class, IApplicationEvent;
    bool TryGet<TEvent>(out TEvent @event) where TEvent : class, IApplicationEvent;
    bool TryRemove<TEvent>(out TEvent @event) where TEvent : class, IApplicationEvent;
    void Clear();
}

For a class which implements this contract:

  • `TryAdd` should try and add an event to the backing store of the class if an event of that specified type does not exist.
  • `TryGet` should retrieve an event of the specified type if one exists in the backing store
  • `TryRemove` should remove an event of the specified type from the backing store if it exists
  • `RemoveAll` should remove all events from the backings store

So now, we can create the implementation of the interface as the `ApplicationEventPool` class. This will require a backing store which will be a dictionary with the key which will be a `System.Type` and a value which will be of `IApplicationEvent`.

public class ApplicationEventPool : IApplicationEventPool
{
    private readonly Dictionary<Type, IApplicationEvent> _applicationEvents;

    public ApplicationEventPool()
    {
        _applicationEvents = new Dictionary<Type, IApplicationEvent>();
    }

    public bool TryAdd<TEvent>(TEvent @event) where TEvent : class, IApplicationEvent
    {
        if (@event == null) throw new ArgumentNullException("event");

        Type eventType = typeof(TEvent);
        if (_applicationEvents.ContainsKey(eventType)) return false;

        _applicationEvents.Add(eventType, @event);

        return true;
    }

    public bool TryGet<TEvent>(out TEvent @event) where TEvent : class, IApplicationEvent
    {
        Type eventType = typeof(TEvent);
        IApplicationEvent applicationEvent;
        @event = null;

        if (_applicationEvents.TryGetValue(eventType, out applicationEvent))
        {
            @event = applicationEvent as TEvent;
        }

        bool eventFound = (@event != null);
        return eventFound;
    }

    public bool TryRemove(Type eventType) //where TEvent : class, IApplicationEvent
    {
        throw new NotImplementedException();
    }

    public void Clear()
    {
        _applicationEvents.Clear();
    }
}

PLEASE NOTE: A set of units tests for the above class can be found in the `ApplicationEventPoolTests` class of the `Dibware.EventDispatcher.Core.Tests` in the repository.

Now that we have an ApplicationEventPoolObject, let's switch back to the UI project and set up a scenario which could use it.

Dibware.EventDispatcher.UI

Let's add a new button to the main form and name it `HelloWorldButton` and give it the text of "Hello World!". Now create a button click handler for the new button. In the handler, let's use the `ApplicationEventDispatcher` to raise a new event called `HelloWorldShouted`.

private void HelloWorldButton_Click(object sender, EventArgs e)
{
    ApplicationEventDispatcher.Dispatch(new HelloWorldShouted());
}

As we haven't got an event called `HelloWorldShouted`, we had better create one in the `Events` folder.

internal class HelloWorldShouted : IApplicationEvent
{
    public string Message
    {
        get { return "Hello, you event driven world, you!"; }
    }
}

Now we need to add a handler method to the main Form controller and wire it up and un-wire it in the `WireUpApplicationEventHandlers` and `UnwireApplicationEventHandlers` methods respectively.

protected override void UnwireApplicationEventHandlers()
{
    ApplicationEventDispatcher.RemoveListener<HelloWorldShouted>(HandleHelloWorld);
    ApplicationEventDispatcher.RemoveListener<ProcessStarted>(HandleProcessStarted);
}

protected override sealed void WireUpApplicationEventHandlers()
{
    ApplicationEventDispatcher.AddListener<HelloWorldShouted>(HandleHelloWorld);
    ApplicationEventDispatcher.AddListener<ProcessStarted>(HandleProcessStarted);
}

Put a break point in the 'HelloWorldButton_Click' method of the `MainForm`, run the application and click the `HelloWorldButton` a few times. you will need to close the message box each time of course! You will likely notice that a new `HelloWorldShouted` event object is created each time. Each time this event is instantiated, it is exactly the same as the last, so this may be a good candidate for using our `ApplicationEventPool`. Let's have a look at implementing this.

In the `MainProcess`, we will create a new private field of interface type `IApplicationEventPool` and initialize it in the constructor as the concrete class `ApplicationEventPool`. We will also pass a reference to the `MainFormController` via its constructor as the interface `IApplicationEventPool`. This will allow us later to swap out the `ApplicationEventPool` for another should we want to for testing or enhancement purposes.

internal class MainProcess : ApplicationEventHandlingBase
{
    private readonly MainFormController _mainFormController;
    private readonly IApplicationEventPool _applicationEventPool;

    public MainProcess(IApplicationEventDispatcher applicationEventDispatcher)
        : base(applicationEventDispatcher)
    {
        _applicationEventPool = new ApplicationEventPool();
        _mainFormController = new MainFormController
                (applicationEventDispatcher, _applicationEventPool);
    }

In the `MainFormController`, we will cache the `IApplicationEventPool` before passing the reference on once more to the `MainForm`.

private readonly IApplicationEventPool _applicationEventPool;
private MainForm _mainForm;

public MainFormController(IApplicationEventDispatcher applicationEventDispatcher,
    IApplicationEventPool applicationEventPool)
    : base(applicationEventDispatcher)
{

    _applicationEventPool = applicationEventPool;
    _mainForm = new MainForm(applicationEventDispatcher, applicationEventPool);

    WireUpApplicationEventHandlers();
}

Finally in the `MainForm`, we will cache the `IApplicationEventPool` once more so it can be accessed throughout the form code.

private readonly IApplicationEventPool _applicationEventPool;

public MainForm(IApplicationEventDispatcher applicationEventDispatcher,
    IApplicationEventPool applicationEventPool)
    : base(applicationEventDispatcher)
{
    InitializeComponent();
}

Now that we have a reference to the `ApplicationEventPool` in the `MainForm`, let's modify the code in the `HelloWorldButton_Click` button handler method.

private void HelloWorldButton_Click(object sender, EventArgs e)
{
    HelloWorldShouted @event;

    var eventAlreadyCached = _applicationEventPool.TryGet(out @event);
    if (!eventAlreadyCached)
    {
        @event = new HelloWorldShouted();
        _applicationEventPool.TryAdd(@event);
    }

    ApplicationEventDispatcher.Dispatch(@event);
}

So now, we declare an event but we won't instantiate it yet. We will try and get this event from the `ApplicationEventPool` and if it exists, we will pass that to the event dispatcher, but if one does not exist, then we will create one and pass that instead.

Clean-Up

We do have an small clean-up task to do in the `MainProcess` where the `ApplicationEventPool` was instantiated. We must clear the pool in the `Dispose(bool)` method.

protected override void Dispose(bool disposing)
{
    if (Disposed) return;

    if (disposing)
    {
        // Free other managed objects that implement IDisposable only
        _mainFormController.Dispose();
        _applicationEventPool.Clear();
        UnwireApplicationEventHandlers();
    }

    // release any unmanaged objects
    // set the object references to null

    Disposed = true;
}

Summary

So now, we can reuse any common events through the lifetime of the application. However, we are not out of the woods yet. Why? Well, this event pool is great for simple events, that carry no data or immutable data that will be the same for each time the event is dispatched but what if our events are mutable or immutable after construction? We can't trust that they will be in the right state when we dispatch them so two classes could dispatch the event and require different data within the event. So how will we handle that? Well, we will look at handling them in the next part, part 4.

Disclaimer - Memory Usage

I am not an expert on Garbage Collection, so please be aware that pooling may not be the best solution for a memory problem using this Event Dispatcher. Pooling may lead to objects that should be short lived and would normally live in Generation 0 and get reclaimed quickly, being moved to Generation 1 or 2 and maybe not being reclaimed until the process ends. You can use `PerfMon.exe` or other profiling tools to help identify memory issues, and make an informed decision on how to combat them.

For further information on the Garbage Collector and [Garbage Collector Generations](https://msdn.microsoft.com/en-us/library/ee787088(v=vs.110).aspx#generations), please see [Fundamentals of Garbage Collection] (https://msdn.microsoft.com/en-us/library/ee787088(v=vs.110).aspx).

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here