Saturday 7 December 2013

Publish Subscribe / Pipeline Pattern For MVP Example C#

Publish Subscribe is a great pattern, it's one of the patterns that really got me in to patterns in the first place.

The Problem
Not so long ago I was using model-view-presenter pattern with windows forms and web forms (feel free to check out my "Model View Presenter (MVP) for Win Forms and ASP.NET Examples" repository). Web form applications can potentially have many user controls on a page, if something happens in user control A then user control B or a web page needs to be updated. Windows forms data grids and controls might need to be refreshed if something happens on the child form.

You can use delegates/event handlers, and subscribe to changes. This is kind of a viable approach, but you are creating direct dependencies between different Presenters. Additionally, if you have many controls managing these delegates and handlers can become cumbersome.

Publish Subscribe Pattern C# - Before
Event handlers subscribing to different user controls, what a mess...


The Solution
Solution is very logical, inject a single pub/sub pipeline that will live for the lifetime of what's being rendered.

Publish Subscribe Pattern C# - After
Presenters subscribing to a single pub/sub pipeline. 

    public interface IPubSub : IDisposable
    {
        void Subscribe(IReceiver receiver, int messageId);

        void Publish<TEventArgs>(object sender, TEventArgs e, int messageId) 
            where TEventArgs : EventArgs;

        void Unsubscribe(IReceiver receiver, int messageId);
    }

 public sealed class PubSub : IPubSub
    {
        private readonly Dictionary<int, List<IReceiver>> messageIdToReceiver;

        public PubSub()
        {
            this.messageIdToReceiver = new Dictionary<int, List<IReceiver>>();
        }

        public void Subscribe(IReceiver receiver, int messageId)
        {
            List<IReceiver> receivers;

            if (this.messageIdToReceiver.TryGetValue(messageId, out receivers))
            {
                receivers.Add(receiver);
            }
            else
            {
                this.messageIdToReceiver.Add(messageId, new List<IReceiver>() { receiver });
            }
        }

        public void Publish<TEventArgs>(object sender, TEventArgs e, int messageId) 
            where TEventArgs : EventArgs
        {
            List<IReceiver> receivers;

            if (this.messageIdToReceiver.TryGetValue(messageId, out receivers))
            {
                foreach (IReceiver receiver in receivers)
                {
                    IReceiver<TEventArgs> receiverToReceive = receiver as IReceiver<TEventArgs>;

                    if (receiverToReceive != null)
                    {
                        receiverToReceive.Receive(sender, e, messageId);
                    }
                }
            }
        }

        public void Unsubscribe(IReceiver receiver, int messageId) 
        {
            List<IReceiver> receivers;

            if (this.messageIdToReceiver.TryGetValue(messageId, out receivers))
            {
                if (receivers.Count > 1)
                {
                    receivers.Remove(receiver);
                }
                else if(receivers.Count == 1)
                {
                    this.messageIdToReceiver.Remove(messageId);
                }
            }
        }

        public void Dispose()
        {
            this.messageIdToReceiver.Clear();
        }
    }


    public interface IReceiver<TEventArgs> : IReceiver
        where TEventArgs : EventArgs
    {
        void Receive(object sender, TEventArgs e, int messageId);
    }

    public interface IReceiver : IDisposable
    {

    }


The Example
  
 public class PresenterA : IReceiver<EventArgs>, IDisposable
    {
        readonly IPubSub pubSub;

        public PresenterA(IPubSub pubSub)
        {
            this.pubSub = pubSub;
            this.pubSub.Subscribe(this, (int)PubSubMsg.AppInstl);
        }

        public void Receive(object sender, EventArgs e, int messageId)
        {
            if ((PubSubMsg)messageId == PubSubMsg.AppInstl)
            {
                //Now that you received the message, update the UI, etc...
            }
        }

        public void Dispose()
        {
            this.pubSub.Unsubscribe(this, (int)PubSubMsg.AppInstl);
        }
    }

    public class PresenterB
    {
        readonly IPubSub pubSub;

        public PresenterB(IPubSub pubSub)
        {
            this.pubSub = pubSub;
        }

        public void btnApplicationRemove(object sender, EventArgs e)
        {
            //Do what you need to do and then publish the message
            this.pubSub.Publish<EventArgs>(this, e, (int)PubSubMsg.AppInstl);
        }
    }

    public enum PubSubMsg
    {
        AppInstl = 1
    }

PubSub class instance can be created by the IoC container. It should be instantiated for the life time of the page context. This means that it will be isolated from the rest of the application and messages will only be distributed around a single parent view.

When it comes to a Win Form it becomes a bit more complicated. Single PubSub class instance is created and shared through out the entire application. This is great, imagine that you are 4 to 7 screens in from the main home screen. If home screen has subscribed to these messages it will receive them and update it self. When users comes back the home screen there is no need to do anything as it's already synchronized.

The Conclusion
It's an elegant solution for cross Presenter communication. It removes dependencies, and enables memory based synchronous communications.

No comments:

Post a Comment