CMemento11

CMemento11 is an application to show the use of Memento and Command design patterns, to implement a undo/redo listView in Qt/C++/QML.

The data model is represented by two classes: CUndoableInventory and CListable

CUndoableInventory class represents the listView and contains a vector of objects extending CListable interface.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* @brief The CUndoableInventory class
*/
class CUndoableInventory
{
protected:
// list of CListable
vector< shared_ptr<CListable> > *inventory_;
vector< shared_ptr<CListable> > *duplicateInventory() const;
void swap(const CUndoableInventory& other);
public:
CUndoableInventory();
CUndoableInventory(const CUndoableInventory& other);
virtual ~CUndoableInventory();
CUndoableInventory & operator= (const CUndoableInventory& other);
// number of listables in the list
int getInventorySize() const;
// get the listable at index 'pos' (unchangeable)
const CListable* getListable(size_t pos) const;
// get the listable at index 'pos' (changeable)
CListable* getListablePtr(size_t pos);
// command list modifiers
void addListable(CListableFactory::listable_t listableType, const std::string& name);
bool insertListable(CListableFactory::listable_t listableType, const std::string& name, size_t pos);
bool removeListable(size_t pos);
bool clearAll();
// print to std::out
void printInventory() const;
// memento methods
CUndoableInventoryMemento* createMemento() const;
void reinstateMemento (CUndoableInventoryMemento* mem);
protected:
void addListablePtr(shared_ptr<CListable> listablePtr);
bool insertListablePtr(shared_ptr<CListable> listablePtr, size_t pos);
};

CListable is a pure virtual class representing an object contained in the listView.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Abstract class CListable
class CListable
{
public:
virtual std::string getTitle() const {
std::string title = getType();
title.append(" - ").append(getName());
return title;
}
virtual std::unique_ptr<CListable> clone() const = 0;
virtual std::string getName() const = 0;
virtual std::string getType() const = 0;
virtual std::string toString() const = 0;
};

This design decouples the CListable abstraction from its implementation (in this case: CBook, CCdRom, CDvd), so that the two can vary independently: it complies to the Bridge Design Pattern.

Undo/Redo: Command and Memento patterns.

Command pattern is an Object behavioral pattern that decouples sender and receiver by encapsulating a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and above all support undo-able operations.
It can also be thought as an object oriented equivalent of call-back method.

Without violating encapsulation the Memento pattern will capture and externalize an object’s internal state so that the object can be restored to this state later.
The Originator (the object to be saved) creates a snap-shot of itself as a Memento object, and passes that reference to the Caretaker object. The Caretaker object keeps the Memento until such a time as the Originator may want to revert to a previous state as recorded in the Memento object.

In my implementation the originator is represented by CUndoableInventory.
CUndoableInventory creates a snap-shot of itself as a CUndoableInventoryMemento object by invoking createMemento(), and can revert to a previous state by invoking reinstateMemento (CUndoableInventoryMemento* mem):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @brief The CUndoableInventoryMemento class
*/
class CUndoableInventoryMemento
{
private:
CUndoableInventory object_;
public:
CUndoableInventoryMemento(const CUndoableInventory& obj);
// want a snapshot of CUndoableInventory itself because of its many data members
CUndoableInventory snapshot() const;
};

The Caretaker is represented by CUndoableInventoryAction class (Command pattern, where the receiver is CUndoableInventory):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* @brief The CUndoableInventoryAction class
*/
class CUndoableInventoryAction
{
private:
CUndoableInventory* receiver_;
// this std::function will contain the reference to receiver's method and all the parameters
function<void(const CUndoableInventory&)> receiverAction_;
static std::vector<CUndoableInventoryAction*> commandList_;
static std::vector<CUndoableInventoryMemento*> mementoList_;
static int numCommands_;
static int maxCommands_;
public:
CUndoableInventoryAction(CUndoableInventory *newReceiver,
function<void(const CUndoableInventory&)> newReceiverAction);
virtual ~CUndoableInventoryAction();
virtual void execute();
static bool isUndoable();
static bool isRedoable();
static void undo();
static void redo();
static void clearAll();
};
#endif // CUNDOABLEINVENTORY_H

An instance of this class represents an undoable operation performed on the CUndoableInventory object:
it keeps a pointer to an instance of the receiver (CUndoableInventory) and the method to be performed on the receiver as a function<void(const CUndoableInventory&)> (the std::function encapsulates both the method of class CUndoableInventory and the parameters to be passed to it).

Moreover, the CUndoableInventoryAction class is the Caretaker, because it keeps two static lists:

  • 1) the list of actions performed;
  • 2) the list of the inner states of the undoable inventory (CUndoableInventoryMemento object).

The undo/redo functions have been implemented as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void CUndoableInventoryAction::undo()
{
if(numCommands_ == 0) {
std::cout << "There is nothing to undo at this point." << std::endl;
return;
}
commandList_[numCommands_ - 1]->receiver_->reinstateMemento(mementoList_[numCommands_ - 1]);
numCommands_--;
}
void CUndoableInventoryAction::redo()
{
if (numCommands_ > maxCommands_) {
std::cout << "There is nothing to redo at this point." << std::endl;
return;
}
CUndoableInventoryAction* commandRedo = commandList_[numCommands_];
commandRedo->receiverAction_(*(commandRedo->receiver_));
numCommands_++;
}

Undoable Operations

The class in charge of performing undoable operations on a CUndoableInventory object, is CUndoableInventoryManager:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class CUndoableInventoryManager
{
protected:
CUndoableInventory undoableInventory_;
public:
CUndoableInventoryManager();
virtual ~CUndoableInventoryManager();
protected:
void executeNonUndoableMethod(function<void(const CUndoableInventory&)> method);
void executeUndoableMethod(function<void(const CUndoableInventory&)> method);
public:
const CUndoableInventory& undoableInventory() const;
void setUndoableInventory(unique_ptr<CUndoableInventory> undoableInventory);
int getInventorySize() const;
const CListable* getInventoryListable(size_t pos) const;
CListable* getInventoryListablePtr(size_t pos);
bool isLastInventoryActionUndoable();
bool isLastInventoryActionRedoable();
void undoLastInventoryAction();
void redoLastInventoryAction();
void inventoryAddListable(CListableFactory::listable_t listableType, const std::string& name);
void inventoryInsertListable(CListableFactory::listable_t listableType, const std::string& name, size_t pos);
void inventoryRemoveListable(size_t pos);
void inventoryClear();
};

The magic happens inside executeUndoableMethod

1
2
3
4
5
void CUndoableInventoryManager::executeUndoableMethod(function<void(const CUndoableInventory&)> method)
{
CUndoableInventoryAction* myInventoryAction = new CUndoableInventoryAction(&undoableInventory_, method);
myInventoryAction->execute();
}

which receives a function<void(const CUndoableInventory&)> as parameter, which encapsulates the method of CUndoableInventory to be invoked and the parameters.
This method creates an action (Command pattern) by creating an object of type CUndoableInventoryAction, and then invokes its execute method myInventoryAction->execute();.

In this way, any function willing to be undoable, will just have to wrap the CUndoableInventory method and the parameters, inside a std::function

For example, let’s have a look at inventoryAddListable(CListableFactory::listable_t listableType, const std::string& name):

1
2
3
4
5
void CUndoableInventoryManager::inventoryAddListable(CListableFactory::listable_t listableType, const string &name)
{
std::function<void(const CUndoableInventory&)> inventoryMethod = std::bind(&CUndoableInventory::addListable, &undoableInventory_, listableType, name);
executeUndoableMethod(inventoryMethod);
}

C++ <—–> QML

In order to “bind” the QML undoable listView with the data model represented by CUndoableInventoryManager object, I set up a MVC architecture.

  • Model: CUndoableInventoryManager
  • View: QML undoable listView
  • Controller: an object of type CInventoryModel extending QAbstractListModel

The class CInventoryModel receives commands from Javascript functions used in QML undoable listView, and then it performs modifications on the data model and the listView itself.
For example, let’s have a look at the addListable method:

1
2
3
4
5
6
7
8
9
10
11
/**
* C++
*/
void CInventoryModel::addListable(int listableType, const QString& name)
{
CListableFactory::listable_t listableFactoryType = static_cast<CListableFactory::listable_t>(listableType);
beginInsertRows(QModelIndex(), myInventoryManager_->getInventorySize(), myInventoryManager_->getInventorySize());
myInventoryManager_->inventoryAddListable(listableFactoryType, name.toStdString());
endInsertRows();
emit modelModified();
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* QML/Javascript
*/
function execAddListable() {
var listableType = listableTypeComboBox.currentIndex + 1;
var listableName = listableNameTextField.text;
inventoryModel.addListable(listableType, listableName);
inventoryList.positionViewAtEnd();
}
...
Button {
x: 10
y: 100
id: cmdButtonAdd
Layout.alignment: Qt.AlignLeft
anchors.topMargin: 10
text: qsTr("Add Item")
style: ButtonStyle {
background: Rectangle {
implicitWidth: 120
implicitHeight: 40
opacity: enabled ? 1 : 0.3
border.color: cmdButtonAdd.down ? selectedColor : normalColor
border.width: 1
radius: 2
}
label: Text {
text: cmdButtonAdd.text
opacity: enabled ? 1.0 : 0.3
color: cmdButtonAdd.down ? selectedColor : normalColor
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
}
action: Action {
enabled: true
onTriggered: {
execAddListable();
console.log("Add detected!")
}
}
}
...

In this way I was able to implement all of the methods to add, insert, delete items on the list, and the undo/redo actions.