Decoupled ViewModel Messaging (Part 3)
NOTE: All the code shown in this series has been published to codeplex. If you would like to download the framework or the sample application that uses the framework you can find it here: http://courier.codeplex.com/
In this part of our saga we find or intrepid hero trapped in a ……. Wait where was I? So far in this series we have covered decoupled messaging between ViewModels at a high level and we have looked at one specific enhancement that I have added to my courier framework that is lacking in other implementations.
In this final post I am going to cover one more feature that I have added to the courier framework that I think provides a very nice addition to the overall process of decoupled ViewModel messaging and that is message caching.
When I first started with this framework I really just set out to wrap my head around what others had done (CoreMVVM,Cinch, Prism, MVVM Foundation). Once I had gotten to that point I wanted to add a little something more to make the framework my own. What I found was that, in my scenarios, I found myself needing to cache messages for later retrieval.
The most obvious case for this is in the “Wizard” type scenario. When a user is performing a sequential set of actions I often have the need to pass data from one screen to the next. In this scenario the next screen is not created until after the previous screen is destroyed. This means that the message broadcast from screen one would happen before screen two is around to listen for it. With me so far?
So, to solve this problem I figured I would implement some sort of caching mechanism that would allow me to:
- Broadcast a message from View one
- Save the message for re-broadcast
- Destroy View one
- Create View two
- Register for that message from View two
- Receive any, valid, cached copies of the message I registered for
The first thing I did was to implement an internal List<T> inside the mediator class that would store these CachedMessage objects. When a message is broadcast with caching options I will save a copy of the message to this List<T>. Any subsequent registrations for this message will receive any valid cached copies of the messages.
So I added the following to the mediator
private readonly List<CachedMessage> cachedMessages = new List<CachedMessage>();
Where the CachedMessage class is defined like this:
[DebuggerDisplay("Message: {Message}, Parameter: {Parameter}")]
internal class CachedMessage
{
public String Message { get; private set; }
public Object Parameter { get; private set; }
public CacheSettings CacheOptions { get; private set;}
public Int32 ResendCount { get; set; }
public CachedMessage(String message, Object parameter)
{
Message = message;
Parameter = parameter;
}
public CachedMessage(String message, Object parameter, CacheSettings cacheOptions)
{
CacheOptions = cacheOptions;
Message = message;
Parameter = parameter;
}
}
Two things to note here. First the CachedMessage class has a public property of type CacheSettings. This gives us the details of how long to keep the message for. Currently I am supporting two types of caching, time based and recurrence based. This means you can specify a specific DateTime when the message will expire or you can specify how many re-broadcasts before the message expires. The CacheSettings class looks like this:
[DebuggerDisplay("Expiration Date: {ExpirationDate}, NumberOfResends: {NumberOfResends}")]
public class CacheSettings
{
public DateTime ExpirationDate { get; private set; }
public Int32 NumberOfResends { get; private set; }
public CacheSettings(DateTime expiration)
{
ExpirationDate = expiration;
NumberOfResends = Int32.MaxValue;
}
public CacheSettings(Int32 timesToResend)
{
NumberOfResends = timesToResend;
ExpirationDate = DateTime.MaxValue;
}
}
The second thing to note about both of these classes (CachedMessage and CacheSettings) is the use of the DebuggerDisplay attribute. I have just recently got in the habit of using this attribute religiously and I don’t know what I ever did without it. I won’t dive too in depth here about it, but suffice it to say, if you aren’t using it right now you should be. If you don’t know what it is stop reading here and jump over to my colleague, Adam Calderon’s, excellent post about it (don’t worry I will wait for you to come back).
Ok, now that we are all writing Debugger friendly classes thanks to Adam, let’s wrap this post up with some usage scenarios. In the “Wizard” like case I talked about above I really just want to be able to broadcast a message and cache it for one re-broadcast. This allows me to broadcast the message from view one and then destroy view one and create view two. view two then registers for the message like it would normally and will immediately receive any messages that are in the cache. Once the message is dispatched from the cache we want to clean it out to prevent further re-broadcast. So first, two examples of how to call BroadcastMessage<T> with caching options
//NumberOfResend based caching example
Mediator.BroadcastMessage("Content1Message",messageContent,new CacheSettings(1));
//Time Based caching example
Mediator.BroadcastMessage("Content1Message", messageContent, new CacheSettings(DateTime.Now.Add(TimeSpan.FromSeconds(30))));
The first example show how to broadcast a message and specify that it should be cached for one re-broadcast. The second example shows how to broadcast a message and store it in the cache for 30 seconds after the first broadcast. Pretty straightforward so far. Now the two key methods in the Mediator class that handle keeping the cache clean and dispatching messages from cache:
private void GetMessagesFromCache(String message, Delegate callback)
{
CleanOutCache();
//Search the cache for matches messages
List<CachedMessage> matches = cachedMessages.FindAll(action => action.Message == message);
//If we find matches invoke the delegate passed in and pass the message payload
matches.ForEach(delegate(CachedMessage action)
{
callback.DynamicInvoke(action.Parameter);
action.ResendCount++;
});
}
private void CleanOutCache()
{
//Remove any expired messages from the cache
cachedMessages.RemoveAll(message => (message.CacheOptions.ExpirationDate < DateTime.Now) || (message.ResendCount >= message.CacheOptions.NumberOfResends));
}
You can see in the GetMessagesFromCache method the first thing it does is clean out any expired messages from the cache. The CleanOutCache method uses the RemoveAll method on List<T> and an lambda to do a particularly elegant line of code. This code checks two properties of the CachedMessage objects (is it’s expiration date in the past OR has it been re-broadcast the max number of times?). If either is true it is removed from the CachedMessages list. A very succinct line of code for a normally cumbersome task.
After the cache has been cleaned we call FindAll (again, using a simple lambda) to get all the messages from cache that match the one we are looking for (There could be multiple messages from different senders in the cache and we want to dispatch them all). Once we have the matching messages we invoke the callback that is registered and we increment the re-send count on the message. Overall, a pretty simple process, but it turns out to be very powerful.
That wraps up this 3 part series ( I, II, III) on the Courier framework. As I add new features to the framework I will post specific updates about the features. If anyone else is interested in joining the codeplex project and adding their ideas to the framework please feel free to contact me through this blog.
NOTE: All the code shown in this series has been published to codeplex. If you would like to download the framework or the sample application that uses the framework you can find it here: http://courier.codeplex.com/