Recently we had a requirement in our project to establish communication between two view models.
The user interface design and their data context values:
The ‘User Control 1” (UC1) has a grid showing the list of business products and ‘User Control 2’ (UC2) a grid with categories of products. The categories and products can be added/removed/changed by the user.
We had a business case that – if the category is removed by the user on UC2 then what should happen to the products on UC1 which are using that category? It was decided to delete those products and it should happen automatically – that is the deletion of category should trigger the deletion of product from the list.
The model contains the observable collection of Products and Categories and bound to the grids on the UI. The categories are deleted with the help of button in one of the columns, this button is connected to a DeleteCommand in the UC2 view model which deletes the selected category from the list.
So how can we “do something” on the products view model when something happens on categories’? What are the options?
- Attach to the Categories collection changed event.
- Pass the product view model to the categories so that categories can call some method like Changed() on product’s view model
- Or we could also let the Categories view model go ahead and delete the product from the Model itself thus automatically reflecting on the product’s grid.
You can see there are problems with all these options – the first two create tight coupling between the view models. And the last one doesn’t couple but does something more terrible – changing the Model that it doesn’t own. It is not meaningful or intuitive for a view model to change a Model class which it does not use on its bound UI and more importantly it violates the single responsibility principle (SRP).
However there is an elegant solution. MVVM Foundation (a framework) offers a Messenger class which facilitates view model communication. It uses Mediator pattern so that there is minimum dependency. Let’s see the code.
1. First a static class which can be used by all the view models.
public static class ViewModelCommunication
{
static ViewModelCommunication()
{
Messaging = new Messenger();
}
//a singleton object for view model communication
public static Messenger Messaging { get; set; }
//list of messages
public static string CategoriesChanged { get { return "CategoriesChanged"; } }
}
We create a “ViewModelCommunication” class which is a static class with a static constructor which initializes the “Messenger”. Also we create a static property for “CategoriesChanged” which can be used by any module to register for this message or raise this message. Additional messages could be added for other message types.
2. Next we modify Product view model to register for notification
ViewModelCommunication.Messaging.Register(ViewModelCommunication.CategoriesChanged, (Action)
delegate()
{
Refresh();
}
);
We use the “Register” method on Messenger class to register our message – CategoriesChanged and assign a delegate to be called whenever such message is received. In our code, the ‘Refresh()’ method would be called whenever the user changes a category.
3. Categories view model raises the alert
//notify messaging colleagues
ViewModelCommunication.Messaging.NotifyColleagues(ViewModelCommunication.CategoriesChanged);
The Categories view model sends notification whenever its own Categories collection changes.
And we are done!
Avoiding Memory leaks
One internal detail – the Messenger class uses a sub-class of WeakReference so that the memory is not leaked (link). Otherwise in our example, let’s say Categories and Products show up on a different windows which can be closed. So when the Products window is closed, the Messaging class will still maintain a reference to it as it points to the delegate – thereby product’s view model would not be GC’ed leading to memory leaks. Using WeakReference solves this problem.
nice explanation and illustration.
ReplyDelete