Click here to Skip to main content
15,885,767 members
Articles / Desktop Programming / Win32

An implementation of Command pattern in C#

Rate me:
Please Sign up or sign in to vote.
4.73/5 (8 votes)
3 Nov 2010CPOL16 min read 50.7K   1.3K   38  
.NET delegates and Generics allow for an elegant implementation of the Command pattern.
using System;
using System.Collections.Generic;
using System.Text;

// This file implements a Command pattern using delegates and generics.
// It provides the facility to perform operations which can be Done, Undone and Redone. It also gives an indication of
// whether the user data has been modified since the last save, whereby Doing an operation on saved data modifies it.
// The user must provide methods for Doing and Undoing whatever operations are required. These methods take two parameters
// and always return an operation definition for their opposite, ie the Do method returns a definition (delegate amd parameters)
// of the corresponding Undo method and the Undo method returns the definition for the corresponding Do method.
// (A Do operation can return null if it has no Undo - an example might be a copy to the clipboard which has no meaningful undo.
// However an Undo operation may not return null for its Do operation).
// Undoing the operation returns it to the unmodified state, Undoing a further operation that was Done before the data was saved
// makes it modified again, and Redoing the last operation again returns the state to unmodified.
// Thus in general the user application defines two methods, each taking two parameters and returning an object of type OperationBase.
// To execute the operation, an Operation object of the required generic form is constructed, requiring a name,
// a delegate method and two parameters. Then its Do method is called. The delegate method executes whatever modifications are
// required on the user data, and constructs another Operation object which specifies the name, method and parameters required
// to undo the operation. This is returned.
// To undo the last operation the application simply calls the static method OperationBase.Undo. Likewise to redo the static method
// OperationBase.Redo() is called.
// Each Operation has a name which is usually used to qualify an Undo or Redo menu item.
// For this reason the name of the Undo operation is usually the same as that of the Do operation, ie. the Undo of an Add
// operation would also have the name "Add" (not, for example, "Remove") so that the menu item says "Undo Add".
// The stack of Undo and Redo operations is not limited, which could lead to memory problems in an application that runs
// for a long time and executes a lot of operations. An enhancement to limit the stacks could be added in a furure version.
// While the use of generics means that any types may be used as the arguments to the actual methods that perform the
// Do and Undo operation, it is not possible with the current .NET framework* to allow for different numbers of arguments.
// Thus all Do and Undo methods must take two parameters (although of course this utility could be upgraded easily to
// use three-parameter methods or more). Any method that does not actually require two arguments must accept a dummy argument
// which it can ignore.
// * This was actually developed using VS2005 .NET2.0. because thatis what I have. I will be upgrading shortly and the possibility
// of improving it will be looked into.

namespace KWUtils
{
	#region Delegate OperationMethod
	/// <summary>
	/// A generic delegate that defines a two-parameter method to be executed to Do or Undo a command
	/// </summary>
	/// <typeparam name="T1">The type of the first parameter of the method</typeparam>
	/// <typeparam name="T2">The type of the second parameter of the method</typeparam>
	/// <param name="operand1">The first parameter of the method</param>
	/// <param name="operand2">The second parameter of the method</param>
	/// <returns></returns>
	public delegate OperationBase OperationMethod<T1, T2>(T1 operand1, T2 operand2);
	#endregion

	#region Delegate CommandEventHandler
	/// <summary>
	/// A delegate that defines the method to be executed when a modifiedChanged event is raised.
	/// </summary>
	/// <param name="sender">The source if the event </param>
	/// <param name="e">A <see cref="CommandEventArgs"/> that contains the event data</param>
	public delegate void CommandEventHandler(object sender, CommandEventArgs e);
	#endregion

	#region class CommandEventArgs
	/// <summary>
	/// Derived from <see cref="System.EventArgs"/> this class provides the arguments that are passed to the <see cref="CommandEventHandler"/></para>
	/// It exposes a bool indicating whether an operation has caused a modification to the program's data.
	/// </summary>
	public class CommandEventArgs : EventArgs
	{
		private bool isModified = false;

		/// <summary>
		/// Constructor. Sets the initial value if the isModified flag
		/// </summary>
		/// <param name="isModified">True if </param>
		public CommandEventArgs(bool isModified)
		{
			this.isModified = isModified;
		}

		/// <summary>
		/// Returns true if the EventArgs object indicates that modifications have taken place.
		/// </summary>
		public bool IsModified
		{
			get
			{
				return isModified;
			}
		}
	}
	#endregion

	#region class OperationBase
	/// <summary>
	/// The abstract base class from which the generic <see cref="Operation"/> classes are derived.
	/// </summary>
	public abstract class OperationBase
	{
		/// <summary>
		/// The name of the operation.
		/// </summary>
		private string name;
		/// <summary>
		/// A list of OperationBase objects that forms the undo list. Every time an operation is done the associated
		/// undo operation is added to this list.
		/// </summary>
		protected static List<OperationBase> undoList = new List<OperationBase>();
		/// <summary>
		/// A list of OperationBase objects that forms the redo list. Every time as operation is undone the associated
		/// do operation is added to this list.
		/// </summary>
		protected static List<OperationBase> redoList = new List<OperationBase>();
		/// <summary>
		/// The index in the undo list where the last save took place. Note that this can be beyond the end of the undo list.
		/// </summary>
		protected static int savePoint = 0;
		/// <summary>
		/// This is used as the sender object when the modifiedChanged event is raised
		/// </summary>
		private static object parent;							// this is set to give the static event a sender
		/// <summary>
		/// Static event that is raised when a modification takes place
		/// </summary>
		public static event CommandEventHandler modifiedChanged;
 
		/// <summary>
		/// Constructor.
		/// </summary>
		/// <param name="name">The name of the operation. Note that the name is usually used to qualify an Undo or Redo menu item.
		/// For this reason the name of the Undo operation is usually the same as that of the Do operation, ie. the Undo of an Add
		/// operation would also have the name "Add" (not, for example, "Remove") so that the menu item says "Undo Add".</param>
		public OperationBase(string name)
		{
			this.name = name;
		}

		/// <summary>
		/// Returns a <see cref="String"/> that represents the current <see cref="Object"/>.
		/// </summary>
		/// <returns>The name of the operation</returns>
		public override string ToString()
		{
			return name;
		}

		/// <summary>
		/// Sets the parent. If required this may be set and will be used as the sender when the <see cref="modifiedChanged"/> event is raised>
		/// </summary>
		public static object Parent
		{
			set
			{
				parent = value;
			}
		}

		/// <summary>
		/// Gets or sets a flag indicating whether a modification has taken place. A modification is deemed to have taken place
		/// if the savePoint is not the index of the last entry in the undo list. When an operation is done its is added to the undo list
		/// causing IsModified to return true. If the operations are undone back to the point where the savepoint is the
		/// index of the last entry in the undo list, then IsModified will once again return false. If further undos are executed IsModified
		/// will once again return true.</para>
		/// When the user application saves its data it should call IsModified = false; This sets the savePoint to the end
		/// of the undo list, thus showing the data as being unmodified.</para>
		/// The user application can also set IsModified = true; Following this call IsModified will always return true, regardless
		/// of what Dos, Undos or Redos are executed, until IsModified is once again set false;</para>
		/// If the modified state changes the modifiedChanged event is fired.
		/// </summary>
		public static bool IsModified
		{
			get
			{
				return savePoint != undoList.Count;
			}
			set
			{
				bool wasModified = (savePoint != undoList.Count);
				if (value)
				{
					// forces IsModified to always return true until IsModified = true is called
					savePoint = -1;
				}
				else
				{
					// sets the savePoint to the end of the undo list so IsModified will return false
					savePoint = undoList.Count;
				}
				if (wasModified != IsModified && modifiedChanged != null)
				{
					modifiedChanged(parent, new CommandEventArgs(IsModified));
				}
			}
		}

		/// <summary>
		/// This method is overridden by the generic derivations of this class
		/// </summary>
		/// <returns>The Execute method returns an object of type OperationBase that defines the opposite operation, ie.
		/// a Do returns an Undo, and Undo returns a Do</returns>
		protected abstract OperationBase Execute();

		/// <summary>
		/// This method is called by the user application to Do an operation. It calls Execute().
		/// The object returned by Execute is the Undo definition and is added to the undo list if it is not null.
		/// Note that if the user executes an Undo after a save and then executes a Do we can never get back to the save point.
		/// In this case IsModified will always return true until another save is done and is it specifically set to false.</para>
		/// If the modified state changes the modifiedChanged event is fired.
		/// </summary>
		public void Do()
		{
			bool wasModified = IsModified;
			OperationBase undoItem = Execute();
			if (undoItem != null)						// operation can return null if there is no undo
			{
				undoList.Add(undoItem);
				if (savePoint >= undoList.Count)
				{
					// IsModified will always return true until another save is done and is it specifically set to false.
					savePoint = -1;
				}
				redoList.Clear();
			}
			if (wasModified != IsModified && modifiedChanged != null)
			{
				modifiedChanged(parent, new CommandEventArgs(IsModified));
			}
		}

		/// <summary>
		/// Static method which executes an Undo by taking an operation of the Undo list and executing it. The operation returned by the Undo
		/// is added to the redo list. If the operation returns null an <see cref="ArgumentException"/> is thrown.</para>
		/// If the modified state changes the modifiedChanged event is fired.
		/// </summary>
		/// <returns>True if the operation is executed, false if the undo list is empty</returns>
		public static bool Undo()
		{
			bool result = false;
			bool wasModified = IsModified;
			if (undoList.Count > 0)
			{
				OperationBase redoItem = undoList[undoList.Count - 1].Execute();
				if (redoItem == null)
				{
					throw (new ArgumentException("An undo method cannot return null for its redo method"));
				}
				redoList.Add(redoItem);
				undoList.RemoveAt(undoList.Count - 1);
				result = true;
			}
			if (wasModified != IsModified && modifiedChanged != null)
			{
				modifiedChanged(parent, new CommandEventArgs(IsModified));
			}
			return result;
		}

		/// <summary>
		/// Static method which executes a redo by taking an operation off the Redo list and executing it. The operation returned by the Redo
		/// is added to the Undo list (it can't be null, otherwise there would have been no Undo to Redo).</para>
		/// If the modified state changes the modifiedChanged event is fired.
		/// </summary>
		/// <returns>True if the operation is executed, false if the redo list is empty</returns>
		public static bool Redo()
		{
			bool result = false;
			bool wasModified = IsModified;
			if (redoList.Count > 0)
			{
				undoList.Add(redoList[redoList.Count - 1].Execute());
				redoList.RemoveAt(redoList.Count - 1);
				result= true;
			}
			if (wasModified != IsModified && modifiedChanged != null)
			{
				modifiedChanged(parent, new CommandEventArgs(IsModified));
			}
			return result;
		}

		/// <summary>
		/// returns the name of the operation that is on top of the undo list, or blank if the undo list is empty.
		/// This is generally used for specifying a name in an Undo menu item.
		/// </summary>
		public static string UndoName
		{
			get
			{
				return undoList.Count > 0 ? undoList[undoList.Count - 1].ToString() : "";
			}
		}

		/// <summary>
		/// returns the name of the operation that is on top of the redo list, or blank if the redo list is empty.
		/// This is generally used for specifying a name in an Redo menu item.
		/// </summary>
		public static string RedoName
		{
			get
			{
				return redoList.Count > 0 ? redoList[redoList.Count - 1].ToString() : "";
			}
		}

		/// <summary>
		/// Completely clears the Command structure by emptying the Redo and Undo stacks and reseting the savePoint.</para>
		/// If the modified state changes the modifiedChanged event is fired.
		/// </summary>
		public static void Clear()
		{
			bool wasModified = IsModified;
			undoList.Clear();
			redoList.Clear();
			savePoint = 0;
			if (wasModified != IsModified && modifiedChanged != null)
			{
				modifiedChanged(parent, new CommandEventArgs(IsModified));
			}
		}

	}
	#endregion

	#region Operation
	/// <summary>
	/// This is a generic class derived from OperationBase, which provides the ability to define generic operations
	/// </summary>
	/// <typeparam name="T1">The type of the first argument passed to the user defined method that executes the operation</typeparam>
	/// <typeparam name="T2">The type of the first argument passed to the user defined method that executes the operation</typeparam>
	public class Operation<T1, T2> : OperationBase
	{
		private OperationMethod<T1, T2> operationMethod;
		private T1 operand1;
		private T2 operand2;

		/// <summary>
		/// Constructor. As well as defining the name in the base class it also defines the method delegate
		/// for the execution of the operation, and its two parameters.
		/// </summary>
		/// <param name="name">The name of the operation. Note that the name is usually used to qualify an Undo or Redo menu item.
		/// For this reason the name of the Undo operation is usually the same as that of the Do operation, ie. the Undo of an Add
		/// operation would also have the name "Add" (not, for example, "Remove") so that the menu item says "Undo Add".</param>
		/// <param name="operationMethod">A generic delegate specifying the user-defined method that will be called to execute the operation.</param>
		/// <param name="operand1">The first argument passed to the user defined method that executes the operation</param>
		/// <param name="operand2">The second argument passed to the user defined method that executes the operation</param>
		public Operation(string name, OperationMethod<T1, T2> operationMethod, T1 operand1, T2 operand2) : base(name)
		{
			this.operationMethod = operationMethod;
			this.operand1 = operand1;
			this.operand2 = operand2;
		}

		/// <summary>
		/// get accessor that returns the first argument.
		/// </summary>
		public T1 Operand1
		{
			get
			{
				return operand1;
			}
		}

		/// <summary>
		/// get accessor that returns the second argument.
		/// </summary>
		public T2 Operand2
		{
			get
			{
				return operand2;
			}
		}

		/// <summary>
		/// Overrides the Execute method to call the user-defined method that executes the operation, passing the genric arguments.
		/// </summary>
		/// <returns></returns>
		protected override OperationBase Execute()
		{
			return operationMethod(operand1, operand2);
		}
	}
	#endregion
}

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
United Kingdom United Kingdom
After acquiring a degree in Electronic Engineering and Physics from Loughborough University, I moved into software engineering. In 1991 I went freelance and have been contracting ever since. I live in the North West of England and spend most of my spare time on the stage, or in Africa. If you like the code in my articles please feel free to offer me a job. If you would like to support my work with Project African Wilderness in Malawi please go to www.ProjectAfricanWilderness.org

Comments and Discussions