65.9K
CodeProject is changing. Read more.
Home

MVVM Mediator Pattern

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (18 votes)

Apr 9, 2009

CPOL

2 min read

viewsIcon

87242

MVVM Mediator pattern

About a year ago, a good friend of mine, Marlon Grech wrote a lovely article on MVC + M. You can read all about Marlon's great article here.

Essentially, what Marlon did was create a message system to allow disparate MVC classes to communicate with each other. The original code by Marlon was spot on, but I just didn’t like the way he was using strings for his messages, but other than that, there is very little change from my code here to his code. So well done Marlon, you are ace.

Anyway, the idea behind Marlon's original post was using the MVC pattern along with an additional class called the Mediator that knew about messages and who to notify when something happened that warrants a message being sent out.

These days, most folk will use the MVVM pattern when developing WPF. I have a small demo app that does the following.

  1. It has 2 textboxes, one of which is a writable textbox, which is used to send messages via the Mediator
  2. The 2nd textbox is refreshed via the message that is sent via the Mediator when the 1st textbox changes

Let's have a look at some code:

First, a helper class (lifted from Marlon's blog):

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Text;
   5:  
   6:  namespace MediatorDemo
   7:  {
   8:      /// <summary>
   9:      /// The multi dictionary is a dictionary that contains 
  10:      /// more than one value per key
  11:      /// </summary>
  12:      /// <typeparam name="T">The type of the key</typeparam>
  13:      /// <typeparam name="K">The type of the list contents</typeparam>
  14:      public class MultiDictionary<T, K>
  15:          : Dictionary<T, List<K>>
  16:      {
  17:  
  18:          #region Private Methods
  19:          //checks if the key is already present
  20:          private void EnsureKey(T key)
  21:          {
  22:              if (!ContainsKey(key))
  23:              {
  24:                  this[key] = new List<K>(1);
  25:              }
  26:              else
  27:              {
  28:                  if (this[key] == null)
  29:                      this[key] = new List<K>(1);
  30:              }
  31:          }
  32:          #endregion
  33:  
  34:          #region Public Methods
  35:          /// <summary>
  36:          /// Adds a new value in the Values collection
  37:          /// </summary>
  38:          /// <param name="key">The key where to place the 
  39:          /// item in the value list</param>
  40:          /// <param name="newItem">The new item to add</param>
  41:          public void AddValue(T key, K newItem)
  42:          {
  43:              EnsureKey(key);
  44:              this[key].Add(newItem);
  45:          }
  46:  
  47:  
  48:          /// <summary>
  49:          /// Adds a list of values to append to the value collection
  50:          /// </summary>
  51:          /// <param name="key">The key where to place the item in the value list</param>
  52:          /// <param name="newItems">The new items to add</param>
  53:          public void AddValues(T key, IEnumerable<K> newItems)
  54:          {
  55:              EnsureKey(key);
  56:              this[key].AddRange(newItems);
  57:          }
  58:  
  59:          /// <summary>
  60:          /// Removes a specific element from the dict
  61:          /// If the value list is empty the key is removed from the dict
  62:          /// </summary>
  63:          /// <param name="key">The key from where to remove the value</param>
  64:          /// <param name="value">The value to remove</param>
  65:          /// <returns>Returns false if the key was not found</returns>
  66:          public bool RemoveValue(T key, K value)
  67:          {
  68:              if (!ContainsKey(key))
  69:                  return false;
  70:  
  71:              this[key].Remove(value);
  72:  
  73:              if (this[key].Count == 0)
  74:                  this.Remove(key);
  75:  
  76:              return true;
  77:          }
  78:  
  79:          /// <summary>
  80:          /// Removes all items that match the prediacte
  81:          /// If the value list is empty the key is removed from the dict
  82:          /// </summary>
  83:          /// <param name="key">The key from where to remove the value</param>
  84:          /// <param name="match">The predicate to match the items</param>
  85:          /// <returns>Returns false if the key was not found</returns>
  86:          public bool RemoveAllValue(T key, Predicate<K> match)
  87:          {
  88:              if (!ContainsKey(key))
  89:                  return false;
  90:  
  91:              this[key].RemoveAll(match);
  92:  
  93:              if (this[key].Count == 0)
  94:                  this.Remove(key);
  95:  
  96:              return true;
  97:          }
  98:          #endregion
  99:      }
 100:  }

This simply allows more than 1 object to be registered for a particular message.

Next, comes the Mediator which is a singleton, and know how to send and register messages against callback, which is what I have changed. In Marlon's original code, he used Strings, whereas I am now using Action<Object> delegates as callbacks. A minor improvement I feel.

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Text;
   5:  
   6:  namespace MediatorDemo
   7:  {
   8:      /// <summary>
   9:      /// Available cross ViewModel messages
  10:      /// </summary>
  11:      public enum ViewModelMessages { UserWroteSomething = 1 };
  12:  
  13:  
  14:      public sealed class Mediator
  15:      {
  16:          #region Data
  17:          static readonly Mediator instance = new Mediator();
  18:          private volatile object locker = new object();
  19:  
  20:          MultiDictionary<ViewModelMessages, Action<Object>> internalList
  21:              = new MultiDictionary<ViewModelMessages, Action<Object>>();
  22:          #endregion
  23:  
  24:          #region Ctor
  25:          //CTORs
  26:          static Mediator()
  27:          {
  28:  
  29:  
  30:          }
  31:  
  32:          private Mediator()
  33:          {
  34:  
  35:          }
  36:          #endregion
  37:  
  38:          #region Public Properties
  39:  
  40:          /// <summary>
  41:          /// The singleton instance
  42:          /// </summary>
  43:          public static Mediator Instance
  44:          {
  45:              get
  46:              {
  47:                  return instance;
  48:              }
  49:          }
  50:  
  51:          #endregion
  52:  
  53:          #region Public Methods
  54:          /// <summary>
  55:          /// Registers a Colleague to a specific message
  56:          /// </summary>
  57:          /// <param name="callback">The callback to use 
  58:          /// when the message it seen</param>
  59:          /// <param name="message">The message to 
  60:          /// register to</param>
  61:          public void Register(Action<Object> callback, 
  62:              ViewModelMessages message)
  63:          {
  64:              internalList.AddValue(message, callback);
  65:          }
  66:  
  67:  
  68:          /// <summary>
  69:          /// Notify all colleagues that are registered to the 
  70:          /// specific message
  71:          /// </summary>
  72:          /// <param name="message">The message for the notify by</param>
  73:          /// <param name="args">The arguments for the message</param>
  74:          public void NotifyColleagues(ViewModelMessages message, 
  75:              object args)
  76:          {
  77:              if (internalList.ContainsKey(message))
  78:              {
  79:                  //forward the message to all listeners
  80:                  foreach (Action<object> callback in 
  81:                      internalList[message])
  82:                          callback(args);
  83:              }
  84:          }
  85:          #endregion
  86:  
  87:      }
  88:  }

So how does this work, well let's see a ViewModel that sends a message via the mediator, notice the NotifyColleagues(..) method usage below.

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.ComponentModel;
   4:  using System.Linq;
   5:  using System.Text;
   6:  using System.Windows.Input;
   7:  using System.Windows.Data;
   8:  
   9:  
  10:  namespace MediatorDemo
  11:  {
  12:      public class WritersViewModel : ViewModelBase
  13:      {
  14:          private String writerText = null;
  15:  
  16:  
  17:          public WritersViewModel()
  18:          {
  19:  
  20:  
  21:          }
  22:  
  23:          public String WriterText
  24:          {
  25:              get { return writerText; }
  26:              set
  27:              {
  28:                  writerText = value;
  29:                  NotifyChanged("WriterText");
  30:                  //alert others about this change
  31:                  //via Mediator
  32:                  Mediator.Instance.NotifyColleagues(
  33:                      ViewModelMessages.UserWroteSomething, 
  34:                          writerText);
  35:              }
  36:          }
  37:  
  38:      }
  39:  }

And how about dealing with a message from the Mediator, this is fairly simple, though we do have to cast the results from Object to the Type we are expecting. This is necessary as the Mediator uses Action<Object> delegates as callbacks, where Object could be any Type of course.

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.ComponentModel;
   4:  using System.Linq;
   5:  using System.Text;
   6:  using System.Windows.Input;
   7:  
   8:  
   9:  namespace MediatorDemo
  10:  {
  11:      public class ReadersViewModel : ViewModelBase
  12:      {
  13:          private String readText = String.Empty;
  14:  
  15:  
  16:          public ReadersViewModel()
  17:          {
  18:              //register to the mediator for the 
  19:              //UserWroteSomething message
  20:              Mediator.Instance.Register(
  21:  
  22:                  //Callback delegate, when message is seen
  23:                  (Object o) =>
  24:                  {
  25:                      ReadText = (String)o;
  26:                  }, ViewModelMessages.UserWroteSomething);
  27:          }
  28:  
  29:  
  30:          public String ReadText
  31:          {
  32:              get { return readText; }
  33:              set
  34:              {
  35:                  readText = value;
  36:                  NotifyChanged("ReadText");
  37:              }
  38:          }
  39:      }
  40:  }

And that is really all there is to it. You now have a Writer ViewModel that will notify a totally disconnected Reader ViewModel about a change, via the messaging providing by the Mediator/Action<Object> delegate callbacks, that were registered for the message by the class that wants to do something based on the message being registered with the Mediator.

As I say, this is all thanks to Marlon, well done Marlon!

And here is a small demo app for you to play with.