In the .NET XAML world, you have the ICommand, the CompositeCommand and the DelegateCommand. You use these commands to in a declarative way bind them as properties to XAML components like menu items and buttons. You can find an excellent book on this titled Prism 5.0 for WPF.
The ICommand defines two things: a canExecute property and an execute() method. The CompositeCommand allows you to combine multiple commands together, the DelegateCommand makes it possible to pass two delegates (functors or lambda’s); one for the canExecute evaluation and one for the execute() method.
The idea here is that you want to make it possible to put said commands in a ViewModel and then data bind them to your View (so in QML that’s with Q_INVOKABLE and Q_PROPERTY). Meaning that the action of the component in the view results in execute() being called, and the component in the view being enabled or not is bound to the canExecute bool property.
In QML that of course corresponds to a ViewModel.cpp for a View.qml. Meanwhile you also want to make it possible to in a declarative way use certain commands in the View.qml without involving the ViewModel.cpp.
So I tried making exactly that. I’ve placed it on github in a project I plan to use more often to collect MVVM techniques I come up with. And in this article I’ll explain how and what. I’ll stick to the header files and the QML file.
We start with defining a AbstractCommand interface. This is very much like .NET’s ICommand, of course:
#include <QObject>
class AbstractCommand : public QObject {
Q_OBJECT
Q_PROPERTY(bool canExecute READ canExecute NOTIFY canExecuteChanged)
public:
AbstractCommand(QObject *parent = 0):QObject(parent){}
Q_INVOKABLE virtual void execute() = 0;
virtual bool canExecute() const = 0;
signals:
void canExecuteChanged(bool canExecute);
};
We will also make a command that is very easy to use in QML, the EmitCommand:
#include <MVVM/Commands/AbstractCommand.h>
class EmitCommand : public AbstractCommand
{
Q_OBJECT
Q_PROPERTY(bool canExecute READ canExecute WRITE setCanExecute NOTIFY privateCanExecuteChanged)
public:
EmitCommand(QObject *parent=0):AbstractCommand(parent){}
void execute() Q_DECL_OVERRIDE;
bool canExecute() const Q_DECL_OVERRIDE;
public slots:
void setCanExecute(bool canExecute);
signals:
void executes();
void privateCanExecuteChanged();
private:
bool canExe = false;
};
We make a command that allows us to combine multiple commands together as one. This is the equivalent of .NET’s CompositeCommand, here you have our own:
#include <QSharedPointer>
#include <QQmlListProperty>
#include <MVVM/Commands/AbstractCommand.h>
#include <MVVM/Commands/ListCommand.h>
class CompositeCommand : public AbstractCommand {
Q_OBJECT
Q_PROPERTY(QQmlListProperty<AbstractCommand> commands READ commands NOTIFY commandsChanged )
Q_CLASSINFO("DefaultProperty", "commands")
public:
CompositeCommand(QObject *parent = 0):AbstractCommand (parent) {}
CompositeCommand(QList<QSharedPointer<AbstractCommand> > cmds, QObject *parent=0);
~CompositeCommand();
void execute() Q_DECL_OVERRIDE;
bool canExecute() const Q_DECL_OVERRIDE;
void remove(const QSharedPointer<AbstractCommand> &cmd);
void add(const QSharedPointer<AbstractCommand> &cmd);
void add(AbstractCommand *cmd);
void clearCommands();
QQmlListProperty<AbstractCommand> commands();
signals:
void commandsChanged();
private slots:
void onCanExecuteChanged(bool canExecute);
private:
QList<QSharedPointer<AbstractCommand> > cmds;
static void appendCommand(QQmlListProperty<AbstractCommand> *lst, AbstractCommand *cmd);
static AbstractCommand* command(QQmlListProperty<AbstractCommand> *lst, int idx);
static void clearCommands(QQmlListProperty<AbstractCommand> *lst);
static int commandCount(QQmlListProperty<AbstractCommand> *lst);
};
We also make a command that looks a lot like ListElement in QML’s ListModel:
#include <MVVM/Commands/AbstractCommand.h>
class ListCommand : public AbstractCommand
{
Q_OBJECT
Q_PROPERTY(AbstractCommand *command READ command WRITE setCommand NOTIFY commandChanged)
Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
public:
ListCommand(QObject *parent = 0):AbstractCommand(parent){}
void execute() Q_DECL_OVERRIDE;
bool canExecute() const Q_DECL_OVERRIDE;
AbstractCommand* command() const;
void setCommand(AbstractCommand *newCommand);
void setCommand(const QSharedPointer<AbstractCommand> &newCommand);
QString text() const;
void setText(const QString &newValue);
signals:
void commandChanged();
void textChanged();
private:
QSharedPointer<AbstractCommand> cmd;
QString txt;
};
Let’s now also make the equivalent for QML’s ListModel, CommandListModel:
#include <QObject>
#include <QQmlListProperty>
#include <MVVM/Commands/ListCommand.h>
class CommandListModel:public QObject {
Q_OBJECT
Q_PROPERTY(QQmlListProperty<ListCommand> commands READ commands NOTIFY commandsChanged )
Q_CLASSINFO("DefaultProperty", "commands")
public:
CommandListModel(QObject *parent = 0):QObject(parent){}
void clearCommands();
int commandCount() const;
QQmlListProperty<ListCommand> commands();
void appendCommand(ListCommand *command);
ListCommand* command(int idx) const;
signals:
void commandsChanged();
private:
static void appendCommand(QQmlListProperty<ListCommand> *lst, ListCommand *cmd);
static ListCommand* command(QQmlListProperty<ListCommand> *lst, int idx);
static void clearCommands(QQmlListProperty<ListCommand> *lst);
static int commandCount(QQmlListProperty<ListCommand> *lst);
QList<ListCommand* > cmds;
};
Okay, let’s now put all this together in a simple example QML:
import QtQuick 2.3
import QtQuick.Window 2.0
import QtQuick.Controls 1.2
import be.codeminded.mvvm 1.0
import Example 1.0 as A
Window {
width: 360
height: 360
visible: true
ListView {
id: listView
anchors.fill: parent
delegate: Item {
height: 20
width: listView.width
MouseArea {
anchors.fill: parent
onClicked: if (modelData.canExecute) modelData.execute()
}
Text {
anchors.fill: parent
text: modelData.text
color: modelData.canExecute ? "black" : "grey"
}
}
model: comsModel.commands
property bool combineCanExecute: false
CommandListModel {
id: comsModel
ListCommand {
text: "C++ Lambda command"
command: A.LambdaCommand
}
ListCommand {
text: "Enable combined"
command: EmitCommand {
onExecutes: { console.warn( "Hello1");
listView.combineCanExecute=true; }
canExecute: true
}
}
ListCommand {
text: "Disable combined"
command: EmitCommand {
onExecutes: { console.warn( "Hello2");
listView.combineCanExecute=false; }
canExecute: true
}
}
ListCommand {
text: "Combined emit commands"
command: CompositeCommand {
EmitCommand {
onExecutes: console.warn( "Emit command 1");
canExecute: listView.combineCanExecute
}
EmitCommand {
onExecutes: console.warn( "Emit command 2");
canExecute: listView.combineCanExecute
}
}
}
}
}
}
I made a task-bug for this on Qt, here.