Using QAbstractItemModel with Cascades on BB10

With QML as abstraction layer, we nowadays have a great way to separate the business logic (C++) of our application from the graphical user interface (QtQuick/QtWidgets/Cascades) on top of it. The interface between the two layers is well defined by the meta-object API, which essentially boils down to signals and slots for notifications and invocations, and properties for data access.

To reuse the business logic objects with different UIs, the data types of the signal/slot parameters and the property types must be known by both sides, the business logic and the UI library. While the parameters for signals and slots are normally simple types like ‘int’, ‘bool’, ‘QString’ or ‘QDateTime’, the property types tend to be more complex. There are scalar values with simple types of course and for non-scalar values a ‘QVariantList’ or ‘QVariantMap’ can be used, however to visualize lists of data in a list view, the UI libraries normally expect some specific model interface.

QtWidgets and QtQuick both use the QAbstractItemModel interface to make data of arbitrary formats accessible to the views. QAbstractItemModel is aimed to work with list, table and tree structures, and to work fast on big data sets. All these requirements reflect in an extensive and not so easy to use API, starting from hierarchic model indexes, over persistent model indexes, down to item data roles. Cascades on the other side defines its own model interface (bb::cascades::DataModel), which is aimed to work with lists and tree structures only and can’t be optimized for large data sets as good as QAbstractItemModel. However its API is much smaller and easier to use.

The problem occurs when you want to implement a business logic class that provides a list of objects to be displayed in a list view. Depending on the UI to use, you’d either have to make your property of type QAbstractItemModel or of type bb::cascades::DataModel.
Needless to say, this breaks our nice separation and the business logic would now
depend on the UI… that’s bad :( But we have to provide the data to the list view in some way, so how?

QAbstractItemModel is declared in QtCore, that means every Qt-based application will know about it. Furthermore, QAbstractItemModel is more generic than bb::cascades::DataModel API-wise, so all the functionality of bb::cascades::DataModel can be modeled with QAbstractItemModel as well.

For this reason I suggest to only use QAbstractItemModel as type for model properties in C++ business logic objects, which you want to use accross multiple platforms!

All right, but how can bb::cascades::ListView access the data from the QAbstractItemModel then, if it only supports bb::cascades::DataModel as input?
Say hello to AbstractItemModel

AbstractItemModel

AbstractItemModel is a class that implements the Adapter design pattern.
It inherits the bb::cascades::DataModel interface, so it can be plugged into a b::cascades::ListView as data model, and as adaptee it uses an arbitrary QAbstractItemModel object. All the mapping between the different index systems (QVariantList indexPath vs. QModelIndex) and the forwarding of change notification signals (items added/removed/updated) is handled by AbstractItemModel, so it can be used out-of-the-box like this:

import bb.cascades 1.0
import com.kdab.components 1.0

Page {
    attachedObjects: [
        AbstractItemModel {
            id: abstractItemModel
            sourceModel: _fileSystemModel
        }
    ]

    ListView {
        dataModel: abstractItemModel

        listItemComponents: [
            ListItemComponent {
                type: ""
                StandardListItem {
                    title: ListItemData.display
                }
            }
        ]
    }
}

At first you have to import the module where the AbstractItemModel is defined in, here we used ‘com.kdab.components’. Then you declare a new instance of AbstractItemModel as attached object of some component and give it an ‘id’. As ‘sourceModel’ you set the QAbstractItemModel based model, here we use a QFileSystemModel, which has been set as context property under the name ‘_fileSystemModel’. Inside the ListView we can use the AbstractItemModel as input for the ‘dataModel’ property now.

The usage of a custom ListItemComponent in this example brings us to a specific in AbstractItemModel: While bb::cascades::DataModel::data(indexPath) returns all data of the item at this indexPath in one go, QAbstractItemModel::data(index, role) returns only the data at this index for the specific role. AbstractItemModel adapts this behavior by calling QAbstractItemModel::data() for every available role and returns all the data as QVariantMap, where the keys are the role names. For this reason we use ‘ListItemData.display’ as title of the custom StandardListItem in the example above, to display the QAbstractItemModel’s data for the Qt::DisplayRole.

bb::cascades::DataModel associates a so called item type with each item in the model. The item type is an arbitrary string value, which should be known by the model and the ListItemComponent. While the standard models use ‘item’ and ‘header’, your custom model could use additional item types like ‘mysubheader’. Unfortunately QAbstractItemModel does not provide such an association by default, however if you know that your QAbstractItemModel will be used together with an AbstractItemModel in a Cascades application, you can simply define an additional role in your QAbstractItemModel (e.g. ItemTypeRole with the role name ‘itemType’) that returns a string value and then specify it as content of the AbstractItemModel’s ‘itemTypeRole’ property:

AbstractItemModel {
    id: abstractItemModel
        sourceModel: _myCustomModel
        itemTypeRole: "itemType"
    }

With this change the AbstractItemModel returns the value of the model’s ItemTypeRole as item type.

Another specific in QAbstractItemModel is its view-driven lazy loading mechanism. Because real-world models can contain thousands or even millions of items, loading all items into memory on start-up is a bad idea. Instead the model loads only an initial subset and the view asks the model to load more in case the view needs to display further items (e.g. after the user expanded the branch of a tree). To query more data, the view uses QAbstractItemModel::fetchMore(index) to trigger the loading of all data underneath index. Since some models expect the call of fetchMore() to actually populate any data, AbstractItemModel provides the slot fetchMore(indexPath), which calls the fetchMore() method of the associated QAbstractItemModel with the mapped index. You would use the fetchMore() slot inside your UI code whenever the user enters a new level in the hierarchy. In our file browser example with the QFileSystemModel, the code would look like that:

    ListView {
        id: listView

        dataModel: abstractItemModel
        onTriggered: {
            listView.rootIndexPath = indexPath
            abstractItemModel.fetchMore(indexPath)
        }

        ...
    }

Whenever the user clicks on a directory entry, we change the ‘rootIndexPath’ property of the ListView to the clicked indexPath, so that the ListView now shows the content of the selected directory. Additionally we tell the AbstractItemModel that it needs to populate the items for this indexPath now (if not done already).

The Code

The code of the AbstractItemModel (and the complete filebrowser example) can be found at
https://github.com/tokoe/cascades/tree/master/abstractitemmodel
It is available under a BSD-like license, so you can freely use it in your own project,
and modify or redistribute it as long as you keep the original copyright notice.

Share on FacebookTweet about this on TwitterShare on Google+

One thought on “Using QAbstractItemModel with Cascades on BB10

Leave a Reply

Your email address will not be published. Required fields are marked *


one × 5 =