The GangOfFour Command Pattern tells about the recurring design theme of separating execution of a command from its invoker allowing parameterize clients with different requests, queue or log requests, and support undoable operations
We can use the command pattern to implement multi-level undo/ redo functionality by tracking the commands executed in the context. For this we need a Command Manager to keep track of the commands on a stack that can be later used for Undo/ Redo execution of the commands.
The classes used in this sample looks like
Command.cs
public abstract class Command
{
public abstract bool Execute();
}
UndoCommand.cs
public abstract class UndoCommand : Command
{
public abstract bool Undo();
}
CommandManager.cs
public class CommandManager : Stack<Command>
{
public UndoCommand PopCommand()
{
if (Count == 0) return default(UndoCommand);
var command = Pop();
while (!(command is UndoCommand))
{
if (Count == 0) return default(UndoCommand);
command = Pop();
}
return command as UndoCommand;
}
public void AddCommand(Command command)
{
Push(command);
}
public Command GetLastCommand()
{
return Count == 0 ? default(Command) : Peek();
}
}
A real world command implementation
public class CopyCommand : UndoCommand
{
public CopyCommand(Document document)
{
if (document == null) throw new ArgumentNullException("document");
_document = document;
}
public override bool Undo()
{
if(string.IsNullOrWhiteSpace(_previousSelection)) return false;
_document.CopiedText = _previousSelection;
return true;
}
public override bool Execute()
{
if(string.IsNullOrWhiteSpace(_document.SelectedText)) return false;
_previousSelection = _document.CopiedText;
_document.CopiedText = _document.SelectedText;
return true;
}
private readonly Document _document;
private string _previousSelection;
}
Testing the pattern
public class Document
{
public Document()
{
InitializeCommands();
}
private void InitializeCommands()
{
_commandManager = new CommandManager();
_copyCommand = new CopyCommand(this);
}
public void Copy()
{
var result = _copyCommand.Execute();
if (result) _commandManager.AddCommand(_copyCommand);
}
public void Undo()
{
var undoCommand = _commandManager.PopCommand();
if(undoCommand != default(UndoCommand)) undoCommand.Undo();
}
public void Redo()
{
var command = _commandManager.GetLastCommand();
if (command != default(Command)) command.Execute();
}
public string CopiedText { get; set; }
public string SelectedText { get; set; }
private CopyCommand _copyCommand;
private CommandManager _commandManager;
}
[TestMethod]
public void ExecuteMethodOnTheCommandShouldExecuteTheLogicForTheCommandObjectTest()
{
var document = new Document();
const string selectionText = "sample selection text";
document.SelectedText = selectionText;
document.Copy();
Assert.IsTrue(document.CopiedText == selectionText);
}
[TestMethod]
public void CommandsShouldHaveUndoFunctionalityToRevertBackTheObjectToPreviousState()
{
var document = new Document {CopiedText = "Previous selection"};
const string selectionText = "sample selection text";
document.SelectedText = selectionText;
document.Copy();
document.Undo();
Assert.IsTrue(document.CopiedText == "Previous selection");
}
[TestMethod]
public void RedoShouldExecuteTheLastExecutedCommand()
{
var document = new Document { CopiedText = "Previous selection" };
const string selectionText = "sample selection text";
document.SelectedText = selectionText;
document.Copy();
document.Redo();
Assert.IsTrue(document.CopiedText == selectionText);
}