In the previous article I explained the basics of a simple Model-View-Presenter architecture in QtQuick. Now it's time to explain the implementation details. I will start with the Presenter QML component which is the base component of all presenters:
FocusScope {
id: root
property Item view
property QtObject model
property var modelToViewBindings: []
property var viewToModelBindings: []
Component.onCompleted: {
root.view.anchors.fill = root;
root.view.focus = true;
for ( i in root.modelToViewBindings ) {
name = root.modelToViewBindings[ i ];
dynamicBinding.createObject( root, {
target: root.view, property: name, source: root.model, sourceProperty: name
} );
}
for ( var i in root.viewToModelBindings ) {
var name = root.viewToModelBindings[ i ];
dynamicBinding.createObject( root, {
target: root.model, property: name, source: root.view, sourceProperty: name
} );
}
}
Component {
id: dynamicBinding
Binding {
id: binding
property var source
property string sourceProperty
value: binding.source[ binding.sourceProperty ]
}
}
}
As you can see, the presenter is a FocusScope, so that it can be used as a top-level visual component of a window or can be embedded in some more complex layout. The presenter contains a view property which holds a reference to the embedded view component and a model property which holds a reference to the model object. The Component.onCompleted function makes sure that the view fills the entire presenter and that it has keyboard focus enabled.
The remaining code is related to binding properties between the model and the view and vice versa in a simplified way. The modelToViewBindings property contains an array of names of properties that should be bound from the model to the view. The first loop in the Component.onCompleted function creates a Binding object for each property binding. The target of the binding is a property of the view. The value of that property is bound to the corresponding source property in the model. Note that JavaScript makes it possible to access a property of an object with a given name using the [] operator, because an object is essentially an associative array. The binding from the view to the model works in exactly the same way. As demonstrated in the example in the previous article, it's even possible to create bi-directional bindings.
The view and model objects are created in the actual presenter component which inherits Presenter. As I explained in the previous article, we want to be able to switch both the view and the model as easily as possible. In order to do that, I implemented a simple factory, which is basically a shared library implemented in JavaScript:
.pragma library
var config = {
modules: {},
urls: { views: "views/", models: "models/" }
};
function createObject( type, name, parent ) {
var module = config.modules[ type ];
if ( module !== undefined ) {
var qml = "import " + module + "; " + name + " {}";
return Qt.createQmlObject( qml, parent );
} else {
var url = config.urls[ type ];
if ( url !== undefined ) {
var component = Qt.createComponent( url + name + ".qml", parent );
var object = component.createObject( parent );
if ( object === null )
console.log( "Could not create " + name + ":\n" + component.errorString() );
return object;
} else {
console.log( "Factory is not configured for " + type );
return null;
}
}
}
function createView( name, parent ) {
return createObject( "views", name, parent );
}
function createModel( name, parent ) {
return createObject( "models", name, parent );
}
The config variable specifies the names of C++ modules and/or URLs of directories that contain QML files with the implementation of views and models. By default, the models can be simple mock-ups written in QML, which makes it possible to run the whole application directly using the qmlscene tool without writing a single line of code in C++.
The createObject() function first looks for a registered C++ module of the given type. If present, it creates a simple component declaration in QML and calls Qt.createQmlObject() to create the instance of the component. When there is no C++ module registered, the function tries to load the component from the QML file in the given location. The createView() and createModel() functions are wrappers that can be used for simplicity.
By changing the default configuration of the factory at run-time, you can not only switch between mock-up models and real models implemented in C++, but also, for example, to use different views in mobile and desktop versions of the application. For example, the Component.onCompleted function of your top-level QML component could contain the following code to override the default configuration:
if ( typeof models !== 'undefined' )
Factory.config.modules.models = models;
The C++ application can register the classes which implement the models in a module and pass the name of that module to the QML engine as a property of the root context, like this:
qmlEngine->rootContext()->setContextProperty( "models", "MyApp.Models 1.0" );
Unfortunately, it's not possible to pass variables from C++ code directly to shared JavaScript libraries which use the ".pragma library" directive (according to this thread, this is done this way on purpose). The method shown above can be used to work around this limitation; just keep in mind that the factory needs to be configured before any objects are actually created.