Introduction
I recently started a rather large project and found myself inclined to try and leverage the separation of concerns that the MVVM pattern tries to offer. However before I could start programming using the MVVM pattern in WPF, I realized that the standard .NET library is not enough, so I needed a lot of tools in order to be able to separate the code as desired. I Immediately went for PRISM as my standard toolset and that helped me a lot, especially with the connection between the View and the ViewModels.
But when I came to the design of the ViewModels, I got to the point where I had several different ViewModels that needed to communicate changes to each other, either between properties or from properties to void. After a bit of searching I found, not surprisingly I might add, that Sacha Barbers Cinch had a solution called a Mediator class.
His Mediator is basically a class that has a shared (static) instance and a shared (static) Dictionary that holds all the items that should be connected. In Cinch you can raise an "event" in a property, that send a delegate to a void in another ViewModel . The schematics below show his implementation, and taken from his article about Cinch and the Mediator (CinchV2 :Version 2 of my Cinch MVVM framework: Part 4 of n):
The image shows a one-way communication between a property in ViewModelY and a subroutine (void) in ViewModelX, and the connection is configured and maintained inside the static Mediator. My problem with this design was that it was just that, a one-way communication. I had some instances where I really needed a two-way communication between two or more properties residing in different ViewModels, so I dived into the Mediator class to see how I could change it to fit my needs. I made some changes to allow the Mediator to be able to set a property and/or start a subroutine.
A weak problem
The connection between properties in WPF can easily be resolved using the event that is tailored made; the PropertyDescriptor. The problem in using that inside the mediator is that each time the event is hooked to a property that you want to listen to, it will create a strong reference between the Mediator and the ViewModel were the property lives, in the same way that a binding between the ViewModel and the View does. This will, in turn, make the garbage collector to believe that the class is still in use, so it can't be collected to free up the unused memory.
The problem with a ViewModel in MVVM is that it can be disconnected from the View without the information beeing propagated to the Mediator. This will in turn then keep the unused ViewModel in memory via either a handle or a property, and they will both create a strong reference to it. If you do this multiple times with the same ViewModel, you will soon have lots of unused classes holding on to your computer memory for dear life.
The solution to this problem is to implement WeakReferance and WeakEvents inside the Mediator. This pattern basically tells the garbage collector that I'm bound to this object but you can garbage collect it if you'd like. So if there are no one other than the weak event listeners or weak references connected to the class, you can remove the class from memory. Note that using WeakEvents does not mean that you can subscribe to events and forget about them, you have to unsubscribe to them as they were a normal event.
A WeakEvent in a normal ViewModel is quite simple to implement, and will function as any normal event:
public class StudentViewModel:NotifierBase
{
public StudentViewModel()
{
StudentViewModel student = new StudentViewModel();
WeakEventManager<StudentViewModel, PropertyChangedEventArgs>.AddHandler(student, "PropertyChanged", newStudent_PropertyChanged);
WeakEventManager<StudentViewModel, PropertyChangedEventArgs>.RemoveHandler(student, "PropertyChanged", newStudent_PropertyChanged);
}
void newStudent_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName.Equals("Name"))
{
}
}
private string m_Name = "Name";
public string Name
{
get { return m_Name; }
set
{
SetProperty(ref m_Name, value);
}
}
}
But with reflection it is not so simple anymore. Here I want to give a big thanks to Sascha Lefèvre who showed me how to implement it. First off you need to create a type mold that you want to specify the number of inputs that the WeakEventManager will take, and then what types of arguments you want it to take.
Type unboundWEMType = typeof(WeakEventManager<,>);
Type[] typeArgs = { _sender.Target.GetType(), typeof(EventArgs) };
Type constructedWEMType = unboundWEMType.MakeGenericType(typeArgs);
You then make the generic type:
WeakEventManager<SenderType, EventArgs>
I had some problems here, as it seemed that the reflected type insisted in using the generic arguments of EventArgs
. If I tried to use PropertyChangedEventArgs
it would just throw an error saying that it was unable to cast it. That meant that I had to cast the EventArgs
into PropertyChangedEventsArgs
afterwords:
EventHandler<EventArgs> handler = new EventHandler<EventArgs>(WeakPropertyDescriptor_PropertyChanged);
private void WeakPropertyDescriptor_PropertyChanged(object sender, EventArgs e)
{
PropertyChangedEventArgs arg = (PropertyChangedEventArgs)e;
if (arg.PropertyName == _PropertyName)
{
...
}
}
All that was left was to use the MethodInfo to create the AddHandler and RemovHandler methods:
MethodInfo addHandlerMethod = constructedWEMType.GetMethod("AddHandler");
addHandlerMethod.Invoke(null, new object[] { _sender.Target, "PropertyChanged", handler });
The WeakReferance is much easier to implement:
WeakReference WeakSender = WeakReferance(Sender);
it also holds the object that Sender was in WeakSender.Target
and a property WeakSender.IsAlive
that indicates if the original object is bound to any other object.
How to use it in a ViewModel
So, you have registered the Mediator in the class you want to use it in, as shown below:
public class A : NotifierBase
{
public A()
{
Mediator.Instance.Register(this);
}
...
}
This will enable the Mediator to loop trough all the propertiesand methods in the class and look for any decorated items with marked by the the MediatorConnection
attribute:
[AttributeUsage(AttributeTargets.Method|AttributeTargets.Property)]
public sealed class MediatorConnection : Attribute
{
public string MessageKey { get; private set; }
public bool SendAsync { get; private set; }
public MediatorConnection()
{
MessageKey = null;
SendAsync = false;
}
public MediatorConnection(string messageKey,bool sendAsync = true)
{
MessageKey = messageKey;
SendAsync = sendAsync;
}
}
As you can see it will decorate the property with a unique identifier string called MessageKey, and it will use it to connect different properties or methods with inside the Mediator. It also has an optional argument to allow properties to be send async, it is set to true by default.
The property in the Class
A
is thus implemented as follows (with async updating of other properties enabled):
private string m_Name = "Class A";
[MediatorConnection("NameMessenge")]
public string Name
{
get { return m_Name; }
set
{
SetProperty(ref m_Name, value);
}
}
Please note that for the property to be updated it needs to implement INotifyPropertyChanged
for the Mediator to react to the changes, SetProperty
is just a helper subroutine that implements it. You should also make sure that the INotifyPropertyChanged
event isn't fired if the value is equal to the current value, to avoid resending properties that havent changed.
The last thing that you could do, is to unsubscribe to the Mediator on Dispose:
~A()
{
Mediator.Instance.Unregister(this);
}
Strictly speaking, this isn't necessary, because all the would be removed as soon as someone tries to update it via the Mediator. This will just force the PropertyMediator to remove it from its watch list immediately.
Inside the Mediator
The Mediator relies heavily on the use of reflection to initialize the weak handlers. First off we loop trough all the properties and methods in the class that is registered, and if they are decorated with a MediatorConnection
attribute it should store all necessary objects of this instance in a Dictionary, with the key that is equal to the attributes MessageKey
.
public void Register(object view)
{
foreach (var mi in view.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public))
{
foreach (var att in mi.GetCustomAttributes(typeof(PropertyMediatorAttribute)))
{
var mha = (PropertyMediatorAttribute)att;
RegisterProperty(mha.MessageKey, view, mi.Name, mha.SendAsync);
}
}
}
The Dictionary where the variables are stored is declared static, and the values are a list of an internal class called WeakMediatorHelper
. All items with the same MessageKey will be added in the list of WeakMediatorHelper
:
private readonly Dictionary<string, List<WeakPropertyDescriptor>> _registeredListners =
new Dictionary<string, List<WeakPropertyDescriptor>>();
since this Dictionary is shared between all PropertyMediator subscribers, it is important to lock the dictionary when you are doing something to it:
lock (_registeredListners)
This will prevent any other instances to make any changes until you release the lock.
All the interesting bits now happens in the internal class (the class below only shows the implementation of the Property part, as the original Mediator had the code for the Delegate method. These are combined in the current downloadable code.):
internal class WeakMediatorHelper
{
private Mediator _MediatorInstance;
private WeakReference _WeakBoundObject;
private Type SendOrListenType;
private string _MessageKey;
private bool _SendAsync;
private bool _IsProperty;
private string _PropertyName;
private EventHandler<EventArgs> handler;
private Type _MessengerType;
private string _methodName;
public WeakMediatorHelper(Mediator MethodOwner, Object sender, string PropertyName, Type PropertyType, string MessageKey, bool SendAsync)
{
_MediatorInstance = MethodOwner;
_WeakBoundObject = new WeakReference(sender);
_PropertyName = PropertyName;
_MessageKey = MessageKey;
_SendAsync = SendAsync;
_IsProperty = true;
_MessengerType = PropertyType;
handler = new EventHandler<EventArgs>(WeakMediatorHelper_PropertyChanged);
Type unboundWEMType = typeof(WeakEventManager<,>);
Type[] typeArgs = { _WeakBoundObject.Target.GetType(), typeof(EventArgs) };
SendOrListenType = unboundWEMType.MakeGenericType(typeArgs);
this.AddHandler();
}
public WeakMediatorHelper(object target, Type actionType, MethodBase mi)
{
_IsProperty = false;
if (target == null)
{
Debug.Assert(mi.IsStatic);
SendOrListenType = mi.DeclaringType;
}
_WeakBoundObject = new WeakReference(target);
_methodName = mi.Name;
_MessengerType = actionType;
LastMessage = new object();
}
public object LastMessage { get; set; }
public bool IsProperty
{
get { return _IsProperty; }
}
public Type MessengerType
{
get { return _MessengerType; }
}
public string PropertyName
{
get { return _PropertyName; }
}
public WeakReference WeakBoundObject
{
get { return _WeakBoundObject; }
}
public bool HasBeenCollected
{
get
{
if (IsProperty)
return (_WeakBoundObject == null || !_WeakBoundObject.IsAlive);
else
return (SendOrListenType == null && (_WeakBoundObject == null || !_WeakBoundObject.IsAlive));
}
}
private void WeakMediatorHelper_PropertyChanged(object sender, EventArgs e)
{
PropertyChangedEventArgs arg = (PropertyChangedEventArgs)e;
if (arg.PropertyName == _PropertyName)
{
if (_SendAsync)
_MediatorInstance.NotifyColleaguesOfVauleChangedAsync(_WeakBoundObject.Target, e, _MessageKey, _PropertyName);
else
_MediatorInstance.NotifyColleaguesOfValueChanged(_WeakBoundObject.Target, e, _MessageKey, _PropertyName);
}
}
public void AddHandler()
{
MethodInfo addHandlerMethod = SendOrListenType.GetMethod("AddHandler");
addHandlerMethod.Invoke(null, new object[] { _WeakBoundObject.Target, "PropertyChanged", handler });
}
public void RemoveHandler()
{
MethodInfo removeHandlerMethod = SendOrListenType.GetMethod("RemoveHandler");
removeHandlerMethod.Invoke(null, new object[] { _WeakBoundObject.Target, "PropertyChanged", handler });
}
public Delegate GetMethod()
{
if (SendOrListenType != null)
{
if (_methodName == null)
return null;
else
return Delegate.CreateDelegate(_MessengerType, SendOrListenType, _methodName);
}
if (_WeakBoundObject != null && _WeakBoundObject.IsAlive)
{
object target = _WeakBoundObject.Target;
if (target != null)
return Delegate.CreateDelegate(_MessengerType, target, _methodName);
}
return null;
}
}
And that is really all that is too it. The other code in the Mediator is just there to update the Dictionary by adding and removing elements or keys, as well as updating the connected methods and properties.
Some functionality tests and tips
The code in the MainWindow simulates a basic binding scenario between a ViewModel and a View:
public partial class MainWindow : Window
{
ViewModelA A = new ViewModelA();
ViewModelB B = new ViewModelB();
public MainWindow()
{
InitializeComponent();
View1.DataContext = A;
View2.DataContext = B;
ViewModelB C = new ViewModelB();
}
private void btn_Click(object sender, RoutedEventArgs e)
{
GC.Collect();
}
}
Please note that I have included a void that shows a message box in ViewModelB, and I know that this is not a good MVVM design, but it will show you the benefit of setting all the SendAsync commands to true. If you change this to false, the UI will in some instances not update the values before the Message box is closed (it will depend on the internal order in whitch the elements are stored in the Dictionary, and I cant know that in advance). In async mode, it will not wait for the Message Box to close.
I intended the property connections to be used for user inputs (from the same user) that could happen in different ViewModels, and so it made sense to update or connect two properties together. You should be a bit careful when using this in an application, as you could easily set up two competing properties in code that are linked together, causing an everlasting tautology to happen.
If you need checks or any other things with this you might want to implement this, as suggested by Pete O'Hanlon with the use of the Reactive Extensions. I've never used it before but this video on Channel 9 was really impressive, and the examples are really easy to use. However, in order to make it work for WeakEvents and WeakReferances
you need to do a bit of tweaking for them to be implemented correctly, along the lines of this. I will hopefully make some changes that will implement the Mediator using Rx in the future.