One of the advantages of using QtQuick is that it's possible to fully separate the user interface from application logic. Not only there are separate classes or components, but even the programming language used in both layers is completely different. The user interface can be written (or designed) using QML, a declarative and highly expressive language created strictly for the purpose of developing modern UI. The application logic on the other hand can be written in C++, which is powerful, abstract and yet exceptionally fast. Such approach has many advantages over writing the UI directly in C++, because with all it's strengths, it's not well suited for UI development. One of these advantages is that you can have two separate teams in the project, and the UI team doesn't need to know C++, and the application logic team doesn't need to know QML.
But wait, eventually these two layers have to be integrated somehow, right? There are several possibilities of doing such integration, and at first they seem quite complex, which defeats the purpose of simplifying the development process. The Qt tutorials and documentation don't give a lot of help. They only mention a general rule of thumb that you should never directly access or manipulate QML objects from C++ code, although the API certainly makes this possible. The whole idea is that the QML code should create and use objects implemented in C++ to perform various operations. But then if a QML button directly invokes some method implemented in C++, this has nothing to do with decoupling the application logic from the UI.
The solution to this problem is well known and it can also be successfully applied to QML and C++. The idea is to introduce another layer of abstraction between the application logic (the model) and the UI (the view). There are several variants of such architecture, the most common ones are Model-View-Presenter and Model-View-Controller. The differences are quite subtle and I'm not going to dig into the details. They mostly have to do with the lifetime and control flow between these layers. Generally MVC is commonly used in web applications, and MVP is often used in desktop applications, but this doesn't always have to be true. My goal wasn't to strictly follow any particular approach, but to find one that is best suited to solve the given problem, which is what design patterns are all about.
My implementation of the Model-View-Presenter architecture in QtQuick is based on the following assumptions:
- A view is a visual QML component which defines the look of the application and has no logic (except for basic validation). It can be anything from a single ListView to a complex form with lots of different controls. The view should have some signals that correspond to user actions (for example indicating that a button was clicked). It also may have some properties that can be used for passing data to the view (for example to change the text of some label) and from the view (for example to retrieve text entered by the user). It doesn't require or directly use any components related to application logic, so a view can be created and tested independently from the other two layers.
- A model is a C++ class that implements some aspect of the application logic. It typically has a number of methods that perform some operations. It may also have signals, which are useful to indicate that an asynchronous operation has completed. Properties can be used to pass data to and from the model, for example parameters and results of an operation. The model doesn't know anything about any visual components of the application. It can be created independently from the other two layers, which is useful for unit testing, automation, etc.
- A presenter is what glues the view and model together. It's a visual component written in QML, but its only visible content is the view that's embedded in it. The presenter also creates the model object. Then it connects appropriate signals and slots from the view and the model and bind their properties. These connections and bindings can include additional logic, but it's best to design the interface of the view and model in such way that they can work directly together. In that case the presenter's only job is to create and set up these two objects and the overhead of having the third layer between the UI and the application logic is very small.
The MVP architecture makes it possible for the views and models to be created and tested independently, by two different teams, as long as their interfaces (signals, slots and properties) are designed in a compatible way. Another advantage of such approach is that it makes prototyping really easy. Designing a good user interface is a difficult craft and often the best solution is to quickly build and test different variants and prototypes. But the prototype often needs to demonstrate not only the appearance, but also the behavior of the application, so it's very useful to have some mock-up of application logic. This can be done by writing mock-up models which simply pretend to perform complex operations by returning fixed or random data. Such mock-up models can even be created in QML, which makes it possible to run and test the entire prototype without writing a single line of C++ code!
I will write more about writing mock-up models and switching between mock-ups and real models in the next article. For now let's assume that a model is a simple C++ class which inherits QObject and a view is a simple QML component which inherits Item or one of its sub-classes. The key and most interesting part of this architecture is obviously the presenter, so let's take a look at an example:
Presenter {
id: root
signal showMainWindow()
view: Factory.createView( "LoginView", root )
model: Factory.createModel( "LoginModel", root )
modelToViewBindings: [ "login", "error" ]
viewToModelBindings: [ "login", "password" ]
Connections {
target: root.view
onLoginClicked: { root.view.disableView(); root.model.beginLogin(); }
}
Connections {
target: root.model
onLoginSucceeded: { root.showMainWindow(); }
onLoginFailed: { root.view.enableView(); }
}
}
As you can see, this component inherits the Presenter class. I will show you the implementation of that class in the next article, but for now let's skip the technical details and take a look at how the actual presenter is implemented, in this case a simple login window.
The showMainWindow() signal is used to communicate to some higher level object (for example the application object) that it should display the main window of the application containing an appropriate presenter. In real life applications there will often be multiple views that can be switched or even displayed side by side, and multiple models that provide functionality for these views. The application will never interact with these views and models directly but rather will create appropriate presenters to handle them.
The view and model properties contain references to the view and model objects which are owned by the presenter. These objects are created using a helper Factory class, which makes it possible to dynamically switch between mock-up models implemented in QML and real models implemented in C++, or for example between views designed for mobile and desktop version of the same application. I will write more about the factory in the next article.
The modelToViewBindings and viewToModelBindings properties make it easy to bind properties of the same name and type between the model and the view. Again, I will present the underlying implementation in the next article, but generally the idea is to create a number of Binding components dynamically using a very simple syntax. Obviously in more complex cases it's possible to set up the bindings manually. The bindings can be unidirectional or bidirectional, like in case of the "login" property. For example, the model can remember and initialize the view with the last used login, and the view can pass the new login entered by the user back to the model.
The last part of the presenter handles signals from both the view and the model. In most cases, the signals from the model are passed directly to slots in the view and vice versa, but they can also be connected to a signal in the presenter itself or can contain more complex logic. In this simple example, when the login button is clicked, the view becomes inactive (with some visual hint like a busy indicator) and the model begins processing the login request asynchronously (for example by contacting a server or database). When login completes successfully, the showMainWindow() signal is emitted to the parent of the presenter, and in case of an error the view is activated again and some error message is displayed through the "error" property. This is obviously a very simplified example, but it represents what might happen in a typical modern application.
You can find the next articles of this series here.
- Log in to post comments