The missing link in BB10/Cascades
Today I’d like to talk a bit about a basic concept of software design: layers. When you design your application (or framework), you should try to define layers of concerns, like storage, business logic and UI. That’s no rocket science and is taught at schools and universities for a couple for years now already. However, when it comes to real software projects, this basic rule is often ignored, which leads to tightly coupled code, that is difficult to understand and to maintain.
If you managed to apply proper layering in your design the next trap is to get the communication between the layers wrong. The basic rule is that only layers on an upper level are allowed to access layers on a lower level, but never the other way around. So for example the UI is allowed to access functionality from the business logic and from the storage, but the storage code should never make assumptions about the UI. Having a clean top-to-down access policy allows to replace upper layers easily without modifications to the lower layers. An use case for that is unit testing of storage or business logic, where you replace the UI with unit tests.
So what does it have to do with BB10/Cascades? For those of you who don’t know Cascades yet, it is the new UI toolkit for the BlackBerry10 platform. It is based on Qt and QML, but uses its own rendering stack instead of QtQuick. While QtQuick only allows you to describe the UI in QML, Cascades provides both, a C++ API to create the UI imperatively and QML to describe it declaratively.
While having a C++ API available looks good at the first glance, it opens a can of worms at the second glance. Using the C++ API tempts the developer to mix business logic code with UI code. That has been the common way of doing it for the last 10 years when developing with QtWidgets (e.g. reimplementing QDialog, create the UI in the constructor and implement business logic in member methods) and various other toolkits. But time goes on and with QML we now have a way to describe the UI in a declarative way and separate it from the business logic through a well defined interface: properties, signals and slots.
The idea is that you implement all of your business logic in C++ and publish the data and functionality through properties, signals and slots. While properties provide access to scalar or list data, signals notify the UI about state changes in the business logic layer and slots (and ‘invokable’ methods) allow the UI to trigger actions inside the business logic layer.
Cascades provides a rich set of properties, signals and slots for its controls already, so you can simply bind properties of type ‘string’ or ‘int’ against the bb::cascades::Label’s “text” property to visualize them and you can provide lists of data (e.g. list of country names) through a property of type ‘bb::cascades::DataModel’, which can be bound against bb::cascades::ListView’s “dataModel” property to show them in the ListView.
However one missing link in this scheme is the bb::cascades::DropDown, which is the Cascades version of QComboBox. Normally you use it like this:
DropDown {
title: qsTr("Countries")
Option {
text: qsTr("Afghanistan")
value: "AFG"
}
Option {
text: qsTr("Albania")
value: "ALB"
}
...
}
This might work for a small number of Options, but for a complete country list, you want to read the data from a file and fill the DropDown dynamically. The logical step would be to load the country data into a DataModel, publish the DataModel as property and use the model as source for the Options of the DropDown. However with the current API (as provided by the Gold SDK), it is not possible to use a bb::cascades::DataModel as source for the DropDown. The current way is to fill the DropDown with Option objects from within C++ code, which is, as we learned before, really bad design…
So during the Christmas holidays I sat down and implemented a solution for this problem, say Hello to DropDownModelAdaptor! (Yes, I know, the name is kind of ugly… suggestions for a better name are welcomed ;)).
Using the DropDownModelAdaptor is straight forward:
- Copy the DropDownModelAdaptor.hpp/cpp into the src/folder of your Momentics project
- Call ‘qmlRegisterType(“bb.cascades.ext”, 1, 0, “DropDownModelAdaptor”);’ inside your main() function
- Use it in the QML file as shown below:
import bb.cascades.ext 1.0
...
DropDown {
id: countryDropDown
attachedObjects: [
DropDownModelAdaptor {
dropDown: countryDropDown
dataModel: _countryModel
Option {
text: OptionData.countryName
value: OptionData.isoCode
}
}
]
}
The DropDownModelAdaptor is a non-visual object, so you create it as attached object of the DropDown. You have to specify the DropDown and the DataModel it should work on, in this case we use a bb::cascades::GroupDataModel, which contains one QVariantMap entry for each country. The keys of the QVariantMaps are ‘countryName’ and ‘isoCode’. The model is exported as context property to QML under the name ‘_countryModel’.
Additionally to the ‘dropDown’ and ‘dataModel’ property you specify an Option delegate, which is similar to the ListItemComponent for a ListView. It is basically a template Option that is used to create the actual instances inside the DropDown. The ‘OptionData’ property is injected by the DropDownModelAdaptor and provides access to the corresponding data from the DataModel.
The DropDownModelAdaptor does not only fill the DropDown with Option objects initially, it also listens to updates from the DataModel (e.g. items added/removed/changed) and adapts the content of the Option objects.
The code of the DropDownModelAdaptor can be found at git://github.com/tokoe/cascades.git You can use this code without any restrictions, so feel free to copy it over into your project until Cascades provides something like that natively. You’ll also find a sample application under dropdownmodeladaptor/example, which shows the class in action.
I hope you like this extension 🙂
I’ll begin with that I totally agree with you, the drop down should be populated with an DataModel.
Anyway, this is actually quite easy to accomplish with a simple javascript function and a ComponentDefinition for an Option.
For a complete example, check: https://bitbucket.org/marioboikov/planning-poker/src/ea4e56cd417c89ff8ea93e78a208c96ac9f03cc2/assets/settings/Decks.qml?at=master
I still want to say that I agree, but I think my solution can be an option when you don’t want to get your hand dirty with C++ 🙂
Hej Mario,
the problem with the JavaScript approach is: 1) QML code should be as declarative as possible, so the lines of imperative JavaScript code should be reduced. QML is about declaring the UI, not about implementing business logic. 2) The JavaScript approach does not allow you to update the content of the DropDown whenever the model changes (e.g. entry is added/removed/changed). Of course you can implement this in JavaScript as well, but it leads to a huge amount of non-reusable code.
I can’t agree with your listed JavaScript problems:
1) JavaScript can be kept in separate *.js file. Here you are creating non-existing purpose for Qt Quick (QML + JavaScript). I can agree that QML itself is for UI but JavaScript is simply programming language.
2) JavaScript approach does not have this limitation. You can connect to signals in JavaScript as well (http://doc.qt.digia.com/qt/qmlevents.html). Maybe implementation that Mario pointed at has this problem but that does not mean that you can’t do the same in JavaScript what you did in C/C++.
Lastly about “reusable code”. If we speak about business logic that goes outside of Qt then you are right (e.g. if we want to reuse the same code in Android NDK or native iOS app) but as long as we stay in Qt realm JavaScript approach is equally reusable. The only problem with JavaScript approach that it is slower (at least in Qt 4.x where we don’t have V8 engine, do we have it in Qt 5?).
In any case, good job 😉