Objective Grid : PART II Programmer’s Guide : Chapter 12 Advanced Design Overview : Undo/Redo Architecture
Undo/Redo Architecture
It is essential for user-friendly applications to support Undo and Redo. Therefore, we wanted to implement this feature in Objective Grid. One advanced requirement for Undo and Redo is the need for a transaction like behavior in the grid. This means, several commands should be packed together and undone or redone in one step.
The solution to this problem was to create command objects for each operation. Each operation executed in the grid creates a command object and stores information how to undo the operation in the command object. The command object will be stored in a history list. Every time a command needs to be undone, the command object will be popped from the history list and executed.
Objective Grid provides many operations that let you change the data in the grid. Some examples for operations in the grid are: changing the cell contents, insert, remove or move rows or columns, freeze rows or columns, cut and paste cells and much more. As we mentioned earlier, we took care to separate user interactions from commands, which perform changes in the grid. The code for performing the command is not part of the user interaction code itself. That means, when the user performs an action in the grid, this action will result in a call to a command method. A consequence of this approach is that grid operations can easily be executed programmatically and through user interactions. For example, when the user changes the row height of a row, the grid will execute the command SetRowHeight(). SetRowHeight() can also be called directly from within the grid without user interaction.
The CGXCommand class is an abstract base class for command objects. For every type of operation in the grid, a special CGXCommand-derivative has been implemented which stores all information necessary for undoing the operation into the command.
In Objective Grid, commands are methods in the grid class which instantiate a CGXCommand object only for Undo support. When the user clicks on a menu item, a command method is executed which creates the command object with Undo information.
All the functionality for the grid operations is implemented in the command methods in the grid. Command objects only store information how to call a command method in the grid.
The following sample code illustrates how CGXCommand objects are implemented. The Undo information is passed to the CGXSetFrozenRowsCmd object when the constructor is called. The Execute() method will be called when the command needs to be undone.
 
class CGXSetFrozenRowsCmd: public CGXCommand
{
public:
// Construction
CGXSetFrozenRowsCmd(ROWCOL nFrozenRows, ROWCOL nHeaderRows);
 
// Operation
virtual BOOL Execute(CGXGridCore* pGrid, GXCmdType ctType);
 
// Data (Undo information)
ROWCOL m_nFrozenRows;
ROWCOL m_nHeaderRows;
};
// implementation file (.cpp)
 
CGXSetFrozenRowsCmd::CGXSetFrozenRowsCmd(ROWCOL nFrozenRows, ROWCOL nHeaderRows)
{
m_nFrozenRows = nFrozenRows;
m_nHeaderRows = nHeaderRows;
}
 
BOOL CGXSetFrozenRowsCmd::Execute(CGXGridCore* pGrid, GXCmdType ctCmd)
{
return pGrid->SetFrozenRows(m_nFrozenRows, m_nHeaderRows,
GX_UPDATENOW, ctCmd);
}
In the associated command method, the CGXCommand-derived object will be instantiated and initialized with the previous state in the grid. The object will then be added to the Undo/Redo list by calling AddCommand().
 
BOOL CGXGridCore::SetFrozenRows(ROWCOL nFrozenRows,
ROWCOL nHeaderRows, UINT flags, GXCmdType ctCmd)
{
ROWCOL nOldFrozenRows = GetFrozenRows();
ROWCOL nOldHeaderRows = GetHeaderRows();
if (StoreFrozenRows(nFrozenRows, nHeaderRows))
{
UpdateFrozenRows(nOldFrozenRows, nOldHeaderRows,flags, TRUE);
 
if (ctCmd != gxRollback && m_pParam->m_bUndoEnabled)
AddCommand(new CGXSetFrozenRowsCmd(nOldFrozenRows, nOldHeaderRows),
ctCmd);
 
return TRUE;
}
 
return FALSE;
}
The ctCmd parameter tells the command method if the operation is executed the first time, if it is undone or redone or if should be rolled back (that is, undone without creating Redo information).
AddCommand() interpretes the ctCmd parameter. If the command is executed the first time (= gxDo) or if it is redone (= gxRedo), the command object will be placed in the Undo list. If the command is undone (= gxUndo), the command object will be placed in the Redo list.
 
void CGXGridCore::AddCommand(CGXCommand* pCmd, GXCmdType ctCmd)
{
CObList* pList;
 
if (ctCmd == gxDo || ctCmd == gxRedo)
pList = &GetParam()->GetUndoList();
else
pList = &GetParam()->GetRedoList();
 
pList->AddHead(pCmd);
}
The Undo() operation pops a command object from the Undo list and calls its execute method. The implementation is straightforward:
 
BOOL CGXGridCore::Undo()
{
CObList& undoList = GetParam()->GetUndoList();
 
if (!undoList.IsEmpty())
{
CGXCommand* pCmd = (CGXCommand*) undoList.RemoveHead();
 
pCmd->Execute(this, gxUndo);
delete pCmd;
return TRUE;
}
 
return FALSE;
}
The Redo operation is implemented in the same way.
Figure 122 – Schematic Structure for the Undo/Redo Implementation in Objective Grid
One advanced requirement for Undo/Redo was the need for a transaction like behavior in the grid. This means, several commands should be packed together and undone/redone with one call. The command pattern suggests creating a MacroCommand which can hold several command objects. This approach could also be used in Objective Grid for implementing the CGXBlockCmd command. The CGXBlockCmd class owns a list of command objects. When the Execute command of the CGXBlockCmd is called, it loops through all commands in the lists and calls each commands Execute() method.
The Undo operation is designed to work for the grid, but not for individual edit cells within the grid. In general, changes in the current cell will simply be discarded and no Undo information will be generated. For instance, if you type in several different cells and then fire Undo several times, the focus will move in reverse order from one cell to another inside the grid, performing the Undo operation for a sequence of cells. If you want to be able to undo changes in the current active cell, you must add the following line to your code:
pGrid->TransferCurrentCell();
The following citation from source code demonstrates this technique:
 
BOOL CGXCommandFactory::Undo(CGXGridCore* pGrid)
{
ROWCOL nRow, nCol;
 
// Uncomment the following line if you want to be able
// to undo changes in the current active cell. Otherwise
// changes in current cell will simply be discarded and
// no Undo information generated.
//
// pGrid->TransferCurrentCell();
...
}
Other Issues Related to the Undo/Redo Architecture
How intelligent should a command be? In the Objective Grid approach, a command object merely defines a binding between the grid and a grid action to be carried out. It provides no additional functionality other than simply calling a grid method.
Supporting Undo and Redo. The pattern suggest several options for storing state information in the command objects. In Objective Grid, state information are the arguments to the operation performed on the grid to return the grid to its previous state.
De-coupling. Command de-couples the Undo and Redo methods, which invoke the operation from the methods which perform the operation. That is, Undo and Redo don't have to know what action will be performed when they call the Execute method of a command object.
Transaction. Commands can be assembled into a composite command. This allows transaction-like behavior in the grid.
Also, it should be noted that it is not possible to add new commands without changing the grid class. The user will have to subclass the grid and add a command method to the grid and also subclass the CGXCommand class and create a command class. You can do this if you have a discrete action that causes a state change that you would like to have Undo/Redo capability.