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 string
s 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.
- It has 2
textbox
es, one of which is a writable textbox
, which is used to send messages via the Mediator
- 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: 9: 10: 11: 12: 13: 14: public class MultiDictionary<T, K>
15: : Dictionary<T, List<K>>
16: {
17:
18: #region Private Methods
19:
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: 36: 37: 38: 39: 40: 41: public void AddValue(T key, K newItem)
42: {
43: EnsureKey(key);
44: this[key].Add(newItem);
45: }
46:
47:
48: 49: 50: 51: 52: 53: public void AddValues(T key, IEnumerable<K> newItems)
54: {
55: EnsureKey(key);
56: this[key].AddRange(newItems);
57: }
58:
59: 60: 61: 62: 63: 64: 65: 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: 80: 81: 82: 83: 84: 85: 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 String
s, whereas I am now using Action<Object> delegate
s 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: 9: 10: 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:
26: static Mediator()
27: {
28:
29:
30: }
31:
32: private Mediator()
33: {
34:
35: }
36: #endregion
37:
38: #region Public Properties
39:
40: 41: 42: 43: public static Mediator Instance
44: {
45: get
46: {
47: return instance;
48: }
49: }
50:
51: #endregion
52:
53: #region Public Methods
54: 55: 56: 57: 58: 59: 60: 61: public void Register(Action<Object> callback,
62: ViewModelMessages message)
63: {
64: internalList.AddValue(message, callback);
65: }
66:
67:
68: 69: 70: 71: 72: 73: 74: public void NotifyColleagues(ViewModelMessages message,
75: object args)
76: {
77: if (internalList.ContainsKey(message))
78: {
79:
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:
31:
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:
19:
20: Mediator.Instance.Register(
21:
22:
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.