Sorted data models

You can use a  GroupDataModel to store data and sort it in a particular order. You can specify sorting keys for the data model, and items in the model are sorted based on the values of those keys. You can add simple item types to a GroupDataModel (such as strings or integers), or you can create and add your own item types by extending QObject. When you want to display the sorted data in your app, you can create a ListView and associate the GroupDataModel with it.

The GroupDataModel class sorts your data automatically, in either ascending order or descending order, when you add the data to the model. This type of model also adds headers automatically to group the data, based on the sorting keys that you specify. This behavior can make this type of model ideal for information that you want to display in a certain order. For example, you can use a GroupDataModel for a list of contacts, and then sort the list based on the contact's first name or last name (or both).

Sorting keys

The sorting keys in a  GroupDataModel determine how the data is sorted. Each key is a QString and represents the name of a property that you want to use to sort the data in the model. For items in the model to be sorted properly, each item must contain a property with the same name as the key. For example, if you specify a key of lastName, each item that you add to the model must include a property called lastName. Then, the value of the lastName property in each item is used to sort the items.

You can specify more than one key to use for sorting. The sortingKeys property in GroupDataModel represents the sorting keys for the model, and accepts a list of QString objects. If the items in your model represent contacts, you might want to sort them based on last name, then first name. To do this, you can specify the list of sorting keys as [lastName, firstName]. The items are first compared and sorted based on the lastName property. If the values of lastName are the same, then the items are compared and sorted based on the firstName property.

Data items

The items in a  GroupDataModel are instances of either QVariantMap or QObject*. A QVariantMap represents a set of key-value pairs, making it a good choice when you want to store simple data (such as text or numbers) in a GroupDataModel. You can use keys in the QVariantMap that correspond to the keys of the GroupDataModel, which allows the QVariantMap items to be sorted according to these keys.

The key in a QVariantMap needs to be a QString, but the value that's associated with the key doesn't. You can use a variety of QVariant types for your values, including StringDouble, and Date. For a complete list, see the API reference for GroupDataModel.

For example, consider a GroupDataModel that represents a list of cities. Each item in the list has properties for the city name (cityName) and the corresponding country name (countryName). Here's how to populate QVariantMap objects with data for three cities. You can then add these QVariantMap objects to a GroupDataModel and sort them using the keys cityName and countryName.

// Create QVariantMap objects for each city
QVariantMap firstCity;
QVariantMap secondCity;
QVariantMap thirdCity;
 
// Populate the QVariantMap objects
firstCity["cityName"] = "Waterloo"; firstCity["countryName"] = "Canada";
secondCity["cityName"] = "Rome"; secondCity["countryName"] = "Italy";
thirdCity["cityName"] = "Barcelona"; thirdCity["countryName"] = "Spain";

You can also use a QObject pointer as an item in a GroupDataModel. This can be a good approach if you want to create your own class to represent the data in your model. In the example of a GroupDataModel that represents a list of cities, you could create a City class that inherits from QObject and then add pointers to City objects to the model.

When you use a QObject to represent data in your model, only the properties that you define in the class (that is, those that you define using Q_PROPERTY) are visible to the GroupDataModel. This means that you can sort the data based only on values of these properties, and you can display only these properties in a ListView. Member variables and functions aren't visible, but can still be useful internally in your class implementation.

Data hierarchy

When you add data items to a  GroupDataModel, all items are added at the same hierarchical level in the model. You can't specify that certain items are children of other items. This is an important difference between a GroupDataModel and other data models in Cascades, such as an  XmlDataModel. In an XmlDataModel, you can arrange data in a hierarchy with multiple levels, and you can precisely define the relationships between the items. For example, you can create an item called "Fruit" and specify child items of "Bananas", "Apples", and "Oranges". This relationship is preserved in the XmlDataModel and is reflected when you display the data using a  ListView.

In a GroupDataModel, there are only two levels of items. Header items, which are created automatically, are on the first level of the model. Data items that you add to the model are on the second level. Header items are created based on the values of the properties that you specify as sorting keys. For example, if your data model represents a list of contacts and you choose to sort the items based on the lastName property, headers are created automatically based on the first character of each lastName property value (that is, all last names that start with A are placed under the "A" header, all last names that start with B are placed under the "B" header, and so on). You can choose to group items in different ways by using the grouping property.

Several functions in GroupDataModel use index paths to specify items in the model. An index path is a list of integers that uniquely identifies a particular item in the model. Functions like GroupDataModel::data() (used to retrieve the data for an item in the model) and GroupDataModel::childCount() (used to determine how many children an item has in the model) use index paths as arguments to specify the appropriate item. To learn more about index paths, see Index paths.

In a GroupDataModel, header items are always on the first level of the model, and so these items have index paths that contain a single integer. Data items that you add to the model are always on the second level, and so they have index paths that contain two integers.

Creating a GroupDataModel in QML

You can create a  GroupDataModel in QML and associate it with a  ListView by using the ListView::dataModel property. This property specifies the data model that a ListView should use. In the simplest case, all you need to do is specify an empty GroupDataModel as the value of this property, and you can populate the data elsewhere in your app.

Here's how to create a ListView that uses a GroupDataModel. After the ListView is created, data items are added to the model using  DataModel::insert(). Each item includes a single property, exampleProperty, with a value. This model doesn't include any sorting keys, and so the items are displayed in the ListView in last-in, first-out (LIFO) order (the item that was added last is displayed first in the list).

import bb.cascades 1.0
 
Page {
    content: ListView {
        dataModel: GroupDataModel {}
         
        onCreationCompleted: {
            for (var a = 0; a < 20; a++) {
                dataModel.insert(
                    {"exampleProperty" : a}
                )
            }
        }
    } // end of ListView
} // end of Page

This approach works well for simple data models when you don't really care about the order of the data. For more complicated data models, or for models that you want to reuse in multiple ListView objects, you can add the GroupDataModel as an attached object and reference it as the value of the dataModel property.

Here's how to create a ListView that uses a GroupDataModel attached object. The model sorts data items according to the name property. After the ListView is created, several items are added to the model.

import bb.cascades 1.0
 
Page {
    content: Container {
        attachedObjects: [
            // Add a data model that sorts based on the name property
            GroupDataModel {
                id: groupDataModel
                sortingKeys: ["name"]
            }
        ]
         
        ListView {
            // Specify the data model from the attached objects list
            dataModel: groupDataModel
             
            // After the list is created, add the data items
            onCreationCompleted: {
                groupDataModel.insert( {"name" : "Charles"} );
                groupDataModel.insert( {"name" : "Bob"} );
                groupDataModel.insert( {"name" : "Karen"} );
                groupDataModel.insert( {"name" : "Samantha"} );
            }
        }
    } // end of Container
} // end of Page

In some cases, you might want more control of how items are grouped and displayed in your list. Consider a GroupDataModel that represents a list of cities. You might want to display the cities grouped by country, with each country name as a heading in the list. Here's how to do that using multiple sorting keys and the grouping property of GroupDataModel.

import bb.cascades 1.0
 
Page {
    content: Container {        
        attachedObjects: [
            GroupDataModel {
                id: groupDataModel
 
                // Sort the data items based first on country name, and
                // then by city name (if the country names are equal)
                sortingKeys: ["countryName", "cityName"]
 
                // Specify that headers should reflect the full value
                // of the sorting key property, instead of just the
                // first letter of the property
                grouping: ItemGrouping.ByFullValue
            }
        ]
         
        ListView {
            dataModel: groupDataModel
             
            // After the list is created, add the data items
            onCreationCompleted: {
                groupDataModel.insert(
                    {"countryName" : "Italy", "cityName" : "Rome"} );
                groupDataModel.insert(
                    {"countryName" : "Spain", "cityName" : "Barcelona"} );
                groupDataModel.insert(
                    {"countryName" : "Canada", "cityName" : "Waterloo"} );
                groupDataModel.insert(
                    {"countryName" : "Canada", "cityName" : "Vancouver"} );
                groupDataModel.insert(
                    {"countryName" : "Italy", "cityName" : "Milan"} );
                groupDataModel.insert(
                    {"countryName" : "Canada", "cityName" : "Toronto"} );
                groupDataModel.insert(
                    {"countryName" : "Spain", "cityName" : "Madrid"} );
            }
        } // end of ListView
    } // end of Container
} // end of Page

The code sample above results in the sorted list that you see to the right. Each country appears as a header item in the list, and the country names are sorted alphabetically.

Within each country group, cities are sorted alphabetically according to the cityName property. Because the countryName values are the same in each country group, the GroupDataModel sorts based on the next key in the list of sorting keys (namely, cityName).

If you don't want any headers to appear in your list, you can set the grouping property to ItemGrouping::None. The items in the data model are still sorted (based on the sorting keys that you specify), but the model won't create headers to group the items.

A list that with seven cities grouped by country.

Item types

In the example above, notice that the countryName properties appear as header items and the cityName properties appear as list items under the headers. The  ListView class creates this arrangement automatically by selecting one of the properties of each item and creating a  StandardListItem to display in the list for that property. In simple cases where each item in your model has only one or two properties, this approach can work well; indeed, it worked nicely in that example by arranging the country names as headers and the city names as list items.

However, in more complicated cases where each item has multiple properties, the ListView might not choose the right property to display. When the data items in your model include more than one property, you should define the appearance of list items yourself. In QML, you do this by creating a  ListItemComponent for each type of item in your list and then specifying which properties you want to display. To learn more about defining the appearance of list items, see List view.

When you use this approach, you also need to tell the ListView the type of each item in the data model. For some data models, such as  XmlDataModel, the type is included in the source of the data model. You can see an example of this in the list view documentation. In that document, an .xml file is used as the source of the data model, and the item types are included in the file itself as XML tags.

Here's a section of the items.xml file that's used as an example in that document. The .xml file defines two types for items, "header" and "listItem", as XML tags. When you use XML tags to define item types, yourListView interprets the types automatically from the XML and lets you define the appearance for them using ListItemComponent objects.

<header title="Fruits" subtitle="Generally sweet"/>
<listItem title="Oranges" subtitle="Rich in vitamin C" status="Eaten"
             checked="1"/>
<listItem title="Apples" subtitle="One a day keeps the doctor away"
             status="Not eaten" checked="0"/>
...

For other data models, such as GroupDataModel, item types aren't defined and interpreted automatically; you need to explicitly state the type of each item in your data model. Fortunately, ListView includes a JavaScript function called itemType() that you can override to provide this information. The itemType() function returns the type of the item with the specified index path. Because a GroupDataModel includes only two levels of hierarchy for its data, you could implement itemType() to return either "header" for header items, or "listItem" for list items. Here's how you might do this by using the index path of the item:

function itemType(data, indexPath) {
    if (indexPath.length == 1) {
        // If the index path contains a single integer, the item
        // is a "header" type item
        return "header";
    } else {
        // If the index path contains more than one integer, the
        // item is a "listItem" type item
        return "listItem";
    }
}

You can then create ListItemComponent objects to define the appearance of each type in your list.

Creating a GroupDataModel in C++

You can create a  GroupDataModel in C++ by simply creating a GroupDataModel object and specifying the sorting keys that you want the model to use. Then, you can create QVariantMap or QObject* objects and insert them into the model. You can also associate the data model with a  ListView by calling ListView::setDataModel().

In many cases, it can be a good approach to populate a GroupDataModel in C++ instead of QML. By using C++, you can access data from various sources more easily, such as from a SQLite database or JSON data structure. You also have the option to create your own class to represent your data, and then add objects of that class (as QObject pointers) to the model. This gives you more control over how you organize and store your data.

An easy way to specify the sorting keys is to use a QStringList, and pass this list (along with the keys to use) to the constructor of GroupDataModel. Here's how to create the same data model of cities, sorted by country, that was presented in QML in the previous section. Each data item is a QVariantMap that contains the properties and values for each item.

// Create the root Page and a top-level Container
Page *root = new Page;
Container *topContainer = new Container;
 
// Create the data model and specify the sorting keys to use
GroupDataModel *model = new GroupDataModel(QStringList() << "countryName"
                                           << "cityName");
 
// Specify the type of grouping to use for the headers in the list
model->setGrouping(ItemGrouping::ByFullValue);
 
// Create a QVariantMap and populate it with data for each item. When the data
// for an item has been populated, add the item to the data model and reuse
// the same QVariantMap for the next item.
QVariantMap map;
map["countryName"] = "Italy"; map["cityName"] = "Rome"; model->insert(map);
map["countryName"] = "Spain"; map["cityName"] = "Barcelona"; model->insert(map);
map["countryName"] = "Canada"; map["cityName"] = "Waterloo"; model->insert(map);
map["countryName"] = "Canada"; map["cityName"] = "Vancouver"; model->insert(map);
map["countryName"] = "Italy"; map["cityName"] = "Milan"; model->insert(map);
map["countryName"] = "Canada"; map["cityName"] = "Toronto"; model->insert(map);
map["countryName"] = "Spain"; map["cityName"] = "Madrid"; model->insert(map);
 
// Create a ListView and associate the data model with it
ListView *listView = new ListView();
listView->setDataModel(model);
 
// Add the ListView to the top-level Container and display the content
topContainer->add(listView);
root->setContent(topContainer);
app->setScene(root);

Creating a GroupDataModel in both QML and C++

You can create a list, associate it with a data model, and populate the model with data either entirely in QML, or entirely in C++. However, it's common to create your list in QML and then populate the data model for the list in C++. This approach lets you separate the UI component (the list, represented by a  ListView) from the business logic component (the data model, represented by a  GroupDataModel). You can change the implementation details of one component without affecting the other, which can be extremely important for complex or large-scale apps that you develop.

Here's how you could create a list and a data model that, when used together, represent a list of employees. Each employee has a first name, a last name, and an employee number, and this information is encapsulated in a custom Employee class. Instances of this class are created to represent each employee and are added to the data model as data items. The list is created and associated with a data model in QML, and the model is populated with data in C++.

The following example combines many of the data model concepts with concepts from the list view documentation to create a more comprehensive and complete example.

// If you want to use Employee objects in QML, you need to register it as a
// type. You can include this C++ code in the constructor for your app.
 
qmlRegisterType<Employee>("myLibrary", 1, 0, "Employee");
// main.qml
 
import bb.cascades 1.0
 
// Import the custom library that contains the Employee type, so you can use
// this type in QML. For example, you might need to use the Employee type in
// QML to respond to the selection of an employee in the list.
import myLibrary 1.0
 
Page {
    content: Container {        
        attachedObjects: [
            // Add the data model as an attached object. Make sure
            // to specify a value for the objectName property,
            // which is used to access the model from C++.
            GroupDataModel {
                id: groupDataModel
                objectName: "groupDataModel"
 
                // Sort the data first by last name, then by first
                // name
                sortingKeys: ["lastName", "firstName"]
            }
        ]
         
        ListView {
            id: listView
             
            // Associate the GroupDataModel from the attached objects
            // list with this ListView
            dataModel: groupDataModel
             
            listItemComponents: [
                // Use a ListItemComponent to define the appearance of
                // list items (that is, those with a type of "listItem")
                ListItemComponent {
                    type: "listItem"
                     
                    // Each list item is represented by a StandardListItem
                    // whose text fields are populated with data from the
                    // item
                    StandardListItem {
                        title: ListItemData.lastName + ", " +
                                   ListItemData.firstName
                        description: ListItemData.employeeNumber
                    }
                }        
            ]
             
            // Override the itemType() function to return the proper type
            // for each item in the list. Because a GroupDataModel has only
            // two levels, use the index path to determine whether the item
            // is a header item or a list item.
            function itemType(data, indexPath) {
                if (indexPath.length == 1) {
                    // If the index path contains a single integer, the item
                    // is a "header" type item
                    return "header";
                } else {
                    // If the index path contains more than one integer, the
                    // item is a "listItem" type item
                    return "listItem";
                }
            }              
        } // end of ListView
    } // end of Container
} // end of Page
// Employee.h
// The implementation of these functions in Employee.cpp just sets or returns
// the corresponding variables mFirstName, mLastName, and mEmployeeNumber.
 
// Remember that to be added to a GroupDataModel, your custom class must
// inherit from QObject
 
#include <QObject>
 
using namespace bb::cascades;
 
class Employee : public QObject {
    Q_OBJECT
 
    Q_PROPERTY(QString firstName READ firstName WRITE setFirstName FINAL)
    Q_PROPERTY(QString lastName READ lastName WRITE setLastName FINAL)
    Q_PROPERTY(int employeeNumber READ employeeNumber WRITE setEmployeeNumber
               FINAL)
 
public:
    Employee(QObject *parent = 0);
    Employee(QString argLastName, QString argFirstName, int argEmployeeNumber);
 
    QString firstName() const;
    QString lastName() const;
    int employeeNumber() const;
 
    Q_SLOT void setFirstName(QString newName);
    Q_SLOT void setLastName(QString newName);
    Q_SLOT void setEmployeeNumber(int newNumber);
 
private:
    QString mFirstName;
    QString mLastName;
    int mEmployeeNumber;
};
// This is the C++ code that populates the data model. You could include this code
// in the constructor for your app, in an initialization function, or another
// location in your app.
 
// Create the GroupDataModel by locating the corresponding QML component
GroupDataModel *pModel = root->findChild<GroupDataModel*>("groupDataModel");
 
// Insert the data as instances of the Employee class
pModel->insert(new Employee("Barichak", "Westlee", 12596375));
pModel->insert(new Employee("Lambier", "Jamie", 53621479));
pModel->insert(new Employee("Chepesky", "Mike", 65523696));
pModel->insert(new Employee("Marshall", "Denise", 77553269));
pModel->insert(new Employee("Taylor", "Matthew", 51236712));
pModel->insert(new Employee("Tiegs", "Mark", 13112965));
pModel->insert(new Employee("Tetzel", "Karla", 99214732));
pModel->insert(new Employee("Dundas", "Ian", 64329841));
pModel->insert(new Employee("Cacciacarro", "Marco", 54575213));

Last modified: 2013-12-21

comments powered by Disqus