Click here to Skip to main content
15,885,244 members
Articles / Desktop Programming / WPF

MVVM # Episode 1

Rate me:
Please Sign up or sign in to vote.
4.96/5 (104 votes)
3 Dec 2013CPOL19 min read 254.2K   5K   366  
Using an extended MVVM pattern for real world LOB applications: Part 1
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;

namespace Messengers
{
	public enum MessageHandledStatus
	{
		NotHandled,         // The message has not been handled
		HandledContinue,    // I have handled the message, but let others know about it
		HandledCompleted,   // I have handled the message but tell nobody else about it
		NotHandledAbort     // I haven't handled the message but want to abort it anyway
	}

	public enum NotificationResult
	{
		MessageNotRegistered,       // The message had no handlers registered
		MessageHandled,             // The message had one or more handlers, and was handled
		MessageAborted,				// The message had a handler who aborted the message
		MessageRegisteredNotHandled // Although the message had one or more handlers, none of them appeared to handle the message
	}
	/// <summary>
	/// Provides loosely-coupled messaging between
	/// various colleague objects.  All references to objects
	/// are stored weakly, to prevent memory leaks.
	/// Based on (stolen from!) the MVVMFoundation Messenger class - but modified to use an enumeration rather than a string
	/// </summary>
	public class Messenger
	{
		static readonly Messenger instance = new Messenger();

		#region Constructor

		/// <summary>
		/// Private constructor for this singleton - use Messenger.Instance
		/// </summary>
		private Messenger()
		{
		}

		#endregion // Constructor

		#region Public Properties
		public static Messenger Instance
		{
			get
			{
				return instance;
			}
		}
		#endregion // Public Properties

		#region Register

		/// <summary>
		/// Registers a call-back method, with no parameter, to be invoked when a specific message is broadcast.
		/// </summary>
		/// <param name="message">The message to register for.</param>
		/// <param name="callback">The call-back to be called when this message is broadcasted.</param>
		public void Register(MessageTypes message, Action callback)
		{
			this.Register(message, callback, null);
		}

		/// <summary>
		/// Registers a call-back method, with a parameter, to be invoked when a specific message is broadcast.
		/// </summary>
		/// <param name="message">The message to register for.</param>
		/// <param name="callback">The call-back to be called when this message is broadcasted.</param>
		public void Register<T>(MessageTypes message, Action<T> callback)
		{
			this.Register(message, callback, typeof(T));
		}

		void Register(MessageTypes message, Delegate callback, Type parameterType)
		{
			if (callback == null)
				throw new ArgumentNullException("callback");

			this.VerifyParameterType(message, parameterType);

			_messageToActionsMap.AddAction(message, callback.Target, callback.Method, parameterType);
		}

		/// <summary>
		/// When your class is listening for a message with a particular method, the DeRegister method will 
		/// remove your delagte from the collection - so this particular listener will no lionger receive this particular message
		/// </summary>
		/// <param name="message"></param>
		/// <param name="callback"></param>
		public void DeRegister(MessageTypes message, Delegate callback)
		{
			_messageToActionsMap.RemoveAction(message, callback.Target, callback.Method);
		}

		/// <summary>
		/// When your class is listening to one or more messages, this DeRegister method will 
		/// remove all delegates for that class from the collection.
		/// Should be called when a ViewModel closes to ensure un-Garbage Collected objects don't continue to
		/// receive messages
		/// </summary>
		/// <param name="callback"></param>
		public void DeRegister(object target)
		{
			_messageToActionsMap.RemoveActions(target);
		}

		[Conditional("DEBUG")]
		void VerifyParameterType(MessageTypes message, Type parameterType)
		{
			Type previouslyRegisteredParameterType = null;
			if (_messageToActionsMap.TryGetParameterType(message, out previouslyRegisteredParameterType))
			{
				if (previouslyRegisteredParameterType != null && parameterType != null)
				{
					if (!previouslyRegisteredParameterType.Equals(parameterType))
						throw new InvalidOperationException(string.Format(
							"The registered action's parameter type is inconsistent with the previously registered actions for message '{0}'.\nExpected: {1}\nAdding: {2}",
							message,
							previouslyRegisteredParameterType.FullName,
							parameterType.FullName));
				}
				else
				{
					// One, or both, of previouslyRegisteredParameterType or callbackParameterType are null.
					if (previouslyRegisteredParameterType != parameterType)   // not both null?
					{
						throw new TargetParameterCountException(string.Format(
							"The registered action has a number of parameters inconsistent with the previously registered actions for message \"{0}\".\nExpected: {1}\nAdding: {2}",
							message,
							previouslyRegisteredParameterType == null ? 0 : 1,
							parameterType == null ? 0 : 1));
					}
				}
			}
		}

		#endregion // Register

		#region NotifyColleagues

		/// <summary>
		/// Notifies all registered parties that a message is being broadcasted.
		/// </summary>
		/// <param name="messageType">The message to broadcast.</param>
		/// <param name="parameter">The parameter to pass together with the message.</param>
		public NotificationResult NotifyColleagues(MessageTypes messageType, object parameter)
		{
			//if (String.IsNullOrEmpty(message))
			//    throw new ArgumentException("'message' cannot be null or empty.");
			NotificationResult result = NotificationResult.MessageNotRegistered;

			Type registeredParameterType;
			if (_messageToActionsMap.TryGetParameterType(messageType, out registeredParameterType))
			{
				if (registeredParameterType == null)
					throw new TargetParameterCountException(string.Format("Cannot pass a parameter with message '{0}'. Registered action(s) expect no parameter.", messageType));
			}

			var actions = _messageToActionsMap.GetActions(messageType);
			if (actions != null)
			{
				Message message = new Message(messageType)
				{
					HandledStatus = MessageHandledStatus.NotHandled,
					Payload = parameter
				};

				foreach (var action in actions)
				{
					Invoke(message, action);
				}

				switch (message.HandledStatus)
				{
					case MessageHandledStatus.NotHandled:
						result = NotificationResult.MessageRegisteredNotHandled;
						break;
					case MessageHandledStatus.HandledContinue:
						result = NotificationResult.MessageHandled;
						break;
					case MessageHandledStatus.HandledCompleted:
						result = NotificationResult.MessageHandled;
						break;
					case MessageHandledStatus.NotHandledAbort:
						result = NotificationResult.MessageAborted;
						break;
					default:
						break;
				}

			}
			return result;

		}
		private void Invoke(Message message, Delegate method)
		{
			if (message.HandledStatus == MessageHandledStatus.HandledContinue || message.HandledStatus == MessageHandledStatus.NotHandled)
			{
				method.DynamicInvoke(message);
			}
		}
		/// <summary>
		/// Notifies all registered parties that a message is being broadcasted.
		/// </summary>
		/// <param name="messageType">The message to broadcast.</param>
		public void NotifyColleagues(MessageTypes messageType)
		{
			//if (String.IsNullOrEmpty(message))
			//    throw new ArgumentException("'message' cannot be null or empty.");

			Type registeredParameterType;
			if (_messageToActionsMap.TryGetParameterType(messageType, out registeredParameterType))
			{
				if (registeredParameterType != null)
					throw new TargetParameterCountException(string.Format("Must pass a parameter of type {0} with this message. Registered action(s) expect it.", registeredParameterType.FullName));
			}

			var actions = _messageToActionsMap.GetActions(messageType);
			if (actions != null)
			{
				// actions.ForEach(action => action.DynamicInvoke());

				foreach (var action in actions)
				{
					action.DynamicInvoke();
				}
			}
		}

		#endregion // NotifyColleauges

		#region MessageToActionsMap [nested class]

		/// <summary>
		/// This class is an implementation detail of the Messenger class.
		/// </summary>
		private class MessageToActionsMap
		{
			#region Constructor

			internal MessageToActionsMap()
			{
			}

			#endregion // Constructor

			#region AddAction

			/// <summary>
			/// Adds an action to the list.
			/// </summary>
			/// <param name="message">The message to register.</param>
			/// <param name="target">The target object to invoke, or null.</param>
			/// <param name="method">The method to invoke.</param>
			/// <param name="actionType">The type of the Action delegate.</param>
			internal void AddAction(MessageTypes message, object target, MethodInfo method, Type actionType)
			{
				//if (message == null)
				//    throw new ArgumentNullException("message");

				if (method == null)
					throw new ArgumentNullException("method");

				lock (_map)
				{
					if (!_map.ContainsKey(message))
						_map[message] = new List<WeakAction>();

					_map[message].Add(new WeakAction(target, method, actionType));
				}
			}

			/// <summary>
			/// Removes an action from the list, for the message type, object and method specified
			/// </summary>
			/// <param name="message"></param>
			/// <param name="target"></param>
			/// <param name="method"></param>
			/// <param name="actionType"></param>
			internal void RemoveAction(MessageTypes message, object target, MethodInfo method)
			{
				lock (_map)
				{
					List<WeakAction> wr;
					if (_map.TryGetValue(message, out wr))
					{
						wr.RemoveAll(wa => target == wa.TargetRef.Target && method == wa.Method);

						if (wr.Count == 0)
							_map.Remove(message);
					}
				}
			}

			/// <summary>
			/// Remove all actions from the list for the given target object
			/// Used when 'closing' a viewmodel
			/// </summary>
			/// <param name="target"></param>
			internal void RemoveActions(object target)
			{
				lock (_map)
				{
					ICollection<List<WeakAction>> x = _map.Values;
					foreach (var item in x)
					{
						item.RemoveAll(wa => target == wa.TargetRef.Target);
					}

				}
			}

			#endregion // AddAction

			#region GetActions

			/// <summary>
			/// Gets the list of actions to be invoked for the specified message
			/// </summary>
			/// <param name="message">The message to get the actions for</param>
			/// <returns>Returns a list of actions that are registered to the specified message</returns>
			internal List<Delegate> GetActions(MessageTypes message)
			{
				//if (message == null)
				//    throw new ArgumentNullException("message");

				List<Delegate> actions;
				lock (_map)
				{
					if (!_map.ContainsKey(message))
						return null;

					List<WeakAction> weakActions = _map[message];
					actions = new List<Delegate>(weakActions.Count);
					for (int i = weakActions.Count - 1; i > -1; --i)
					{
						WeakAction weakAction = weakActions[i];
						if (weakAction == null)
							continue;

						Delegate action = weakAction.CreateAction();
						if (action != null)
						{
							actions.Add(action);
						}
						else
						{
							// The target object is dead, so get rid of the weak action.
							weakActions.Remove(weakAction);
						}
					}

					// Delete the list from the map if it is now empty.
					if (weakActions.Count == 0)
						_map.Remove(message);
				}

				// Reverse the list to ensure the callbacks are invoked in the order they were registered.
				actions.Reverse();

				return actions;
			}

			#endregion // GetActions

			#region TryGetParameterType

			/// <summary>
			/// Get the parameter type of the actions registered for the specified message.
			/// </summary>
			/// <param name="message">The message to check for actions.</param>
			/// <param name="parameterType">
			/// When this method returns, contains the type for parameters 
			/// for the registered actions associated with the specified message, if any; otherwise, null.
			/// This will also be null if the registered actions have no parameters.
			/// This parameter is passed uninitialized.
			/// </param>
			/// <returns>true if any actions were registered for the message</returns>
			internal bool TryGetParameterType(MessageTypes message, out Type parameterType)
			{
				//if (message == null)
				//    throw new ArgumentNullException("message");

				parameterType = null;
				List<WeakAction> weakActions;
				lock (_map)
				{
					if (!_map.TryGetValue(message, out weakActions) || weakActions.Count == 0)
						return false;
				}
				parameterType = weakActions[0].ParameterType;
				return true;
			}

			#endregion // TryGetParameterType

			#region Fields

			// Stores a hash where the key is the message and the value is the list of callbacks to invoke.
			readonly Dictionary<MessageTypes, List<WeakAction>> _map = new Dictionary<MessageTypes, List<WeakAction>>();

			#endregion // Fields
		}

		#endregion // MessageToActionsMap [nested class]

		#region WeakAction [nested class]

		/// <summary>
		/// This class is an implementation detail of the MessageToActionsMap class.
		/// </summary>
		private class WeakAction
		{
			#region Constructor

			/// <summary>
			/// Constructs a WeakAction.
			/// </summary>
			/// <param name="target">The object on which the target method is invoked, or null if the method is static.</param>
			/// <param name="method">The MethodInfo used to create the Action.</param>
			/// <param name="parameterType">The type of parameter to be passed to the action. Pass null if there is no parameter.</param>
			internal WeakAction(object target, MethodInfo method, Type parameterType)
			{
				if (target == null)
				{
					_targetRef = null;
				}
				else
				{
					_targetRef = new WeakReference(target);
				}

				_method = method;

				this.ParameterType = parameterType;

				if (parameterType == null)
				{
					_delegateType = typeof(Action);
				}
				else
				{
					_delegateType = typeof(Action<>).MakeGenericType(parameterType);
				}
			}

			#endregion // Constructor

			#region CreateAction

			/// <summary>
			/// Creates a "throw away" delegate to invoke the method on the target, or null if the target object is dead.
			/// </summary>
			internal Delegate CreateAction()
			{
				// Rehydrate into a real Action object, so that the method can be invoked.
				if (_targetRef == null)
				{
					return Delegate.CreateDelegate(_delegateType, _method);
				}
				else
				{
					try
					{
						object target = _targetRef.Target;
						if (target != null)
							return Delegate.CreateDelegate(_delegateType, target, _method);
					}
					catch
					{
					}
				}

				return null;
			}

			#endregion // CreateAction

			#region Fields

			internal readonly Type ParameterType;

			readonly Type _delegateType;
			readonly MethodInfo _method;
			readonly WeakReference _targetRef;

			#endregion // Fields
			public WeakReference TargetRef
			{
				get
				{
					return _targetRef;
				}
			}
			public MethodInfo Method
			{
				get
				{
					return _method;
				}
			}

		}
		#endregion // WeakAction [nested class]

		#region Fields

		readonly MessageToActionsMap _messageToActionsMap = new MessageToActionsMap();

		#endregion // Fields

	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Devo
Australia Australia
Software developer par excellence,sometime artist, teacher, musician, husband, father and half-life 2 player (in no particular order either of preference or ability)
Started programming aged about 16 on a Commodore Pet.
Self-taught 6500 assembler - wrote Missile Command on the Pet (impressive, if I say so myself, on a text-only screen!)
Progressed to BBC Micro - wrote a number of prize-winning programs - including the best graphics application in one line of basic (it drew 6 multicoloured spheres viewed in perspective)
Trained with the MET Police as a COBOL programmer
Wrote platform game PooperPig which was top of the Ceefax Charts for a while in the UK
Did a number of software dev roles in COBOL
Progressed to Atari ST - learned 68000 assembler & write masked sprite engine.
Worked at Atari ST User magazine as Technical Editor - and was editor of Atari ST World for a while.
Moved on to IBM Mid range for work - working as team leader then project manager
Emigrated to Aus.
Learned RPG programming on the job (by having frequent coffee breaks with the wife!!)
Moved around a few RPG sites
Wrote for PC User magazine - was Shareware Magazine editor for a while.
Organised the first large-scale usage of the Internet in Australia through PC User magazine.
Moved from RPG to Delphi 1
Developed large applications in Delphi before moving on to VB .Net and C#
Became I.T. Manager - realised how boring paper pushing can be
And now I pretty much do .Net development in the daytime, while redeveloping PooperPig for the mobile market at night.

Comments and Discussions