Skip to content

Memento Design pattern in C++ 11 to add the undo/redo features to a class (QML ListView example)

License

Notifications You must be signed in to change notification settings

evildead/CMemento11

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CMemento11

It 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.

/**
 * @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.

// 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):

/**
 * @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):

/**
 * @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;
    1. the list of the inner states of the undoable inventory (CUndoableInventoryMemento object).

The undo/redo functions have been implemented as follows:

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:

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

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):

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:

/**
 * 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();
}
/**
 * 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.

About

Memento Design pattern in C++ 11 to add the undo/redo features to a class (QML ListView example)

Resources

License

Stars

Watchers

Forks

Packages

No packages published