I have created several add-ins that perform different tasks for a buisness application. Each of these components are self contained and therefore have no knowledge of the existance of any of the other add-ins hosted by SoapBox Core. This, of course, is the power of a composite application and is the reason I chose to use SBC in the first place. However, I have now reached a point where I need my add-ins to be able to talk with one another and work together.

SoapBox Core, already provides some support for doing exactly what is being asked about. Specifically, if you go back and look at the PinBall demo you'll see that it imports an IEnumerable<> of 'IExecutableCommand' using the 'ExtensionPoints.PinBall.GameOverCommands'. This extension point is then used by the PinBall add-in to "communicate" to any other interested add-ins that, "a game has ended, you should do something now".

This is a great start and is all that is needed in many cases. However, this commanding pattern does not provide some important features:

  1. There is no simple way to pass a strongly typed parameter (or two, or three,..., or N) to the IExecutableCommand(s) that were imported to the list.

  2. The only time an add-in can register for the gameovercommands (for example) is during composition. But what if a new object is created after composition and needs to add itself to the gameovercommands? I don't want to have to implement the registering of new IExecutableCommands on all of my ViewModels after composition.

  3. Composing too many commands can make the start-up time of your application too long unless you use Lazy<>.

  4. There is no way for an add-in to 'unregister' itself once it has been imported. Maybe an add-in will want to listen for some set of things to happen when it is in some initial state, but then an entirely different set of things once it has transitioned into another state. (This is the converse of (2))

  5. If an object exports into the gameovercommands, but is then disposed, the command itself will throw an exception everytime the importer tries to call Run() on it. Though SBC does a good job of error handling in this case, it remains a import concern when implemented in other places and can be easy to forget or overlook. Plus there is a potential performance issue here if lots of commands get disposed and too many exceptions are getting thrown.

  6. There is no support for any sort of callback.

So what's a developer to do?

How can I get my add-ins to talk to one-another while still allowing them to be ignorant of each others existance and over come the limitations listed above?

asked 10 Dec '10, 10:14

KarlB's gravatar image

KarlB
576202136
accept rate: 0%

edited 10 Dec '10, 11:33


The Microsoft Patterns and Practices Team released PRISM 4 last month which is the newest installment of their composite application guidance – an excellent resource for anyone using SoapBox. Chapter 9 of this document is titled ‘Communicating Between Loosely Coupled Components’. This chapter explains why loosley coupled communication between add-ins is important then it discusses several different approaches for enabling such communication without creating direct add-in dependencies. The article also discusses th challenges involved in doing so by alerting the user to issues that must be addressed when designing a successful messenger.

But what about cross-add-in communication specifically in SoapBox? Well, being the SoapBox disciple that I am, I decided to create a ‘MessageBusService’ add-in for SoapBox Core. After reading the new PRISM guidance, and this article on the message bus pattern I decided that Laurent Bugion’s MVVM Light Toolkit Messenger class would be great to have in SBC. (plus, as previously pointed out by Scott, the licenses are the same, so it feels like a perfect fit).

It only took the tiniest amount of work to download the MVVM Light Source Code, alter the messenger class to better suite SBC (at least I think so), and have it up and running as its’ own add-in. As you can see from the code I have included below, the only two changes I made to the original MVVM Light Toolkit Messenger source is to rename the class (‘MessageService’ was already taken) and to remove the singleton pattern implementation parts (MEF takes care of that for us in SBC).

Here is the complete plug-in:

namespace SoapBox.Core.MessageBus
{
    /// <summary>
    /// The Messenger is a class allowing objects to exchange messages.
    /// </summary>
    ////[ClassInfo(typeof(Messenger),
    ////    VersionString = "3.0.0.0",
    ////    DateString = "201003041420",
    ////    Description = "A messenger class allowing a class to send a message to multiple recipients",
    ////    UrlContacts = "http://www.galasoft.ch/contact_en.html",
    ////    Email = "[email protected]")]
    [Export(Services.MessageBus.MessageBusService, typeof(IMessageBusService))]
    public class MessageBusService : IMessageBusService
    {
        private Dictionary<Type, List<WeakActionAndToken>> _recipientsOfSubclassesAction;

        private Dictionary<Type, List<WeakActionAndToken>> _recipientsStrictAction;

        public MessageBusService()
        {

        }

        /// <summary>
        /// Registers a recipient for a type of message TMessage. The action
        /// parameter will be executed when a corresponding message is sent.
        /// <para>Registering a recipient does not create a hard reference to it,
        /// so if this recipient is deleted, no memory leak is caused.</para>
        /// </summary>
        /// <typeparam name="TMessage">The type of message that the recipient registers
        /// for.</typeparam>
        /// <param name="recipient">The recipient that will receive the messages.</param>
        /// <param name="action">The action that will be executed when a message
        /// of type TMessage is sent.</param>
        public virtual void Register<TMessage>(object recipient, Action<TMessage> action)
        {
            Register(recipient, null, false, action);
        }

        /// <summary>
        /// Registers a recipient for a type of message TMessage.
        /// The action parameter will be executed when a corresponding 
        /// message is sent. See the receiveDerivedMessagesToo parameter
        /// for details on how messages deriving from TMessage (or, if TMessage is an interface,
        /// messages implementing TMessage) can be received too.
        /// <para>Registering a recipient does not create a hard reference to it,
        /// so if this recipient is deleted, no memory leak is caused.</para>
        /// </summary>
        /// <typeparam name="TMessage">The type of message that the recipient registers
        /// for.</typeparam>
        /// <param name="recipient">The recipient that will receive the messages.</param>
        /// <param name="receiveDerivedMessagesToo">If true, message types deriving from
        /// TMessage will also be transmitted to the recipient. For example, if a SendOrderMessage
        /// and an ExecuteOrderMessage derive from OrderMessage, registering for OrderMessage
        /// and setting receiveDerivedMessagesToo to true will send SendOrderMessage
        /// and ExecuteOrderMessage to the recipient that registered.
        /// <para>Also, if TMessage is an interface, message types implementing TMessage will also be
        /// transmitted to the recipient. For example, if a SendOrderMessage
        /// and an ExecuteOrderMessage implement IOrderMessage, registering for IOrderMessage
        /// and setting receiveDerivedMessagesToo to true will send SendOrderMessage
        /// and ExecuteOrderMessage to the recipient that registered.</para>
        /// </param>
        /// <param name="action">The action that will be executed when a message
        /// of type TMessage is sent.</param>
        public virtual void Register<TMessage>(object recipient, bool receiveDerivedMessagesToo, Action<TMessage> action)
        {
            Register(recipient, null, receiveDerivedMessagesToo, action);
        }

        /// <summary>
        /// Registers a recipient for a type of message TMessage.
        /// The action parameter will be executed when a corresponding 
        /// message is sent.
        /// <para>Registering a recipient does not create a hard reference to it,
        /// so if this recipient is deleted, no memory leak is caused.</para>
        /// </summary>
        /// <typeparam name="TMessage">The type of message that the recipient registers
        /// for.</typeparam>
        /// <param name="recipient">The recipient that will receive the messages.</param>
        /// <param name="token">A token for a messaging channel. If a recipient registers
        /// using a token, and a sender sends a message using the same token, then this
        /// message will be delivered to the recipient. Other recipients who did not
        /// use a token when registering (or who used a different token) will not
        /// get the message. Similarly, messages sent without any token, or with a different
        /// token, will not be delivered to that recipient.</param>
        /// <param name="action">The action that will be executed when a message
        /// of type TMessage is sent.</param>
        public virtual void Register<TMessage>(object recipient, object token, Action<TMessage> action)
        {
            Register(recipient, token, false, action);
        }

        /// <summary>
        /// Registers a recipient for a type of message TMessage.
        /// The action parameter will be executed when a corresponding 
        /// message is sent. See the receiveDerivedMessagesToo parameter
        /// for details on how messages deriving from TMessage (or, if TMessage is an interface,
        /// messages implementing TMessage) can be received too.
        /// <para>Registering a recipient does not create a hard reference to it,
        /// so if this recipient is deleted, no memory leak is caused.</para>
        /// </summary>
        /// <typeparam name="TMessage">The type of message that the recipient registers
        /// for.</typeparam>
        /// <param name="recipient">The recipient that will receive the messages.</param>
        /// <param name="token">A token for a messaging channel. If a recipient registers
        /// using a token, and a sender sends a message using the same token, then this
        /// message will be delivered to the recipient. Other recipients who did not
        /// use a token when registering (or who used a different token) will not
        /// get the message. Similarly, messages sent without any token, or with a different
        /// token, will not be delivered to that recipient.</param>
        /// <param name="receiveDerivedMessagesToo">If true, message types deriving from
        /// TMessage will also be transmitted to the recipient. For example, if a SendOrderMessage
        /// and an ExecuteOrderMessage derive from OrderMessage, registering for OrderMessage
        /// and setting receiveDerivedMessagesToo to true will send SendOrderMessage
        /// and ExecuteOrderMessage to the recipient that registered.
        /// <para>Also, if TMessage is an interface, message types implementing TMessage will also be
        /// transmitted to the recipient. For example, if a SendOrderMessage
        /// and an ExecuteOrderMessage implement IOrderMessage, registering for IOrderMessage
        /// and setting receiveDerivedMessagesToo to true will send SendOrderMessage
        /// and ExecuteOrderMessage to the recipient that registered.</para>
        /// </param>
        /// <param name="action">The action that will be executed when a message
        /// of type TMessage is sent.</param>
        public virtual void Register<TMessage>(object recipient,object token,bool receiveDerivedMessagesToo,Action<TMessage> action)
        {
            var messageType = typeof(TMessage);

            Dictionary<Type, List<WeakActionAndToken>> recipients;

            if (receiveDerivedMessagesToo)
            {
                if (_recipientsOfSubclassesAction == null)
                {
                    _recipientsOfSubclassesAction = new Dictionary<Type, List<WeakActionAndToken>>();
                }

                recipients = _recipientsOfSubclassesAction;
            }
            else
            {
                if (_recipientsStrictAction == null)
                {
                    _recipientsStrictAction = new Dictionary<Type, List<WeakActionAndToken>>();
                }

                recipients = _recipientsStrictAction;
            }

            List<WeakActionAndToken> list;

            if (!recipients.ContainsKey(messageType))
            {
                list = new List<WeakActionAndToken>();
                recipients.Add(messageType, list);
            }
            else
            {
                list = recipients[messageType];
            }

            var weakAction = new WeakAction<TMessage>(recipient, action);
            var item = new WeakActionAndToken
            {
                Action = weakAction,
                Token = token
            };
            list.Add(item);

            Cleanup();
        }

        /// <summary>
        /// Sends a message to registered recipients. The message will
        /// reach all recipients that registered for this message type
        /// using one of the Register methods.
        /// </summary>
        /// <typeparam name="TMessage">The type of message that will be sent.</typeparam>
        /// <param name="message">The message to send to registered recipients.</param>
        public virtual void Send<TMessage>(TMessage message)
        {
            SendToTargetOrType(message, null, null);
        }

        /// <summary>
        /// Sends a message to registered recipients. The message will
        /// reach only recipients that registered for this message type
        /// using one of the Register methods, and that are
        /// of the targetType.
        /// </summary>
        /// <typeparam name="TMessage">The type of message that will be sent.</typeparam>
        /// <typeparam name="TTarget">The type of recipients that will receive
        /// the message. The message won't be sent to recipients of another type.</typeparam>
        /// <param name="message">The message to send to registered recipients.</param>
        [SuppressMessage("Microsoft.Design","CA1004:GenericMethodsShouldProvideTypeParameter",Justification = "This syntax is more convenient than other alternatives.")]
        public virtual void Send<TMessage, TTarget>(TMessage message)
        {
            SendToTargetOrType(message, typeof(TTarget), null);
        }

        /// <summary>
        /// Sends a message to registered recipients. The message will
        /// reach only recipients that registered for this message type
        /// using one of the Register methods, and that are
        /// of the targetType.
        /// </summary>
        /// <typeparam name="TMessage">The type of message that will be sent.</typeparam>
        /// <param name="message">The message to send to registered recipients.</param>
        /// <param name="token">A token for a messaging channel. If a recipient registers
        /// using a token, and a sender sends a message using the same token, then this
        /// message will be delivered to the recipient. Other recipients who did not
        /// use a token when registering (or who used a different token) will not
        /// get the message. Similarly, messages sent without any token, or with a different
        /// token, will not be delivered to that recipient.</param>
        public virtual void Send<TMessage>(TMessage message, object token)
        {
            SendToTargetOrType(message, null, token);
        }

        /// <summary>
        /// Unregisters a messager recipient completely. After this method
        /// is executed, the recipient will not receive any messages anymore.
        /// </summary>
        /// <param name="recipient">The recipient that must be unregistered.</param>
        public virtual void Unregister(object recipient)
        {
            UnregisterFromLists(recipient, _recipientsOfSubclassesAction);
            UnregisterFromLists(recipient, _recipientsStrictAction);
        }

        /// <summary>
        /// Unregisters a message recipient for a given type of messages only. 
        /// After this method is executed, the recipient will not receive messages
        /// of type TMessage anymore, but will still receive other message types (if it
        /// registered for them previously).
        /// </summary>
        /// <typeparam name="TMessage">The type of messages that the recipient wants
        /// to unregister from.</typeparam>
        /// <param name="recipient">The recipient that must be unregistered.</param>
        [SuppressMessage("Microsoft.Design","CA1004:GenericMethodsShouldProvideTypeParameter",Justification ="The type parameter TMessage identifies the message type that the recipient wants to unregister for.")]
        public virtual void Unregister<TMessage>(object recipient)
        {
            Unregister<TMessage>(recipient, null);
        }

        /// <summary>
        /// Unregisters a message recipient for a given type of messages and for
        /// a given action. Other message types will still be transmitted to the
        /// recipient (if it registered for them previously). Other actions that have
        /// been registered for the message type TMessage and for the given recipient (if
        /// available) will also remain available.
        /// </summary>
        /// <typeparam name="TMessage">The type of messages that the recipient wants
        /// to unregister from.</typeparam>
        /// <param name="recipient">The recipient that must be unregistered.</param>
        /// <param name="action">The action that must be unregistered for
        /// the recipient and for the message type TMessage.</param>
        public virtual void Unregister<TMessage>(object recipient, Action<TMessage> action)
        {
            UnregisterFromLists(recipient, action, _recipientsStrictAction);
            UnregisterFromLists(recipient, action, _recipientsOfSubclassesAction);
            Cleanup();
        }

        private void CleanupList(IDictionary<Type, List<WeakActionAndToken>> lists)
        {
            if (lists == null)
            {
                return;
            }

            var listsToRemove = new List<Type>();
            foreach (var list in lists)
            {
                var recipientsToRemove = new List<WeakActionAndToken>();
                foreach (var item in list.Value)
                {
                    if (item.Action == null
                        || !item.Action.IsAlive)
                    {
                        recipientsToRemove.Add(item);
                    }
                }

                foreach (var recipient in recipientsToRemove)
                {
                    list.Value.Remove(recipient);
                }

                if (list.Value.Count == 0)
                {
                    listsToRemove.Add(list.Key);
                }
            }

            foreach (var key in listsToRemove)
            {
                lists.Remove(key);
            }
        }

        private bool Implements(Type instanceType, Type interfaceType)
        {
            if (interfaceType == null
                || instanceType == null)
            {
                return false;
            }

            var interfaces = instanceType.GetInterfaces();
            foreach (var currentInterface in interfaces)
            {
                if (currentInterface == interfaceType)
                {
                    return true;
                }
            }

            return false;
        }

        private void SendToList<TMessage>( TMessage message, IEnumerable<WeakActionAndToken> list, Type messageTargetType, object token)
        {
            if (list != null)
            {
                // Clone to protect from people registering in a "receive message" method
                // Bug correction Messaging BL0004.007
                var listClone = list.Take(list.Count()).ToList();

                foreach (var item in listClone)
                {
                    var executeAction = item.Action as IExecuteWithObject;

                    if (executeAction != null
                        && item.Action.IsAlive
                        && item.Action.Target != null
                        && (messageTargetType == null
                            || item.Action.Target.GetType() == messageTargetType
                            || Implements(item.Action.Target.GetType(), messageTargetType))
                        && ((item.Token == null && token == null)
                            || item.Token != null && item.Token.Equals(token)))
                    {
                        executeAction.ExecuteWithObject(message);
                    }
                }
            }
        }

        private void UnregisterFromLists(object recipient, Dictionary<Type, List<WeakActionAndToken>> lists)
        {
            if (recipient == null
                || lists == null
                || lists.Count == 0)
            {
                return;
            }

            lock (lists)
            {
                foreach (var messageType in lists.Keys)
                {
                    foreach (var item in lists[messageType])
                    {
                        var weakAction = item.Action;

                        if (weakAction != null
                            && recipient == weakAction.Target)
                        {
                            weakAction.MarkForDeletion();
                        }
                    }
                }
            }
        }

        private void UnregisterFromLists<TMessage>( object recipient, Action<TMessage> action, Dictionary<Type, List<WeakActionAndToken>> lists)
        {
            var messageType = typeof(TMessage);

            if (recipient == null
                || lists == null
                || lists.Count == 0
                || !lists.ContainsKey(messageType))
            {
                return;
            }

            lock (lists)
            {
                foreach (var item in lists[messageType])
                {
                    var weakActionCasted = item.Action as WeakAction<TMessage>;

                    if (weakActionCasted != null
                        && recipient == weakActionCasted.Target
                        && (action == null
                            || action == weakActionCasted.Action))
                    {
                        item.Action.MarkForDeletion();
                    }
                }
            }
        }

        private void Cleanup()
        {
            CleanupList(_recipientsOfSubclassesAction);
            CleanupList(_recipientsStrictAction);
        }

        private void SendToTargetOrType<TMessage>(TMessage message, Type messageTargetType, object token)
        {
            var messageType = typeof(TMessage);

            if (_recipientsOfSubclassesAction != null)
            {
                // Clone to protect from people registering in a "receive message" method
                // Bug correction Messaging BL0008.002
                var listClone = _recipientsOfSubclassesAction.Keys.Take(_recipientsOfSubclassesAction.Count()).ToList();

                foreach (var type in listClone)
                {
                    List<WeakActionAndToken> list = null;

                    if (messageType == type
                        || messageType.IsSubclassOf(type)
                        || Implements(messageType, type))
                    {
                        list = _recipientsOfSubclassesAction[type];
                    }

                    SendToList(message, list, messageTargetType, token);
                }
            }

            if (_recipientsStrictAction != null)
            {
                if (_recipientsStrictAction.ContainsKey(messageType))
                {
                    var list = _recipientsStrictAction[messageType];
                    SendToList(message, list, messageTargetType, token);
                }
            }

            Cleanup();
        }

        private struct WeakActionAndToken
        {
            public WeakAction Action;

            public object Token;
        }
    }
}

(There are some helper classes used in there whose source I will not include here, but are included with the MVVM Light Toolkit source and that I had to copy into my add-in project.)

To complete the use of this new MessageServiceBus add-in I simply imported it to the AbstractViewModel class, like so:

[Export(SoapBox.Core.ExtensionPoints.Host.Void, typeof(Object))] // us to load so we get MEF to run
    public class AbstractViewModel : IViewModel
    {

        #region " messageBus singleton "
        /// <summary>
        /// Anyone who inherits from AbstractViewModel gets a free
        /// reference to the message bus service.
        /// </summary>
        [Import(Services.MessageBus.MessageBusService, typeof(IMessageBusService))]
        protected IMessageBusService messageBus 
        {
            get
            {
                return m_messageBus;
            }
            set
            {
                m_messageBus = value;
            }
        }
        private static IMessageBusService m_messageBus = null;
        public static string m_messageBus_Name = NotifyPropertyChangedHelper.GetPropertyName<AbstractViewModel>(o => o.messageBus);
        #endregion

        #region " logger singleton "
        /// <summary>
        /// Anyone who inherits from AbstractViewModel gets a free
        /// reference to the logging service.
        /// </summary>
        [Import(Services.Logging.LoggingService, typeof(ILoggingService))]
        protected ILoggingService logger
        {
            get
            {
                return m_logger;
            }
            set
            {
                m_logger = value;
            }
        }
        private static ILoggingService m_logger = null;
        public static string m_logger_Name = NotifyPropertyChangedHelper.GetPropertyName<AbstractViewModel>(o => o.logger);
        #endregion

        #region " Implement INotifyPropertyChanged "
      ///…
        protected void NotifyPropertyChanged(PropertyChangedEventArgs e)
        {
            var evt = PropertyChanged;
            if (evt != null)
            {
                evt(this, e);
            }
        }
        public event PropertyChangedEventHandler PropertyChanged;
        #endregion
    }

And now, all the view models of my add-ins can communicate with one-another in a very loosley coupled manner. While addressing the issues raised above as follows:

  1. By creating custom message types, I can pass any strongly typed data I want to the commands. So far, this has included some metadata, or processing instructions.

  2. Any add-in that has a referrence to the message bus service can register for any message it wants at any time. Typically, I register for messaging in the default constructor.

  3. Since I can register at any time, and typcially register in a viewmodels' constructor, I save lots of time during composition. In my case, a noticeable amount of time.

  4. Just as I can register for any message at any time, I can also unregister for any message at any time. I have gotten in the habit of registering during creation and unregistering when my document or pad's panel is closed, then re-registering when it is opened again.

  5. The MessageBusService takes care of all the nulls for me and does a very nice job of cleaning-up often so that it maintains a small list of message types and recipeients.

  6. The MessageBusService has built in support for callbacks. Or you can just process the message, then raise another message as your call back if needed.

There are several other advantages to using this MessageBusService that I am sure you will find as you start playing with it.

Using the MessageBusService has been great and has worked very well, though I must give you the same warning that was given to me: use it carefully. This add-in is nice to work with, but – just like anything else – if used incorrectly, can be bad.

I am sorry that this response has been light on an explainatino of what the MVVM Light Messenger is, but I hope that the referrences I have included here explain all of that. If anyone requests it, I will be more than happy to write a follow up Q&A that gives a specific example of how I have successfully used this add-in.

For a really great demo of the entire MVVM Light Toolkit, watch this Mix10 lecture by Laurent Bugion himself

Please leave any and all thoughts, ideas, comments and suggestions. I really appreciate the feedback.

link

answered 10 Dec '10, 11:33

KarlB's gravatar image

KarlB
576202136
accept rate: 0%

Very cool! I like the Messaging stuff in MVVM Light. This solves the problem of many-to-many communications.

(10 Dec '10, 12:23) Scott Whitlock ♦♦

This is the type of add-in I would like to be able to share with the community. Does anyone know what the progress is with creating a repository for sharing add-in sources? For now, if anyone wants the full add-in project, please just e-mail me and I'd be happy to send it to you.

(11 Dec '10, 01:38) KarlB

There are a couple of other loosely coupled communication paths available in SoapBox Core:

  1. The ICondition interface is a great way to export a boolean condition from an add-in that all other add-ins can subscribe to. I didn't use it in the Demo, but the idea is that you can export, for instance, a SolutionLoaded condition from your main application, and all the interested add-ins can import that condition, and be notified when the boolean changes.

  2. The Options module. If your add-in creates an OptionsPad, it will be notified when the options change.

Of course, both of these only work in a "tree-like" structure. That is, the consumer has to know where the producer is defining the message, or at least the contract name.

link

answered 10 Dec '10, 12:21

Scott%20Whitlock's gravatar image

Scott Whitlock ♦♦
696262833
accept rate: 50%

Scott, thanks for taking the time to add your remarks. For the sake of balance and objectivity will you explain the advantages of using the IExecutabelCommand, ICondition and Options module for cross-component communication? I am afraid that in the process of trying to explain why the MessageBusService is good, I might have implied that these approaches are not - which is FAR from the truth. When are these patterns prefered to using a MessageBusService? How can a developer choose between all these approaches?

(11 Dec '10, 01:29) KarlB
Your answer
toggle preview

Follow this question

By Email:

Once you sign in you will be able to subscribe for any updates here

By RSS:

Answers

Answers and Comments

Markdown Basics

  • *italic* or _italic_
  • **bold** or __bold__
  • link:[text](http://url.com/ "Title")
  • image?![alt text](/path/img.jpg "Title")
  • numbered list: 1. Foo 2. Bar
  • to add a line break simply add two spaces to where you would like the new line to be.
  • basic HTML tags are also supported

Tags:

×1
×1
×1
×1

Asked: 10 Dec '10, 10:14

Seen: 4,560 times

Last updated: 11 Dec '10, 01:38

Related questions

powered by OSQA