Create a notification system in C#

One of the things I really like when coding in Objective-C is the NSNotificationCenter object. I can register to receive notifications when a specific event takes place, without my receiving object having any knowledge of where that event takes place within my code. Nor does the object causing the event need to know about the objects that will react to it.

C# has a couple of interfaces that tries to achieve the same effect. IObservable and IObserver. They fall pretty short of what NSNotificationCenter does.

IObservable and IObserver

The following is a basic example of IObservable and IObserver being implemented.

public struct Message
{
    string text;

    public Message(string newText)
    {
        this.text = newText;
    }

    public string Text
    {
        get
        {
            return this.text;
        }
    }
}

public class Headquarters : IObservable<Message data-preserve-html-node="true">
{
    public Headquarters()
    {
        observers = new List<IObserver<Message data-preserve-html-node="true">>();
    }

    private List<IObserver<Message data-preserve-html-node="true">> observers;

    public IDisposable Subscribe(IObserver<Message data-preserve-html-node="true"> observer)
    {
        if (!observers.Contains(observer))
            observers.Add(observer);
        return new Unsubscriber(observers, observer);
    }

    private class Unsubscriber : IDisposable
    {
        private List<IObserver<Message data-preserve-html-node="true">> _observers;
        private IObserver<Message data-preserve-html-node="true"> _observer;

        public Unsubscriber(List<IObserver<Message data-preserve-html-node="true">> observers, IObserver<Message data-preserve-html-node="true"> observer)
        {
            this._observers = observers;
            this._observer = observer;
        }

        public void Dispose()
        {
            if (_observer != null && _observers.Contains(_observer))
                _observers.Remove(_observer);
        }
    }

    public void SendMessage(Nullable<Message data-preserve-html-node="true"> loc)
    {
        foreach (var observer in observers)
        {
            if (!loc.HasValue)
                observer.OnError(new MessageUnknownException());
            else
                observer.OnNext(loc.Value);
        }
    }

    public void EndTransmission()
    {
        foreach (var observer in observers.ToArray())
            if (observers.Contains(observer))
                observer.OnCompleted();

        observers.Clear();
    }
}

public class MessageUnknownException : Exception
{
    internal MessageUnknownException()
    {
    }
}

public class Inspector : IObserver<Message data-preserve-html-node="true">
{
    private IDisposable unsubscriber;
    private string instName;

    public Inspector(string name)
    {
        this.instName = name;
    }

    public string Name
    {
        get
        {
            return this.instName;
        }
    }

    public virtual void Subscribe(IObservable<Message data-preserve-html-node="true"> provider)
    {
        if (provider != null)
            unsubscriber = provider.Subscribe(this);
    }

    public virtual void OnCompleted()
    {
        Console.WriteLine("The headquarters has completed transmitting data to {0}.", this.Name);
        this.Unsubscribe();
    }

    public virtual void OnError(Exception e)
    {
        Console.WriteLine("{0}: Cannot get message from headquarters.", this.Name);
    }

    public virtual void OnNext(Message value)
    {
        Console.WriteLine("{1}: Message I got from headquarters: {0}", value.Text, this.Name);
    }

    public virtual void Unsubscribe()
    {
        unsubscriber.Dispose();
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        Inspector inspector1 = new Inspector("Greg Lestrade");
        Inspector inspector2 = new Inspector("Sherlock Holmes");

        Headquarters headquarters = new Headquarters();

        inspector1.Subscribe(headquarters);
        inspector2.Subscribe(headquarters);

        headquarters.SendMessage(new Message("Catch Moriarty!"));
        headquarters.EndTransmission();

        Console.ReadKey();
    }
}

It's worth noting here that the Headquarters object is strongly typed to only supporting the Message object. That means, that if you ever need to do something different, you have to write a head Headquarters class, implementing IObservable along with your custom Messages class replacement. That goes for the Inspector as well.

On top of that, for each class in your application, you have to implement the IObserver interface. So each class you want to have as an observer, must have a decent amount of code wrote. You could try and abstract it out; it still will require each model object you want to have as an observer to have the above code wrote.

Broadcast

To solve this problem, I created the Broadcast project on GitHub tonight. The Broadcast project contains a single object that you need to interact with. A NotificationManager class.

Using the same example as above, we will create a Inspector class and post a notification to it. Since the Headquarter in the above example acted as the central repository for all Message based communication, we will not need to implement it. Instead, we will write our NotificationManager object in its place.

public static class NotificationManager
{
    // Collection of notification observers.
    private static Dictionary<string, data-preserve-html-node="true" List<NotificationObserver data-preserve-html-node="true">> _Observers = new Dictionary<string, data-preserve-html-node="true" List<NotificationObserver data-preserve-html-node="true">>();

    public static void RegisterObserver(object observer, string notification, Action<object, data-preserve-html-node="true" Dictionary<string, data-preserve-html-node="true" object>> action)
    {
        // We only register valid objects.
        if (string.IsNullOrWhiteSpace(notification) || action == null || observer == null) return;

        // Create a new NotificationObserver object.
        // Currently you provide it a reference to the observer. This is not used anywhere; there are plans to use this.
        var registeredObserver = new NotificationObserver(observer, action);

        // Make sure the notification has already been registered.
        // If not, we add the notification to the dictionary, then add the observer.
        if (_Observers.ContainsKey(notification))
            _Observers[notification].Add(registeredObserver);
        else
        {
            var observerList = new List<NotificationObserver data-preserve-html-node="true">();
            observerList.Add(registeredObserver);
            _Observers.Add(notification, observerList);
        }
    }

    public static void PostNotification(object sender, string notification, Dictionary<string, data-preserve-html-node="true" object> userData)
    {
        // Make sure the notification exists.
        if (_Observers.ContainsKey(notification))
        {
            // Loop through each objects in the collection and invoke their methods.
            foreach (NotificationObserver observer in _Observers[notification])
            {
                observer.Action(sender, userData);
            }
        }
    }

    public static void PostNotificationAsync(object sender, string notification, Dictionary<string, data-preserve-html-node="true" object> userData)
    {
        // Make sure the notification exists.
        if (_Observers.ContainsKey(notification))
        {
            // Loop through each objects in the collection and invoke their methods.
            foreach (NotificationObserver observer in _Observers[notification])
            {
                Task.Run(new Action(() => { observer.Action(sender, userData); }));
            }
        }
    }
}

Alright, the above code isn't to bad. The nice part however is that it is 100% reusable. There will not be a need to create a new Headquarter or NotificationManager class for each type of interaction you want with your objects (such as sending a Message).

While the NotificationManager class might have close to the same amount of code as found in the Headquarter class, we are about to save a lot of work through-out the rest of our app with it.

Let's create the Inspector class next.

public class Inspector
{
    private string instName;

    public Inspector(string name)
    {
        instName = name;
        NotificationManager.RegisterObserver(this, "MessageSent", ReceiveMessage);
    }

    public void ReceiveMessage(object sender, Dictionary<string, data-preserve-html-node="true" object> userData)
    {
        if (sender is string)
            Console.WriteLine(instName + " received the following message:" + sender as string);
    }
}

As you can see, there is a lot less code in here. All we need to do is just register ourself to receive notifications and then handle the notification. With this setup, the Inspector class can register to multiple notifications, each with a different method associated. This allows for much more flexibility.

Lastly, we put it into practice with the app. We just instance two Inspector objects, then post a notification.

class Program
{
    static void Main(string[] args)
    {
        Inspector inspector1 = new Inspector("Bob");
        Inspector inspector2 = new Inspector("Jan");

        NotificationManager.PostNotification("Program broadcasted to both Inspector's from one Notification Post!", "MessageSent");
        Console.ReadKey();
    }
}

Conclusion

There are use cases for both approaches to observing objects although I have yet to find a use-case that Microsoft's implementation satisfied that my Broadcast project could not. With the NotificationManager, you can post notification's asynchronously as well. So the following line

NotificationManager.PostNotificationAsync(someObject, "aNotification");

Will invoke all of the observing object methods asynchronously. This works great for using the NotificationManager in UI based applications. You can broadcast to hundreds of objects and not worry about slowing down the UI responsiveness.

Hopefully this helps you guys out~

Be sure to check out the full project over on GitHub!