Tuesday, May 31, 2011

Using Command Pattern for multilevel Undo/ Redo functionality

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);
}

1 comment:

Anonymous said...

Thanks! Very good explanation!