Tuesday, November 20, 2012

A Slight Improvement for the GroupDataModel


In the BB10 Cascades API the GroupDataModel is a very handy class that provides for the sorting and grouping of List data items by one, or more, properties. Along with its siblings that also extend the abstract base class DataModel it helps programmers implement the Model-View-Controller pattern. It also creates the concept of Headers, created out of the content of the grouping properties, and items the actual data elements provided to the data model. This is also the source of one of its major failings. To understand you will need some background.

In Cascades the List user interface object provides a view into a set of data managed by a DataModel. The C++ API allows full flexibility in creating user interface object to represent each individual type of data item the model may hold. The UI created is determined by the type property returned by the data item. This flexibility comes at the usual cost of programming complexity. The QML API is also very flexible, and the UI created for each item is also determined by the type property but returned by the DataModel not by the data item. Since the GroupDataModel is adding the concept of grouping and headers it will only return one of two values for the type property: header or item. These are all reasonable design decisions and don't really limit the C++ programmer. But what of the programmer who wants to leverage the rapid user interface design provided by Cascades? With a little C++ programming we can provide the same flexibility in QML.

We start by extending the GroupDataModel overriding the virtual function itemType(), I've called this class TypedGroupDataModel:

TypedGroupDataModel.hpp:
/*
 * TypedGroupDataModel.hpp
 *
 *  Created on: 2012-11-19
 *      Author: Richard
 */

#ifndef TYPEDGROUPDATAMODEL_HPP_
#define TYPEDGROUPDATAMODEL_HPP_

#include 

namespace bb {
namespace test {

class TypedGroupDataModel: public bb::cascades::GroupDataModel {
public:
 TypedGroupDataModel();
 virtual ~TypedGroupDataModel();

 virtual QString itemType(const QVariantList &indexPath);
};

} /* namespace test */
} /* namespace bb */
#endif /* TYPEDGROUPDATAMODEL_HPP_ */


TypedGroupDataModel.cpp:
/*
 * TypedGroupDataModel.cpp
 *
 *  Created on: 2012-11-19
 *      Author: Richard
 */

#include "TypedGroupDataModel.hpp"

namespace bb {
namespace test {

TypedGroupDataModel::TypedGroupDataModel() : GroupDataModel() {
}

TypedGroupDataModel::~TypedGroupDataModel() {
}

QString TypedGroupDataModel::itemType(const QVariantList &indexPath) {
 // Get the type GroupDataModel thinks it is
 QString type = GroupDataModel::itemType(indexPath);

 // If it isn't a header then see if the item has an idea what it is
 if (type != GroupDataModel::Header) {
  QVariant dataItem = data(indexPath);
  if (dataItem.isValid()) {
   if (dataItem.type() == QVariant::Map) {
    QVariantMap map = dataItem.toMap();
    if (map.contains("type")) {
     return map["type"].toString();
    }
   } else {
    QObject* object = dataItem.value<QObject*>();
    dataItem = object->property("type");
    if (dataItem.isValid()) {
     return dataItem.toString();
    }
   }
  }
 }

 // if all else fails return the type GroupDataModel gave us.
 return type;
}
} /* namespace test */
} /* namespace bb */


We also have to make sure that QML is aware of our new data model, so by registering TypedGroupDataModel before we load the QML document:

    // register the TypedGroupDataModel C++ type to be visible in QML
    qmlRegisterType<TypedGroupDataModel>("test.lib", 1, 0, "TypedGroupDataModel");

    // create scene document from main.qml asset
    // set parent to created document to ensure it
    // exists for the whole application lifetime
    QmlDocument *qml = QmlDocument::create("asset:///main.qml").parent(this);


Now we can use the TypedGroupDataModel in our QML:

// List with a context menu project template
import bb.cascades 1.0
import test.lib 1.0

Page {
    content: Container {
        layout: DockLayout {
        }
        ListView {
            id: listView
            objectName: "listView"
            horizontalAlignment: HorizontalAlignment.Center
            dataModel: TypedGroupDataModel {
                id: myListModel
                sortingKeys: [
                    "call"
                ]
                grouping: ItemGrouping.ByFullValue
            }
            
            property int activeItem: -1

            listItemComponents: [
                // define delegates for different item types here
                ListItemComponent {
                    type: "item"

                    // Use a standard list item to display the data in the model
                    // that doesn't override the 'item' type.

                    StandardListItem {
                        title: "Item"
                        //imageSource: ListItemData.image
                        description: ListItemData.data
                    }
                },
                ListItemComponent {
                    type: "One"
                    Container {
                        layout: StackLayout {
                            orientation: LayoutOrientation.LeftToRight
                        }
                        Label {
                            text: ListItemData.msg_type
                        }
                        Label {
                            text: ListItemData.status
                        }
                    }
                },
                ListItemComponent {
                    type: "Two"
                    Container {
                        layout: StackLayout {
                            orientation: LayoutOrientation.LeftToRight
                        }
                        Label {
                            text: ListItemData.objectName
                        }
                        Label {
                            text: ListItemData.comment
                        }
                    }
                },
                ListItemComponent {
                    type: "Three"
                    Container {
                        layout: StackLayout {
                            orientation: LayoutOrientation.LeftToRight
                        }
                        Label {
                            text: ListItemData.comment
                        }
                    }
                }
            ]
        }
    }
}

5 comments:

  1. Can we override insert & insertList & remove in the same way?

    ReplyDelete
    Replies
    1. An interesting questions. From a C++ theoretical point of view the short answer is no. itemType is a virtual function allowing run-time binding. This allows code written without any knowledge of our TypedGroupDataModel to use the overriding version of the function. The other three methods (insert, insertList and remove) are not virtual functions so this is not possible.

      You could re-define those functions in TypedGroupDataModel which would hide the original functions from any code that included the TypedGroupDataModel header file. I don't see what use case you could make out of that though.

      Delete
  2. Thank you.

    I'd like to improve the filterable group data model from examples (and also add second filter):
    https://github.com/jamespaulmuir/CascadesTextFilterableListViewSample/blob/master/src/FilterableGroupDataModel.cpp

    Current implementation has an issue: if mFilterText (value you search for) is not empty and you add/remove/update an item, you'll miss this change in case of mFilterText resetting.
    I'm looking for a simpler way to correct handling of add/remove/update items between changes of mFilterText value.

    ReplyDelete
    Replies
    1. I will try to give that some thought. Not familiar with FilterableListView sample. If I come up with anything I'll post it here.

      Delete