Introduction
In the modern world of programming, the command pattern is one of the most frequently uttered word and has received nearly an oscar for its contribution in implementing undo/redo feature in world class apps. However, a practical example of this design pattern is found once in a blue moon if you get lucky (atleast I wasn't that lucky). There are lots of papers, books or sites that deal with the theoretical aspect of this pattern, but none comes up with a concrete example of it at a practical field. So, this article is a demonstration of my very own implementation of command pattern and that too in a database app. What were you expecting? A drawing app with rectangle/ellipse or bitmap draw and undo the draw and redo again :) ..?
Background
In one of my projects, I had to implement a command processor which acted on the guidelines of command pattern to implement several undo/redo steps for certain operations. Later, in one of my colleague's project (which was related to database), work flow serialization was required where after performing several database operations, it was needed to roll back few steps and start from a previous point. That gave me the idea to apply the command pattern on a database app and see the special effect of the rollback and roll on feature. And no!! my genius code wasn't applied on that real database project since I have done it in a very small scale, and the original app's context was different and the scale was huge. Mind you, the concept can still be applied for huge apps.
The Command Processor Class
To implement the command pattern, first what is required - is a command processor class. This class will be the owner of the commands, and manage the execution and roll back of the commands.
In order to encapsulate a command, I have to come up with a useful abstract
class, from where all the application specific commands will be derived from. I named the base class CGCommand
class and it looks like this:
class CGCommand
{
public:
CGCommand(){ }
virtual ~CGCommand() { }
virtual BOOL Execute() = 0;
virtual BOOL UnExecute() = 0;
virtual BOOL CanUndo()
{
return TRUE;
}
virtual BOOL CanRedo()
{
return TRUE;
}
virtual BOOL Load()
{
return FALSE;
}
virtual BOOL Save()
{
return FALSE;
}
};
I hope the comments are self explanatory and the class is too simple to explain. Now that I have given you a glimpse of what the command class looks like, let's have a look at the grand class The Processor of Commands and its APIs:
class CGCommandProcessor
{
public:
CGCommandProcessor();
virtual ~CGCommandProcessor();
virtual BOOL Submit(CGCommand* pCommand, BOOL bStore = TRUE,
BOOL bReleaseIfNotStored = TRUE);
virtual BOOL Store(CGCommand* pCommand);
CGCommand* GetCurrentCommand();
CGCommand* GetNextCommand();
virtual BOOL CanUndo();
virtual BOOL CanRedo();
virtual BOOL Undo();
virtual BOOL Redo();
void SetCommandsArrayMaxSize(UINT_PTR nMaxNoCommands)
{
m_nMaxNoCommands = nMaxNoCommands;
}
UINT_PTR GetCommandsArrayMaxSize()
{
return m_nMaxNoCommands;
}
void ClearCommands();
void ClearCommands(INT_PTR nStartIndex, INT_PTR nCount = 1);
....
....
};
So, I guess you are almost clear about what the command processor does. It takes a command through the Submit
API and executes and stores (if specified) the command. Then you can invoke the undo and redo APIs which ends up calling the command object's UnExecute
and Execute
APIs. The current command location in the command chain and its execution and unexecution is maintained by the processor as you'd expect. A practical example now should clarify the usage of the command class and its processor more adequately.
The Database Commands
As I have mentioned before, to encapsulate an application command, you have to implement the abstract
class CGCommand
and override mainly the Execute
and UnExecute
APIs. In my database application, there are mainly three major operations: Add, Delete and Edit a row. Although I am working with single row addition, deletion and edition, the concept can easily be extended to account for multiple rows. Following are my 3 classes respectively for Addition, Deleting and Editing rows:
class CGDBAddRowCommand : public CGDataBaseCommand
{
public:
CGDBAddRowCommand(_RecordsetPtr pSet, const CDDXFields& fields, CDataGrid* pGrid);
BOOL Execute();
BOOL UnExecute();
CDataGrid* m_pGrid;
};
class CGDBDeleteRowCommand : public CGDBAddRowCommand
{
public:
CGDBDeleteRowCommand(_RecordsetPtr pSet, const CDDXFields& fields, CDataGrid* pGrid);
BOOL Execute();
BOOL UnExecute();
protected:
BOOL m_bFirstTimeExecution;
};
class CGDBEditRowCommand : public CGDataBaseCommand
{
public:
CGDBEditRowCommand(_RecordsetPtr pSet, const CDDXFields& fields, CDataGrid* pGrid);
BOOL Execute();
BOOL UnExecute();
CDDXFields m_undoDDXFields;
CDataGrid* m_pGrid;
};
All of the above classes are inherited from CGDataBaseCommand
class which is just a derivation from CGCommand
class with some extra attributes. Following is that class's declaration:
class CGDataBaseCommand : public CGCommand
{
public:
CGDataBaseCommand(_RecordsetPtr pSet, const CDDXFields& fields)
{
m_pSet = pSet;
m_DDXFields.RemoveAll();
m_DDXFields.Append(fields);
}
protected:
_RecordsetPtr m_pSet;
CDDXFields m_DDXFields;
};
If you look at the implementation of all the above mentioned classes, you'd see the CGDBAddRowCommand
class in its Execute
method uses the record set pointer m_pSet
to add a row in a data table. However, it saves the book mark of the newly created row in a conceptual map called m_mapBookMarksVsCommands
(which actually is implemented by CArray
of MFC). Then, when the UnExecute
method is invoked, it retrieves the book mark of the row from that map (where the key for retrieving the bookmark is the command object itself) and deletes the row which corresponds to the bookmark. There is a small tricky part where a function UpdateBookMarks
is called upon after we call UnExecute
and then Execute
to add the recently deleted row again (i.e., redo after undo in short). What the UpdateBookMarks
function does is, it finds the previously saved bookmark of the row (when Execute
was called initially) and replaces that by the newly created row bookmark (when Execute
is called again after an UnExecute
). We need to do this, because the initial bookmark would be nothing but a dangling pointer after undoing (UnExecute
) happened. Ok, enough with Adding Row. Let's move on to deleting row.
CGDBDeleteRowCommand
class is nothing but the reverse version of the CGDBAddRowCommand
class and its implementation is nothing more than doing the reverse of the latter which would be evident if you simply browse through the source code for this class.
Lastly, the CGDBEditRowCommand
class edits a row with the member recordset pointer in its Execute
method and at the same time saves the previous info of the row so that it can undo the edition. All it has to do is edit the row to these saved values when UnExecute
is invoked. This class also saves the row book mark inside Execute
so that it can come back to the exact row when the undoing is to be executed.
My real intention in this article is to give a feel of how a command can be encapsulated for undo and redo-ing purposes and although the operations that I implemented are pretty trivial, the intelligent reader, when it comes to database apps can use the recordset (or in case of C# and other advanced languages, use something similar but more likable than the recordset) for much advanced purposes.
Using the Code
Here is an example of the declaration and getting a reference of the command processor (a useful function to get the singleton instance of the command processor):
CGCommandProcessor& AppGetCommandProcessor()
{
static CGCommandProcessor g_commandProcessor;
return g_commandProcessor;
}
An example of submitting, executing and storing of a command:
if(bAddData)
{
AppGetCommandProcessor().Submit( new CGDBAddRowCommand
(m_pSet, m_DDXFields, m_pGrid) );
}
else
{
AppGetCommandProcessor().Submit( new CGDBEditRowCommand
(m_pSet, m_DDXFields, m_pGrid) );
}
Points of Interest
After deleting the last row, the recordset pointer may point to EOF, so you have to make an extra call to MovePrevious -
a record set pointer API to point to a valid row. This application wasn't tested with a fully empty data table, so beware of a possible crash. If such a crash really occurs, I hope somebody would be kind enough to put appropriate checking in the BOOL CGDBAddRowCommand::UnExecute()
method and notify me about the appropriate solution. Thanks in advance.
Acknowledgements
My acknowledgements go to Mr. Ariful Huq (Enosis Solutions) for sharing some of his project details with me which gave me the idea to implement the command pattern concept in the database realm.
I heartily thank Kirill Panov (http://www.codeproject.com/script/Membership/View.aspx?mid=30110) for his classy implementation of CDataGrid
and the relevant app (http://www.codeproject.com/KB/miscctrl/datagrid.aspx) which made it very easy for me to apply the command pattern concept in a database application.
History
- Article uploaded: 8th Feb, 2011