About 1 year ago a good friend of mine Marlon Grech wrote a lovely article on MVC + M. You can read all about Marlons great article over at http://marlongrech.wordpress.com/2008/03/20/more-than-just-mvc-for-wpf/
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 Marlons original post was using the MVC pattern along with an additional class called the Mediator whom 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 textboxes, 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
Lets have a look at some code:
1st a helper class (lifted from Marlons 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 Marlons original code he used Strings, where as 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: 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 lets 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