MVC Commands and Undo/Redo
A command is an object that encapsulates an action to be performed on another object or command receiver. Commands invoke one or more methods on the command receiver and store the data required as parameters for those methods. It is also possible to reverse or undo the action performed by a command. Since commands are objects, they can be stored, logged, and used to support undoable operations. Commands are used in MVC to perform actions on models and to support undo and redo capabilities. Although the command design pattern and the undo and redo capabilities are not part of the basic MVC design pattern, they complement MVC very nicely.
The CMvcCommand class provides a base class for all commands. It defines virtual Execute() and Unexecute() functions, which are overridden by derived classes. Classes derived from CMvcCommand must store the data required to execute the command, which usually includes a pointer to the object that is acted upon by the command. The object acted upon by a command is referred to as the command receiver, and is usually a model in the case of MVC. Implementing the Unexecute() command is optional, so the CMvcCommand class defines the IsUndoable() function. Command classes that implement the Unexecute() function must be capable of restoring the command receiver to the state it was in prior to the call to Execute(). Since commands store the parameters needed to execute and undo an action, they can be thought of as persistent function calls.
Commands as Messages
The CMvcCommand class implements the IMessage interface, which is part of the notification mechanism of the subject-observer design pattern (see “The Subject-Observer Pattern.”) Messages are sent from the subject to observers via the OnUpdate() function. An IMessage pointer is passed as a parameter to the OnUpdate() function and used to determine the nature of the change made to the subject. Since commands are messages, they can be used to notify observers of the changes made to the model they execute against. After a command is executed, it can be passed as the message parameter to the model’s UpdateAllObservers() function.
The IMvcUndoRedo interface defines methods for executing, undoing, and redoing commands. The IMvcUndoRedo interface is shown in Example 84. The Do() method executes the given command and logs it. The other methods in this interface are fairly self-explanatory.
Example 84 – The IMvcUndoRedo interface
class IMvcUndoRedo
/* Execute and log a command*/
virtual BOOL Do(MvcCommand* pCmd) = 0;
/* Undo a command*/
virtual MvcCommand* Undo() = 0;
/* Redo a command*/
virtual MvcCommand* Redo() = 0;
/* What is the next command on the undo stack*/
virtual MvcCommand* PeekUndo() = 0;
/* What is the next command on the redo stack*/
virtual MvcCommand* PeekRedo() = 0;
The MvcTransactionModel class is an MVC model that implements command undo and redo capabilities. An excerpt from the declaration of MvcTransactionModel is shown in Example 85.
Example 85 – Declaration of MvcTransactionModel
class MvcTransactionModel : public MvcModel
. . .
/* Reset the state of the transaction model to its initial
virtual void Reset();
/* Tests whether the transaction model has stored new commands
since last save*/
virtual BOOL IsModified() const;
/* Records the specified command for later undo or event
virtual BOOL Log(MvcCommand* pCmd);
/* Execute and log a command*/
virtual BOOL Do(MvcCommand* pCmd);
/* Undo a command*/
virtual MvcCommand* Undo();
/* Redo a command*/
virtual MvcCommand* Redo();
/* Get the command that will be reversed next time Undo is
MvcCommand* PeekUndo();
/* Get the command that will be execute next time Redo is
MvcCommand* PeekRedo();
/* Set the number of commands that can be stored by the
transaction model*/
void SetHistorySize(int m_nHistorySize);
. . .
The MvcTransactionModel maintains two separate stacks of commands: an undo stack and a redo stack. As commands are executed, they are pushed onto the undo stack. If the transaction model is instructed to undo the most recent command, it pops the command off the top of the undo stack and invokes the Unexecute() function. Then, it pushes the command onto the redo stack. If a redo is requested, the exact opposite occurs.