Dependency injection in C++ class registered with QML_SINGLETON
-
I managed to find a way to enable the dependency injection pattern:
// contactsModel.h (SEE the `MessengerService*` property) class ContactsModel: public QAbstractListModel { Q_OBJECT QML_ELEMENT Q_PROPERTY(MessengerService* messengerService READ messengerService WRITE setMessengerService NOTIFY messengerServiceChanged REQUIRED) public: enum ContactRoles { DisplayName = Qt::UserRole + 1, Bio, ProfilePictureUrl, Email, Tuned, Status }; ContactsModel(QObject *parent = nullptr); int rowCount(const QModelIndex &parent = QModelIndex()) const; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; MessengerService* messengerService() const; void setMessengerService(MessengerService* _messengerService); signals: void messengerServiceChanged(); public slots: // fetches contacts from api void fetchContacts(); // update whether or not we are tuned into this person void updateTuneContact(const QString &email, const bool tuneIn = false); private slots: void finishedFetchContacts(); void finishedUpdateTuneContact(); protected: QHash<int, QByteArray> roleNames() const; private: QList<Contact> m_contacts; QNetworkAccessManager *m_net_access_manager; QNetworkReply *m_net_contacts_reply; QNetworkReply *m_net_update_tuned_contact; MessengerService* m_messenger_service; }; // messengerservice.h (This is what I want as a shared dependency between different models) // in charge of managing connection with messenger service // realtime & presence messages class MessengerService: public QObject { Q_OBJECT QML_ELEMENT QML_SINGLETON public: MessengerService() = default; signals: // filter for presence only messages // these would be pongs of presence heartbeats to other clients void incomingPresencePong(const IncomingPresencePong &message); void incomingPresenceQuery(const IncomingPresenceQuery &message); // all messages void messageReceived(const QString &message); public slots: void connect(const QString &email); // successfully connected void connected(); void disconnected(); void errorOccurred(QAbstractSocket::SocketError err); void sendTextMessage(const QString &message); void sendPresencePong(const OutgoingPresencePong &pong); void sendPresenceQuery(const OutgoingPresenceQuery &query); private slots: void textMessageReceived(const QString &message); private: QWebSocket m_webSocket; }; } // Now in our QML ContactsModel { id: contactsModel messengerService: MessengerService }
Now, the compiler will require the ContactsModel initialization to include an instance of MessengerService. Since MessengerService is registered as a QML_SINGLETON, it can be passed in.
We can then also pass the same MessengerService singleton to other models that are initialized in QML.
@SGaist any thoughts on this approach of mine? My experience with golang backend design patterns and also flutter bloc pattern inspired me to do it this way.
Do you know if there is an enterprise standard patterns for Qt/QML apps? I am shocked that Qt doesn't document how complex apps should be written (where there are many "view models" needing to access each other.
-
@talksik I had a similar reaction to the advice to move away from context properties to singletons. However, it was pointed out to me that it wasn't necessary for the C++ object actually to be a singleton.
The advice was to use
qmlRegisterSingletonInstance
, whereby an object is registered as the instance that is accessed from QML. The object just needs to be aQObject
and provide a QML-compatible API, but how it is created, what constructor arguments it has, etc., is up to you. The "singleton" aspect simply refers to the syntax with which it is accessed from QML. -
@Bob64 Thank you Bob for the suggestion!
I just want to clarify something. In the qml code I provided I am initializing
ContactsModel {}
. However, what is not shown is that this model is initialized is a "sub-qml" file calledDashboard.qml
. My main qml file looks like this:ApplicationWindow { id: root visible: true ... // Container element for rotating screen Rectangle { id: main ... StackView { id: stackView anchors.fill: parent initialItem: loginView Component { id: loginView Login {} } Component { id: dashboardView Dashboard {} } } } }
Note the dashboardView component. I have it this way because I do not want ContactsModel to be a top-level model. I want it to have the lifecycle of the
dashboardView.
From what you said, I imagine you are suggesting something similar to the following?
// main.cpp MessengerService* messengerService = new MessengerService(); ContactsModel* contactsModel = new ContactsModel(messengerService); qmlRegisterSingletonInstance(contactsModel, ...);
What I am trying to achieve is the following: whenever we want to use certain models in any qml file, it should receive the
MessengerService
instance on initialization. Right now I am doing a hacky way by making messengerService a Q_PROPERTY that gets set in the QML where I initialize oneContactsModel {}
.Thank you! Please let me know if that makes sense. I would appreciate any tips :)
-
@talksik Something is not exactly clear from your explanations.
Can you explain your architecture ? You seem to have several models that are each in charge of a different aspect of your MessengerService, is that correct ?
If so, it seems that you should basically create all your models passing the service to them and then set them as
qmlRegisterSingletonInstance
as suggested by @Bob64. I would no see any advantages creating the MessengerService in QML to pass it to your model unless you plan to be able to switch MessengerService objects on the fly. This would require for your models to have a suitable API to do the change. -
@SGaist Thank you so much for the response!
I considered that approach as @Bob64 suggested, but the thing is: I initialize the model within a sub-qml file deep within the QML tree. I also do NOT do the
qmlRegisterSingletonInstance
approach because I want the model to have the same lifecycle as the QML component that it is initialized in. Even more, this approach allow multiple of the same model being created within the QML in different places/pages/views.The
qmlRegisterSingletonInstance
registers a singleton, which is not what I would like in my architecture. I may do that if that's the only way, however, I wanted to see if there was a way to do it my way so that each model is isolated to the QML component (and sub-components) that uses it.You are right that I do not want to switch MessengerService objects on the fly. I am stuck in this dilemma. Please see my response to @Bob64's suggestion here for clarity: https://forum.qt.io/post/792823
-
@KH-219Design I found your great details of using MVVM with Qt/QML! I agree with it completely (https://forum.qt.io/topic/127714/qt-qml-c-hybrid-application-best-practices/10) Can you please help me in the dilemma I present in this thread?
What if we have a ViewModel that we define in C++, but that view model and other view models want to share a data object that is a singleton. How would you pass that singleton. I instantiate my view models that were defined in C++ within my QML deep within the QML tree. I see that you don't have this problem because you send the view models to QML using context properties. Please read the comments before this to understand what I am saying.
I would really appreciate this as I am shocked that I still haven't found an elegant way around this :)
-
@talksik Have you found an elegant way around this? I'm struggling to find a pretty way of sharing single data source object among multiple models in QML.
This object communicates with
QSerialPort
so I guess it can't be anything but a singleton. Currently I'm usingqmlRegisterSingletonInstance
for all of my models which I create inmain.cpp
passing the data source object pointer to their constructors. It looks sort of messy if you ask me.So, any ideas? Thank you!