Reputation: 43
Background
I am writing a WPF application using the MVVM pattern. I am using a Messenger to communicate between ViewModels as I learned in various tutorials. I am using the implementation of a Messenger class found in the Code section of this post (thanks to @Dalstroem WPF MVVM communication between View Model and Gill Cleeren at Pluralsight).
Due to the large number of Views/VMs needed by my app, each ViewModel is instantiated at the time a View is required and disposed subsequently (view-first, VM specified as DataContext of View).
Issue
The constructor of each ViewModel loads resources (Commands, Services, etc.) as necessary, and registers for messages of interest. Messages that were sent from a previously existing ViewModels are not picked up by new ViewModels.
Thus, I cannot communicate between ViewModels using my Messenger class.
Thoughts
Some examples I've seen use a ViewModelLocator that instantiates all ViewModels upfront. The Views, when created, simply pull the existing ViewModel from the VML. This approach means that Messages will always be received and available in every ViewModel. My concern is that with 30+ ViewModels that all load a substantial amount of data with use, my app will become slow with extended use as each View is used (no resources ever disposed).
I've considered finding a way to store Messages and subsequently resend all messages to any registered recipients. If implemented, this would allow me to call a Resend method of sorts after registering for messages in each ViewModel. I have a few concerns with this approach, including the accumulation of messages over time.
I'm not sure what I'm doing wrong or if there are approachs I just don't know about.
Code
public class Messenger
{
private static readonly object CreationLock = new object();
private static readonly ConcurrentDictionary<MessengerKey, object> Dictionary = new ConcurrentDictionary<MessengerKey, object>();
#region Default property
private static Messenger _instance;
/// <summary>
/// Gets the single instance of the Messenger.
/// </summary>
public static Messenger Default
{
get
{
if (_instance == null)
{
lock (CreationLock)
{
if (_instance == null)
{
_instance = new Messenger();
}
}
}
return _instance;
}
}
#endregion
/// <summary>
/// Initializes a new instance of the Messenger class.
/// </summary>
private Messenger()
{
}
/// <summary>
/// Registers a recipient for a type of message T. The action parameter will be executed
/// when a corresponding message is sent.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="recipient"></param>
/// <param name="action"></param>
public void Register<T>(object recipient, Action<T> action)
{
Register(recipient, action, null);
}
/// <summary>
/// Registers a recipient for a type of message T and a matching context. The action parameter will be executed
/// when a corresponding message is sent.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="recipient"></param>
/// <param name="action"></param>
/// <param name="context"></param>
public void Register<T>(object recipient, Action<T> action, object context)
{
var key = new MessengerKey(recipient, context);
Dictionary.TryAdd(key, action);
}
/// <summary>
/// Unregisters a messenger recipient completely. After this method is executed, the recipient will
/// no longer receive any messages.
/// </summary>
/// <param name="recipient"></param>
public void Unregister(object recipient)
{
Unregister(recipient, null);
}
/// <summary>
/// Unregisters a messenger recipient with a matching context completely. After this method is executed, the recipient will
/// no longer receive any messages.
/// </summary>
/// <param name="recipient"></param>
/// <param name="context"></param>
public void Unregister(object recipient, object context)
{
object action;
var key = new MessengerKey(recipient, context);
Dictionary.TryRemove(key, out action);
}
/// <summary>
/// Sends a message to registered recipients. The message will reach all recipients that are
/// registered for this message type.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="message"></param>
public void Send<T>(T message)
{
Send(message, null);
}
/// <summary>
/// Sends a message to registered recipients. The message will reach all recipients that are
/// registered for this message type and matching context.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="message"></param>
/// <param name="context"></param>
public void Send<T>(T message, object context)
{
IEnumerable<KeyValuePair<MessengerKey, object>> result;
if (context == null)
{
// Get all recipients where the context is null.
result = from r in Dictionary where r.Key.Context == null select r;
}
else
{
// Get all recipients where the context is matching.
result = from r in Dictionary where r.Key.Context != null && r.Key.Context.Equals(context) select r;
}
foreach (var action in result.Select(x => x.Value).OfType<Action<T>>())
{
// Send the message to all recipients.
action(message);
}
}
protected class MessengerKey
{
public object Recipient { get; private set; }
public object Context { get; private set; }
/// <summary>
/// Initializes a new instance of the MessengerKey class.
/// </summary>
/// <param name="recipient"></param>
/// <param name="context"></param>
public MessengerKey(object recipient, object context)
{
Recipient = recipient;
Context = context;
}
/// <summary>
/// Determines whether the specified MessengerKey is equal to the current MessengerKey.
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
protected bool Equals(MessengerKey other)
{
return Equals(Recipient, other.Recipient) && Equals(Context, other.Context);
}
/// <summary>
/// Determines whether the specified MessengerKey is equal to the current MessengerKey.
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((MessengerKey)obj);
}
/// <summary>
/// Serves as a hash function for a particular type.
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
unchecked
{
return ((Recipient != null ? Recipient.GetHashCode() : 0) * 397) ^ (Context != null ? Context.GetHashCode() : 0);
}
}
}
}
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Update
The way my application is architectured, there is a ViewModel used with my MainWindow, which serves as a sort of basic shell. It provides a primary layout with a few controls for navigation and login/logout, etc.
All subsequent Views are displayed inside a ContentControl inside the MainWindow (taking up most of the window real estate). The ContentControl is bound to a "CurrentView" property of my "MainWindowViewModel." The MainWindowViewModel instantiates a custom Navigation service I created for the purpose of selecting and returning the appropriate View to update my "CurrentView" property.
This architecture may be unorthodox, but I wasn't sure how navigation is typically accompished without using out-the-box things like TabControl.
Idea
Building on ideas from @axlj, I could keep an ApplicationState object as a property of my "MainWindowViewModel." Using my Messenger class, I could pub an ApplicationState message whenever injecting a new View in my MainWindow. The ViewModels for each View would, of course, sub this message and gain state immediately upon creation. If any ViewModels make changes to their copy of ApplicationState, they would pub a message. The MainWindowViewModel would then be updated via its subscription.
Upvotes: 4
Views: 5211
Reputation: 49985
...and registers for messages of interest. Messages that were sent from a previously existing ViewModels are not picked up by new ViewModels. Thus, I cannot communicate between ViewModels using my Messenger class.
Why exactly do your VMs need to be aware of historical messages?
Generally messaging should be pub/sub; messages are published ("pub") and anyone who might be interested in specific messages subscribes ("sub") to receive those. The publisher shouldn't care what is done with the message - that is up to the subscriber.
If you have some obscure business case that requires knowledge of previous messages then you should create your own message queue mechanism (i.e. store them in a database and retrieve them based on datetime).
Upvotes: 1
Reputation: 2770
I would recommend against "storing messages" -- even if you work out a good pattern for recovering messages, you'll still end up with logic that is difficult to test. This is really a sign that your view models need to know too much about the application state.
In the case of view model locator -- a well designed view model locator will likely lazy-load the view models, which would leave you in the same place you are right now.
Option 1
Instead, consider using UserControls and DependencyProperties where possible.
Option 2
If your views are in fact really views, then consider a singleton context class that maintains the necessary state and inject that into your view models. The benefit of this method is that your context class can implement INotifyPropertyChanged
and any changes will automatically be propagated to your consuming views.
Option 3
If you're navigating between views, you may want to implement a Navigation service similar to something described here.
interface INavigationService(string location, object parameter) {}
In this case, your parameter is considered your state object. The new view model receives the model data from the view you're navigating away from.
This blog post is helpful in explaining best practices around when to use view models and user controls.
Upvotes: 1