ListView
is a fundamental Cascades control because it gives you an efficient way of displaying to the user hierarchical data on a screen where the real estate is relatively limited. List views are therefore one of the most flexible controls available in the Cascades framework and provide you lots of options for specifying how your data will be rendered as list items. Another important aspect of list views is their ability to clearly separate your data from its visual appearance by using the model-view-controller pattern. As illustrated in Figure 6-1, the ListView
plays the role of a controller, which handles—among other things—user interactions; the DataModel
represents your data; and, finally, a ListItemComponent
is a QML template defining visual controls for rendering your data. You can also define multiple ListItemComponent
s for different data item types (I will tell more about types in the “Data Models” section. For the moment, simply keep in mind that a data model can define a type, which is used by the ListView to render a data item.).
ListView
as your main UI control for data-centric apps.-
How to use list views in your own applications to display hierarchical data to the user.
-
Create navigation-based apps using a
ListView
as the main UI control. -
Use the standard data models provided by Cascades to display data in a
ListView
. -
Implement your own “custom” data models for data types or sources not supported out of the box by Cascades.
List Views
ListView
aggregates a data model and its visual representation. This section will mostly focus on the visual aspects and touch interactions of the ListView
, and the next section will give you a more detailed description of data models. Listing 6-1 illustrates a minimal ListView
control added to a Page
control.import bb.cascades 1.2
Page {
ListView {
id: listview
dataModel: XmlDataModel {
id: people
source: "people.xml"
}
}
}
ListView
’s dataModel
property defines the data to be displayed in the ListView
(in the example shown in Listing 6-1, we are using an XmlDataModel
, which loads its data from an XML
file; Listing 6-2 gives you the sample XML
content).<people>
<category value="Actors">
<person name="John Wayne"/>
<person name="Kirk Douglas"/>
<person name="Clint Eastwood"/>
<person name="Spencer Tracy"/>
<person name="Lee Van Cleef"/>
</category>
<category value="US Presidents">
<person name="John F. Kennedy"/>
<person name="Bill Clinton"/>
<person name="George Bush"/>
<person name="Barack Obama"/>
</category>
</people>
XML
document; however, the hierarchical structure of the XML
document has been “flattened,” which is not what we want (the Actors and US Presidents categories should, in fact, appear as header items, with actors and presidents displayed under the corresponding headers).ListItemComponent Definition
ListItemComponent
is a kind of factory, which contains a QML component definition. The actual component is a visual control (or simply a visual) responsible for rendering data items of a given type. In other words, a ListView
uses a ListItemComponent
to create a visual representation of its data items. The following properties are used in the component definition:
-
QString ListItemComponent::type()
: The data item type that this component definition should be used for. -
QDeclarativeComponent* ListItemComponent::content()
: The QML component definition used for creating the visuals responsible for rendering the data item whose type isListItemComponent::type()
. (Note that aQDeclarativeComponent
is very similar in nature to aComponentDefinition
, which is used to define QML components for dynamic creation, see Chapter 5).content
is alsoListItemComponent
’s default property (see Chapter 2 for an explanation of default properties).
ListItemComponent
in practice.import bb.cascades 1.2
Page {
ListView {
id: listview
dataModel: XmlDataModel {
id: people
source: "people.xml"
}
listItemComponents:[
ListItemComponent {
type: "category"
Container{
id: container
Label {
id: myLabel
text: container.ListItem.data.value // equivalent to ListItemData.value
}
}
}
] // ListItemComponents
} // ListView
}
ListItemComponent
to the ListView
’s listItemComponents
property (in a moment, you will see that you can define multiple ListItemComponents
corresponding to different data item types in the data model). At runtime, the ListView
uses the component definition to instantiate the visuals for rendering its data items (therefore, in the previous code, the visuals created at runtime are the Container
, which is the root visual, and the Label
). The ListView
also dynamically attaches the ListItem
attached property to the root visual, which is the Container
. (An attached property is a mechanism for dynamically adding a property, which was not part of a control’s initial definition.) Finally, the Label
uses the Container
’s ListItem
property to access the data item.ListView
to select the correct ListItemComponent
component definition? In the “Data Models” section, you will see that the data model provides the type information. For example, in the specific case of an XmlDataModel
, the returned type corresponds to the name of the tag in the XML
document. Therefore, the XmlDataModel
will return the category
type for the corresponding XML
tag shown in Listing 6-2.ListItem
property also defines the following properties, which you can use in your component definition (note that you have already used the data property in Listing 6-3 to access the data item):
-
ListItem.initialized
: States whether the root visual is initialized or not. Theinitialized
property istrue
when the initialization of the root visual is finished (in other words, all properties have been updated to reflect the current item). Otherwise, the property isfalse
. For performance reasons,ListViews
« recycle »ListItem
s. The data model should therefore only be updated when theListItem
is initialized. For example, if aCheckBox
is used for updating a corresponding item status in the data model, theonCheckChanged()
slot should check theListItem
’sinitialized
property before propagating the change to the data model. If theListItem
is not initialized, you could potentially corrupt the data model’s state. -
ListItem.data
: The data item returned byDataModel::data()
. Common values areQString
,QVariantMap
, andQObject*
. Note that in QML you can use themapname.keyname
andobjectname.propertyname
syntax to access individual data items exposed by a map or an object, respectively. Also, as mentioned previously, theListItem
property is only defined on the root visual. You will therefore have to use the<rootId>.ListItem.data
notation to access thedata
property from any visual located further down the tree. As a convenience, theListView
also provides theListItemData
alias, which is a context property accessible from anywhere in the visual tree (equivalently, instead of setting theLabel
’stext
property usingcontainer.ListItem.data.value
, you could have usedListItemData.value
). -
ListItem.indexPath
: The index path identifying this item in the data model. -
ListItem.view
: TheListView
in which this item is visible. -
ListItem.component
: TheListItemComponent
from which this visual has been created. -
ListItem.active
:true
if the visual is active (in other words, the user is pressing on it). -
ListItem.selected
:true
if this visual is selected. An item is typically selected if the user intends to perform an action on the item or access details for the item (the “Detecting Selection” section will give you more information about handling selection).
ListItemComponent
definition do not share the same document context as main.qml
, where the ListView
has been declared. This means that only the properties defined in the ListItem
attached property are visible to the root visual at runtime. In other words, you can’t access by id as you would usually do for any of the controls declared in main.qml
. You will see how to circumvent this problem in the “Context Actions” section.Header Definition
Header
control instead of a Label
in order to render items of type category
. A Header
control has title
and subtitle
properties that you can set using the ListItemData
property (see Listing 6-4; note that only the Header
’s title
is set).import bb.cascades 1.2
Page {
ListView {
id: listview
dataModel: XmlDataModel {
id: people
source: "people.xml"
}
listItemComponents:[
ListItemComponent {
type: "category"
Header {
title: ListItemData.value
}
}
] // listItemComponents
} // ListView
} // Page
StandardListItem Definition
XML
document shown in Listing 6-2 to include additional information such as the person’s date of birth, a picture, and name of spouse (see Listing 6-5).<people>
<category value="Actors">
<person name="John Wayne" born="May 26, 1907" spouse="Pilar Pallete" pic="wayne.jpg"/>
<person name="Kirk Douglas" born="December 9, 1916" spouse="Anne Buydens"
pic="douglas.jpg"/>
<person name="Clint Eastwood" born="May 31, 1930" spouse="Dina Eastwood"
pic="eastwood.jpg"/>
<person name="Spencer Tracy" born="April 5, 1900" spouse="Louise Treadwell"
pic="tracy.jpg"/>
<person name="Lee Van Cleef" born="January 9, 1925" spouse="Barbara Havelone"
pic="vancleef.jpg"/>
</category>
<category value="US Presidents">
<person name="John F. Kennedy" born="May 29, 1917" spouse="Jacqueline Kennedy"
pic="kennedy.jpg"/>
<person name="Bill Clinton" born="August 19, 1946" spouse="Hillary Rodham Clinton"
pic="clinton.jpg"/>
<person name="George Bush" born="July 6, 1946" spouse="Laura Bush" pic="bush.jpg"/>
<person name="Barack Obama" born="August 4, 1961" spouse="Michelle Obama"
pic="obama.jpg"/>
</category>
</people>
XML
document given by Listing 6-5, the resulting UI will be similar to Figure 6-4, which is not what you want (only the person’s date of birth is displayed).
Header
items, you need a way to tell the ListView
how to render items of type person
. You can achieve this in several ways, but I will first show you how to use the StandardListItem
visual (see Listing 6-6).import bb.cascades 1.2
Page {
ListView {
id: listview
dataModel: XmlDataModel {
id: people
source: "people.xml"
}
onTriggered: {
}
listItemComponents:[
ListItemComponent {
type: "category"
Header {
title: ListItemData.value
}
},
ListItemComponent {
type: "person"
StandardListItem {
title: ListItemData.name
description: ListItemData.born
status: ListItemData.spouse
imageSource: "asset:///pics/"+ListItemData.pic
}
}
] // ListItemComponents
} // ListView
}
StandarListItem
is a control with a standard list of properties to be displayed in a ListView
. The properties are title
(displayed in bold text), description
, status
, and imageSource
(all properties are optional). For example, the code in Listing 6-6 uses a StandardListItem
control to render an item of person
type by using the corresponding XML
attributes provided by the data model. Figure 6-5 illustrates the resulting UI (note that the person’s picture is loaded from the application’s assets folder).
CustomListItem Definition
CustomListItem
visual. The CustomListItem
defines a highlight, a divider, and a user-specified control for rendering a data item. The highlight, which is defined by the highlightAppearance
property, determines what the list item looks like when it is selected. The divider, which is defined by the dividerVisible
property, determines if a divider should be shown in order to separate the list item from adjacent items. Finally, the content
property used for rendering the list item can be any Cascades control you decide to use (note that if you use a Container
, you will be able to aggregate several controls). To illustrate how to use a CustomListItem
in practice, Listing 6-7 shows you how to customize the list’s headers with a CustomListItem
.import bb.cascades 1.2
Page {
ListView {
id: listview
dataModel: XmlDataModel {
id: people
source: "people.xml"
}
listItemComponents:[
ListItemComponent {
type:"category"
CustomListItem {
dividerVisible: true
Label {
text: ListItemData.value
// Apply a text style to create a large, bold font with
// a specific color
textStyle {
base: SystemDefaults.TextStyles.BigText
fontWeight: FontWeight.Bold
color: Color.create ("#7a184a")
}
} // Label
} // CustomListItem
},
ListItemComponent {
type: "person"
StandardListItem {
title: ListItemData.name
description: ListItemData.born
status: ListItemData.spouse
imageSource: "asset:///pics/"+ListItemData.pic
}
}
] // listItemComponents
}
}
CustomListItem
illustrated in Listing 6-7 uses a Label
control to apply text styling to the header element (see Figure 6-6 for the resulting UI).
content
property of a ListItemComponent
. As a matter of fact, you can simply use any Cascades control to display the data item to the user. For example, in the case of a rich data model, you could add multiple Cascades controls—such as a CheckBox
, a Label
, and an ImageView
—to a Container
playing the role of the root visual (the main advantage of leveraging the stock Header
, StandardListItem
, and CustomListItem
controls is to provide a smooth Cascades look and feel across your applications).Detecting Selection
ListView
programming. However, you will also need to detect item selection so that your users can interact with the ListView
. The following topics will be discussed in this section:
-
Detecting the selected item when the user performs a single tap in the
ListView
. -
Using item selection to navigate from a master view to a details view.
-
Handling context actions when the user performs a long press on an item.
-
Handling multiselection by defining a
MultipleSelectActionItem
control. Multiselection enables the user to select multiple items before triggering a context action on the selected items.
Single Tap
ListView
by responding to the ListView
’s triggered()
signal:
-
triggered(QVariantList indexPath)
: Emitted when the user taps an item with the intention to execute some action associated with it. The signal will not be emitted when theListView
is in multiselection mode. TheindexPath
parameter identifies the tapped item.
triggered()
signal in practice: the ListView
’s onTriggered
slot uses the implicit indexPath
variable to select an item in the ListView
(note that the code clears any previous selections before selecting the current item). In QML, an index path is an array of integers. I will tell you more about index paths when we discuss data models. For the moment, you can simply consider that an index path identifies the tapped item. An index path is also a kind of pointer to the data node in the data model. In other words, you can use the index path to access the data node corresponding to the tapped item.ListView {
id: listview
dataModel: XmlDataModel {
id: people
source: "people.xml"
}
onTriggered: {
listview.clearSelection();
select(indexPath);
}
listItemComponents: [// ... code omitted]
}
ListView
emits the selectionChanged()
signal:
-
selectionChanged(QVariantList indexPath, bool selected)
: Emitted when the selection state has changed for an item (in other words, the item has been either selected or deselected). You can use theselected
parameter to determine if the item is selected (true
) or not (false
).
Referencing an Item in an Action
Page {
actions: [
ActionItem {
ActionBar.placement: ActionBarPlacement.OnBar
title: "Share"
onTriggered: {
if(listview.selected().length > 1){
var dataItem = listview.dataModel.data(listview.selected());
// share data item.
}
} // onTriggered
}
]
ListView {
id: listview
dataModel: XmlDataModel {
id: people
source: "people.xml"
}
onTriggered: {
listview.clearSelection();
toggleSelection(indexPath);
}
listItemComponents: [// ... code omitted]
} // ListView
} // Page
triggered()
signal to retrieve the data node corresponding to the current selected item in the ListView
. See Figure 6-7. Because we are only interested in person
type data items, the code checks whether the item’s index path size is bigger than 1 before accessing the data node. (The index path of the root node in the data hierarchy is an empty array; header
items, which correspond to the category
type, have an index path of size 1, and “leaf” items, which correspond to the person
type, have an index path of size 2.) Also, if you need to define multiple actions on an item, it is usually better to use context actions (see the following section for more information on context actions).
Navigating a Master-Details View
ListView
in a navigation pane (see Listing 6-10).import bb.cascades 1.2
NavigationPane {
id: nav
attachedObjects: [
ComponentDefinition {
id: itemPageDefinition
source: "PersonDetails.qml"
}
]
onPopTransitionEnded: {
page.destroy();
}
Page {
ListView {
id: listview
dataModel: XmlDataModel {
id: people
source: "people.xml"
}
onTriggered: {
if (indexPath.length > 1) {
var person = people.data(indexPath);
var personDetails = itemPageDefinition.createObject();
personDetails.person = person
nav.push(personDetails);
}
}
listItemComponents: [
// ...code omitted
]
} // ListView
}// Page
} // NavigationPane
NavigationPane
, you have managed to create a navigation-based application using a ListView
as the main UI element. The navigation from the ListView
to the details page is initiated by the ListView
’s triggered()
signal. The details page is defined by the PersonDetails
control, which I will explain shortly. Note that before pushing a new PersonDetails
page on the NavigationPane
’s stack, you need to initialize the PersonDetails
’s person
property with the selected data node (because the data node will be used by PersonDetails
to initialize its controls).import bb.cascades 1.0
Page {
property variant person;
Container {
verticalAlignment: VerticalAlignment.Center
horizontalAlignment: HorizontalAlignment.Center
topPadding: 50
ImageView {
horizontalAlignment: HorizontalAlignment.Center
imageSource: "asset:///pics/"+person.pic
preferredWidth: 400
preferredHeight: 400
}
Label {
textStyle.base: SystemDefaults.TextStyles.BigText
horizontalAlignment: HorizontalAlignment.Center
text: person.name
}
Label {
textStyle.base: SystemDefaults.TextStyles.SubtitleText
horizontalAlignment: HorizontalAlignment.Center
text: "date of birth: "+person.born
}
} // Container
} // Page
person
property corresponds to the selected node in the data model and is used to initialize the controls located on the page (the data node is a map and you can use its keys to retrieve the underlying data). To navigate back to the ListView
, the user can use the Back button on the action bar. Figure 6-8 illustrates the corresponding UI when the details view is shown.
Context Actions
ListItemComponent
definition. The actions will then appear in a context menu when the user performs a long press on an item (see Listing 6-12).ListItemComponent {
type: "person"
StandardListItem {
id: standardListItem
title: ListItemData.name
description: ListItemData.born
status: ListItemData.spouse
imageSource: "asset:///pics/" + ListItemData.pic
contextActions: [
ActionSet {
DeleteActionItem {
onTriggered: {
var myview = standardListItem.ListItem.view;
var dataModel = myview.dataModel;
var indexPath = myview.selected();
// data model must support item deletion.
dataModel.removeItem(indexPath);
}
}// DeleteActionItem
}// ActionSet
] // ContextActions
} // StandardListItem
} // ListItemComponent
DeleteActionItem
’s onTriggered()
slot, you will notice that the code is accessing the ListView
using the root visual’s ListItem
property (typically, you would use the ListView
’s id directly). Once again, this would not work because the ListView
’s id has been defined in a different document context and is not visible to the DeleteActionItem
. Instead, you must use the StandardListItem
’s ListItem
attached property to get to the view.Accessing the Application Delegate
ListItem
’s properties, you will notice that ListItem
does not provide a property to reference the application delegate (or, as a matter of fact, any other object added to main.qml
’s document context). As mentioned in Chapter 3, document contexts are hierarchical in nature. Therefore, if you set a property in the root context created by the QML declarative engine, it will be visible to all document contexts (because the root context is inherited by all document contexts). This is very similar to a global variable, which will be visible anywhere in your code. As illustrated in Listing 6-13, the standard way of setting the application delegate was to use the main.qml
document context (see Chapter 3 for more details about document contexts).// Create scene document from main.qml asset, the parent is set
// to ensure the document gets destroyed properly at shut down.
QmlDocument *qml = QmlDocument::create("asset:///main.qml").parent(this);
qml->documentContext()->setContextProperty("_app", this);
main.qml
’s document context (see Listing 6-14).QDeclarativeEngine* engine = QmlDocument::defaultDeclarativeEngine();
QDeclarativeContext* rootContext = engine->rootContext();
rootContext->setContextProperty("_app", this);
main.qml
, you will have to declare a property alias referencing the original property in the ListView
(see Listing 6-15).TextField{
id: myfield
}
ListView {
id: listview
property alias text: myfield.text; // accessible as ListItem.view.text
}
Multiple Selection Mode
ListView
and then use a context action to process those items (note that the action will appear in a special overflow context menu and will only be visible when multiple selection mode is active). Listing 6-16 shows you how to implement multiple selection mode.Page {
actions: [
MultiSelectActionItem {
multiSelectHandler: listview.multiSelectHandler
}
]
ListView {
id: listview
dataModel: XmlDataModel {
id: people
source: "people.xml"
}
multiSelectHandler {
status: "0 items selected"
actions: [
ActionItem {
title: "Share"
onTriggered:{
// handle share items}
}
} // ActionItem
] // actions
} // multiSelectHandler
onSelectionChanged: {
listview.multiSelectHandler.status =
listview.selectionList().length + " items selected";
}
onTriggered:{
// code omitted
}
listItemComponents:[
// code omitted
]
} // ListView
} // Page
MultiSelectActionItem
using the Page
’s actions
property (this will effectively create a “Select items” action in the overflow menu; see Figure 6-9). Then the Page’s MultiSelectActionItem
has to reference a MultiSelectHandler
, which is defined using the ListView
’s multiSelectHandler
property. In other words, the MultiSelectActionItem
is a special type of ActionItem
that references a MultiSelectHandler
, which actually defines ActionItems
(the ActionItems
will be displayed only when multiselection mode is active). Also note that in multiselection mode, the ListView
’s triggered()
signal is not emitted when an item is selected. Instead, if you need to determine which item has been selected, you will need to use the ListView
’s selectionChanged()
signal. Finally, a MultiSelectHandler
can also display a status
and a cancel
button (see Listing 6-16 and Figure 6-10).MultiSelectActionItem
directly in the ListView
(Listing 6-17) by setting the ListView
’s multiSelectAction
property (in this case, the multiple selection mode will be available as a context action; in other words, it will appear after a long press on a ListView
item).Page{
ListView {
id: listview
dataModel: XmlDataModel {
id: people
source: "people.xml"
}
multiSelectAction: MultiSelectActionItem {
multiSelectHandler: listview.multiSelectHandler
}
multiSelectHandler {
// same as before
}
}
}
Layout
ListView
’s default layout, which is a StackListLayout
. You can, however, customize the layout by using a GridListLayout
, which will display the list items in a grid. I am not going to get into specifics of the GridListLayout
, but I will mention that you can use it to display list items as image thumbnails. For example, Listing 6-18 shows you how to use an ImageView
to display image thumbnails.import bb.cascades 1.2
import bb.data 1.0
Page {
Container {
ListView {
id: listview
layout: GridListLayout {
}
dataModel: XmlDataModel {
id: datamodel
source: "people.xml"
}
listItemComponents: [
ListItemComponent {
type: "person"
ImageView {
imageSource: "asset:///pics/" + ListItemData.pic
scalingMethod: ScalingMethod.AspectFill
}
} // LisitItemComponent
]
} // ListView
} // Container
} // Page
ListView
, the data model has to be flat (see Listing 6-19).<people>
<person name="John Wayne" pic="wayne.jpg"/>
<person name="Kirk Douglas" pic="douglas.jpg"/>
<person name="Clint Eastwood" pic="eastwood.jpg"/>
<person name="Spencer Tracy" pic="tracy.jpg"/>
<person name="Lee Van Cleef" pic="vancleef.jpg"/>
<person name="John F. Kennedy" pic="kennedy.jpg"/>
<person name="Bill Clinton" pic="clinton.jpg"/>
<person name="George Bush" pic="bush.jpg"/>
<person name="Barack Obama" pic="obama.jpg"/>
</people>
Creating Visuals in C++
ListItemProvider
class. Note that a recurring theme in this book has always been to use the declarative power of QML to create your UI, and that C++ should be exclusively used for your app’s business logic. Therefore, ListItemProvider
is mentioned here for the sake of completeness without getting into the implementation details (the techniques shown in the previous sections based on ListItemComponent
objects should be preferred in most cases in practice).ListItemProvider
is essentially a factory interface for creating visuals in C++. For your own ListItemProvider
subclass, you must implement the following pure virtual functions:
-
VisualNode* ListItemProvider::createItem(ListView* listview, const QString& type)
: A factory method for creating a VisualNode. Returns a visual for the listview for an item of the given type. Note that theListView
will take ownership of theVisualNode
instance. -
void ListItemProvider::updateItem(ListView* listView, VisualNode* visual, const QString& type, const QVariantList& indexPath, const QVariant& data)
: Updates the specified list item based on the provided item type, index path, and data.
VisualNode
is the parent class of all Cascades controls, including custom controls. You can therefore create your own custom control in C++ and return it from ListItemProvider::createItem()
(or alternatively, you could use a Cascades Container
as the root visual).VisualNode
s are kept in an internal cache and « recycled » by the ListView
to improve performance. You should therefore be aware that you can’t store a data model’s state in a VisualNode
and access it at a later time. You must always make sure that an item’s state is updated and stored in the data model directly (I will tell you more about recycling in the following section).ListItemProvider
instance can optionally implement the ListItemListener
interface, which is called by the ListView
to handle focus and item states:
-
ListItemListener::select(bool select)
: Called by theListView
when an already visible item becomes selected.select
istrue
when the item is selected; it is false otherwise. -
ListItemListener::activate(bool activate)
: Called by theListView
when an already visible item is active. An item is active while a user is pressing the item. -
ListItemListener::reset(bool selected, bool activated)
: Called by theListView
when an item is about to be shown. If selected istrue
, the item should appear selected. If activated istrue
, the item should appear active.
https://github.com/blackberry/Cascades-Samples/tree/master/
cascadescookbookcpp
. Look in the project’s src
folder for the RecipeItemFactory
and RecipeItem
classes, which respectively provide implementations for ListItemProvider
and ListItemListener
classes.Data Models
ListView
, it is time to consider what happens behind the scenes in a data model. A data model not only encapsulates data but also specifies how it will be mapped to the contents of a list view. The data model can be an arbitrarily complex tree structure, but the list view will always display at most a two-level deep hierarchy. You can, however, set any node in the list view as the root of the hierarchy. It is also important to emphasize that the data model does not care about any visual adornments of the data. In other words, a data model is all about data description (the actual data presentation and formatting is taken care of by the visuals described in the “List Views” section). Finally, the Cascades framework comes out of the box with several standard data models than you can readily plug into your own applications.Index Paths
DataModel
. The root node of a data model always has an empty index path. The items immediately under the root node have an index path of size 1. The items two levels down have an index path of size 2, and so on. For example, Figure 6-12 illustrates a hypothetical data model for fruits and vegetables.
QVariantList
of integers).Standard Data Models
XmlDataModel
, which is a great drop-in model for prototyping the visual aspects of your ListView
. However, XmlDataModel
has its own set of limitations: for example, you can’t update the data by adding or removing items. Therefore, in practice, you will have to use one of the other standard data models or take the extra step of implementing your own DataModel
instance from scratch. You can also broadly categorize data models as sorted and unsorted models (a sorted data model will use a key for sorting its data items). In this section, I concentrate on the ArrayDataModel
and GroupDataModel
.ArrayDataModel
ArrayDataModel
is an unsorted flat
DataModel
(in others words, the ArrayDataModel
’s hierarchy is only one level deep, immediately below the root node). An ArrayDataModel
is useful when you want to manage a list of items and manipulate their order manually. You can easily insert, remove, and shuffle items. The items must be QVariant
s in order to insert them in an ArrayDataModel
. An interesting characteristic of the ArrayDataModel
is that if a QVariant
contains a QObject*
, the ArrayDataModel
will take ownership of the object if it does not already have a parent (in other words, the ArrayDataModel
can handle memory management of the objects for you).ArrayDataModel
:-
ArrayDataModel::append(const QVariant& value):
Insertsvalue
at the end of this model. -
ArrayDataModel::append(const QVariantList& values)
: Inserts a list of values at the end of this model. -
ArrayDataModel::insert(int i, const QVariant& value)
: Insertsvalue
at the position defined byi
. Ifi
is 0, the value is preprended, and ifi
isArrayDataModel::size()
, the value is appended. -
ArrayDataModel::move(int from, int to)
: Moves thevalue
from one index position to another index position. The index positions have to be in the range[0,ArrayDataModel::size()]
. This method has no effect if the indexes are out of range. Note that this is practically equivalent to an insert. The element already at theto
position is not removed. -
ArrayDataModel::swap(int i, int j)
: Swaps the values given by index positionsi
andj
. -
ArrayDataModel::removeAt(int i)
: Removes the value at index positioni
. If the value is aQObject*
owned by theArrayDataModel
, it will also be deleted. -
ArrayDataModel::replace(int i, const QVariant& value)
: Replaces the item at specified index positioni
withvalue
. If the previousvalue
at positioni
is owned by theArrayDataModel
, it will be deleted.
ArrayDataModel
. You can nevertheless call the previous functions from QML because they are all marked as Q_INVOKABLE
in C++ (for example, Listing 6-20 shows you how to use an ArrayDataModel
in QML).import bb.cascades 1.2
Page {
Container {
ListView {
id: listview
dataModel: ArrayDataModel {
id: arrayDataModel
}
listItemComponents: [
ListItemComponent {
type: ""
StandardListItem {
title: ListItemData
description: "Fruit"
status: "Good for you!"
}
}
]
onTriggered: {
listview.clearSelection();
listview.toggleSelection(indexPath);
}
} // ListView
onCreationCompleted: {
var values = ["apple", "banana", "peach", "tangerine"]
arrayDataModel.append(values);
arrayDataModel.append("mango");
}
} // Container
} // Page
ArrayDataModel
will always return an empty string for the type
property. You will therefore have to also define an empty string for ListItemComponent
’s type
property in the previous example so that the ListView matches the ListItemComponent
to the ArrayDataModel
’s items.StandardListItem
’s title
property is bound to ListItemData
, which directly corresponds to a data node in the ArrayDataModel
(unlike some of the previous examples in this chapter, where the returned data node was a map and you had to use ListItemData.<keyname>
to access the actual data value).JSON
file, rather than creating them in the onCreationCompleted()
slot (see Listing 6-21). In that case, you will have to use a DataSource
to load the JSON
content and append the values to the ArrayDataModel
(a DataSource
loads data from a local source such as a JSON
or XML
file or an SQL
database). (You can also use the DataSource
’s query
property to specify an SQL
query statement or an XML
path. Finally, the DataSource
’s source
property specifies a local file or a remote URL
from which the data is loaded.)[
{
"name" : "apple",
"description" : "fruit"
},
{
"name" : "banana",
"description" : "fruit"
},
{
"name" : "peach",
"description" : "fruit"
},
{
"name" : "tangerine",
"description" : "fruit"
},
{
"name" : "mango",
"description" : "fruit"
}
]
DataSource
to load the JSON
document shown in Listing 6-21 in an ArrayDataModel
.import bb.cascades 1.2
import bb.data 1.0
Page {
Container {
ListView {
id: listview
dataModel: ArrayDataModel {
id: arrayDataModel
}
listItemComponents: [
ListItemComponent {
type: ""
StandardListItem {
title: ListItemData.name
description: ListItemData.description
status: "Good for you!"
}
}
]
onTriggered: {
listview.clearSelection();
listview.toggleSelection(indexPath);
}
} // ListView
attachedObjects: [
DataSource {
id: dataSource
source: "asset:///fruits.json"
onDataLoaded: {
for (var i = 0; i < data.length; i ++) {
arrayDataModel.append(data[i]);
}
}
}
]
onCreationCompleted: {
dataSource.load();
}
} // Container
} // Page
import bb.data 1.0
statement before using the DataSource
control in QML. Also, as shown in the code, the ListView
’s onCreationCompleted
slot loads the data in the DataSource
. As soon as the loading process has completed, the DataSource
triggers the dataLoaded()
signal, which is used to populate the ArrayDataModel
(the signal passes an implicit data
parameter, which is either an array
if the root element in the JSON
document is an array
, or a map
if the root element is an object
).GroupDataModel
GroupDataModel
is a sorted data model where you can specify keys to sort the model’s items. Data items can be QVariantMap
objects and/or QObject*
pointers (you might recall from Chapter 3 that a QVariantMap
is a typedef QMap<QString, QVariant>
). If the data item is a QVariantMap
, a GroupDataModel
will try to sort it by matching its own keys with corresponding keys in the QVariantMap
. If the item is a QObject*
, it will try to match the keys with object properties. Obviously, to sort an item correctly, it must contain a corresponding key or property. You can also specify multiple keys for the GroupDataModel
. In that case, the items will be sorted by applying the sorting criteria in the order the keys have been defined.GroupDataModel::grouping
property. When grouping is enabled, a two-level hierarchy is automatically created for you and passed to the list view for display. The first level (with an index path of size 1) corresponds to the grouping headers and is generated by a GroupDataModel
. The second level (with an index path of size 2) corresponds to the data items. Finally, a GroupDataModel
’s type
property will return “header” for header items and “item” for all other items).GroupDataModel
:
-
GroupDataModel::GroupDataModel(const QStringList& keys, QObject* parent=0)
: Constructs a newGroupDataModel
with the specified sorting keys. -
GroupDataModel::insert(QObject* object)
: Inserts theQObject*
in theGroupDataModel
. If the object has no parent, theGroupDataModel
will take ownership of the object. -
GroupDataModel::insert(const QVariantMap& item)
: Inserts theQVariantMap
in thisGroupDataModel
. -
GroupDataModel::insertList(const QVariantList& items)
: Inserts theQVariantList
in thisGroupDataModel
. The items can be either instances ofQVariantMap
orQObject*
. -
GroupDataModel::setGrouping(bb::cascades::ItemGrouping::Type itemGrouping)
: Sets the grouping logic for thisGroupDataModel. ItemGrouping::Type
can beItemGrouping::None
(items are not grouped),ItemGrouping::ByFirstChar
(items will be grouped by first character), andItemGrouping::ByFullValue
(items are grouped using entire strings). -
GroupDataModel::setSortAscending(bool ascending)
: If true, items are sorted in ascending order; otherwise, items are sorted in descending order.
GroupDataModel
in QML; note how the sorting keys are defined in the onCreationCompleted()
slot).import bb.cascades 1.2
import bb.data 1.0
Page {
Container {
ListView {
id: listview
dataModel: GroupDataModel {
id: groupDataModel
}
listItemComponents: [
ListItemComponent {
type: "item"
StandardListItem {
title: ListItemData.name
description: ListItemData.description
status: "Good for you!"
}
}
]
onTriggered: {
listview.clearSelection();
listview.toggleSelection(indexPath);
}
} // ListView
attachedObjects: [
DataSource {
id: dataSource
source: "asset:///fruitsandvegetables.json"
onDataLoaded: {
for (var i = 0; i < data.length; i ++) {
groupDataModel.insert(data[i]);
}
}
}
]
onCreationCompleted: {
dataSource.load();
groupDataModel.sortingKeys = ["name", "description"];
}
} // Container
} // Page
ListView
with an updated version of the JSON
, which includes vegetables. This mainly illustrates how items are clustered and sorted by the GroupDataModel
(once again, keep in mind that the GroupDataModel
automatically generates the header items).
Mapping Item Types
ListView
essentially uses a DataModel’s item type to select the corresponding visual for rendering the item. You can further refine the way data items are mapped to types by using one of the following techniques:
-
In QML, you can define a JavaScript « mapping » function in the
ListView
. The function will have to return a string specifying the type of a given data item and an index path. -
You can choose to override a
DataModel::itemType(const QVarianList & indexPath)
in C++ so that it returns a meaningful type (for example, you could define aMyArrayDataModel
class, which inherits fromArrayDataModel
and overrides theArrayDataModel::itemType(const QVarianList & indexPath)
method to return something other than the empty string). -
You can implement the
ListItemTypeMapper
interface in C++ and then assign it to theListView
usingListView::setListItemTypeMapper(ListItemTypeMapper* mapper)
.
Defining a JavaScript Mapping Function
ListView
’s body declaration, which will « override » the DataModel::itemType(const QVariantList& indexPath)
method provided by the DataModel
. For example, Listing 6-24 shows you how to override the default « item » and « header » types returned by a GroupDataModel
using a JavaScript.import bb.cascades 1.2
import bb.data 1.0
Page {
Container {
ListView {
id: listview
dataModel: GroupDataModel {
id: groupDataModel
}
function itemType(data, indexPath) {
return (indexPath.length == 1 ? "myheader" : "myitem");
}
listItemComponents: [
ListItemComponent {
type: "myheader"
CustomListItem {
dividerVisible: true
Label {
text: ListItemData
textStyle {
base: SystemDefaults.TextStyles.BigText
fontWeight: FontWeight.Bold
color: Color.create("#7a184a")
}
}
}
},
ListItemComponent {
type: "myitem"
StandardListItem {
id: standardListItem
title: ListItemData.name
description: ListItemData.description
status: "Good for you!"
}
}
]
} // ListView
attachedObjects: [
DataSource {
id: dataSource
source: "asset:///fruitsandvegetables.json"
onDataLoaded: {
for (var i = 0; i < data.length; i ++) {
groupDataModel.insert(data[i]);
}
}
}
]
onCreationCompleted: {
dataSource.load();
groupDataModel.sortingKeys = [ "name", "description" ];
}
} // Container
} // Page
Implementing ListItemTypeMapper
ListItemTypeMapper
interface can be used in C++ to map a data item to an item type. Here again, the main disadvantage of using a ListItemTypeMapper
is that you will have to reference the ListView
from C++, which is something you should try to avoid in practice because it adds tight coupling between your QML UI and C++ business logic (note that to access the ListView
from C++, you will also have to set its objectName
in QML). On the other hand, A ListItemTypeMapper
cleanly separates the type mapping logic from the actual data model implementation. In other words, by implementing a ListItemTypeMapper
class, you can save yourself the necessity of extending one of the standard data model classes to override DataModel::itemType()
.ListView
’s ListItemTypeMapper
in C++ (for illustration purposes, MyListItemTypeMapper
’s methods have been defined inline).#include <bb/cascades/Application>
#include <bb/cascades/QmlDocument>
#include <bb/cascades/AbstractPane>
#include <bb/cascades/ListItemTypeMapper>
#include <bb/cascades/ListView>
using namespace bb::cascades;
class MyListItemTypeMapper : public ListItemTypeMapper, QObject{
public:
MyListItemTypeMapper(QObject* parent) : QObject(parent){};
∼MyListItemTypeMapper(){};
QString itemType(const QVariant& data, const QVariantList& indexPath){
return (indexPath.length() == 1 ? "myheader" : "myitem");
}
};
ApplicationUI::ApplicationUI(bb::cascades::Application *app) :
QObject(app)
{
QmlDocument *qml = QmlDocument::create("asset:///main.qml").parent(this);
// Create root object for the UI
AbstractPane *root = qml->createRootObject<AbstractPane>();
ListView* listView = root->findChild<ListView*>("listview");
MyListItemTypeMapper* mapper = new MyListItemTypeMapper(listView);
listView->setListItemTypeMapper(mapper);
// Set created root object as the application scene
app->setScene(root);
}
Implementing a Custom Data Model
DataModel
class (see Listing 6-26).class DataModel : public QObject {
Q_OBJECT
public:
Q_INVOKABLE virtual int childCount(const QVariantList &indexPath) = 0;
Q_INVOKABLE virtual bool hasChildren(const QVariantList &indexPath) = 0;
Q_INVOKABLE virtual QVariant data(const QVariantList &indexPath) = 0;
Q_INVOKABLE virtual QString itemType(const QVariantList &indexPath);
signals:
void itemAdded(QVarianList indexPath);
void itemRemoved(QVariantList indexPath);
void itemUpdated(QVariantList indexPath);
// itemChanged() omitted
};
DataModel
defines the following methods for which you will have to provide an implementation:
-
int DataModel::hasChildren(const QVariantList& indexPath)
: Returnstrue
if the data item identified byindexPath
has children; it isfalse
otherwise. This is a pure virtual function. -
int DataModel::childCount(const QVariantList& indexPath)
: Returns the number of children of the data item specified byindexPath
. -
QVariant DataModel::data(const QVariantList& indexPath)
: Returns the data item that is associated withindexPath
wrapped as aQVariant
.
DataModel::itemType()
function that is used by the ListView
to match the corresponding ListItemComponent
for creating the item visuals (or alternatively, provide a ListItemTypeMapper
implementation to the ListView
, as illustrated in the previous section):-
QString DataModel::itemType(const QVariantList& indexPath)
: Returns the type of the data item identified by theindexPath
. By default, the method returns an empty string.
ListView
when the DataModel’s state changes:-
void DataModel::itemAdded(QVariantList indexPath)
: Emitted when a new item has been added to this DataModel. indexPath gives the index path of the new item. -
void DataModel::itemRemoved(QVariantList indexPath)
: Emitted when an item has been removed from this DataModel. indexPath is the index path of the removed item. -
void DataModel::itemUpdated(QVariantList indexPath)
: Emitted when an item has been updated. indexPath is the index path of the updated item.
DataModel::itemChanged()
, is not covered here, but it can be used for notifying bulk operations such as multiple additions and removals (the signal can be used in practice to optimize notifications, rather than emitting multiple-times more granular signals, such as DataModel::itemAdded()
and DataModel::itemRemoved()
).DataModel
can return to the ListView
any kind of data that can be contained in a QVariant
(however, the typical data types packaged as QVariant
s are QString
, QVariantMap
, and QObject*
).DataModel
implementation in practice, let’s replace the XmlDataModel
used in Listing 6-6 with our own custom model. Also, let’s switch the data source format from XML
to JSON
. Listing 6-27 gives you an equivalent JSON
representation of the XML document provided in Listing 6-5 (note that unlike the XML
document, the JSON
format is nonhierarchical. However, a new job attribute has been introduced to differentiate an Actor from a President).[
{
"name" : "John F. Kennedy",
"born" : "May 29, 1917",
"spouse" : "Jacqueline Kennedy",
"pic" : "kennedy.jpg",
"job" : "president"
},
{
"name" : "Bill Clinton",
"born" : "August 19, 1946",
"spouse" : "Hillary Rodham Clinton",
"pic" : "clinton.jpg",
"job" : "president"
},
{
"name" : "John Wayne",
"born" : "May 26, 1907",
"spouse" : "Pilar Pallete",
"pic" : "wayne.jpg",
"job" : "actor"
},
// more presidents and actors in no particular order.
]
#ifndef MYDATAMODEL_H_
#define MYDATAMODEL_H_
#include <QObject>
#include <bb/cascades/DataModel>
#include <bb/data/JsonDataAccess>
class MyDataModel: public bb::cascades::DataModel {
Q_OBJECT
Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged);
public:
MyDataModel(QObject* parent = 0);
virtual ∼MyDataModel();
Q_INVOKABLE int childCount(const QVariantList& indexPath);
Q_INVOKABLE QVariant data(const QVariantList& indexPath);
Q_INVOKABLE bool hasChildren(const QVariantList& indexPath);
Q_INVOKABLE QString itemType(const QVariantList& indexPath);
Q_INVOKABLE void removeItem(const QVariantList& indexPath);
signals:
void sourceChanged();
private:
QString source();
void setSource(QString source);
void load(QString filename);
QString m_source;
QVariantList m_presidents;
QVariantList m_actors;
};
#endif /* MYDATAMODEL_H_ */
MyDataModel
class definition declares a source
property, which can be set in QML to identify the source file containing the JSON
data. The m_presidents
and m_actors
member variables are used to store the data items loaded from the JSON
file. Finally, all virtual functions declared in the DataModel
interface are overridden (the function definitions are discussed next).setSource()
method is called when MyDataModel
’s source
property is set in QML (Listing 6-29). The method updates the corresponding m_source
member variable and then calls the load()
function, which is responsible for loading the JSON
data from the file system.void MyDataModel::setSource(QString source) {
if (m_source == source)
return;
m_source = source;
this->load(source);
emit sourceChanged();
}
load()
function given in Listing 6-30 uses a JsonDataAccess
object to load the contents of the JSON
file (note that the function assumes that the file is located in the application’s assets folder). Because the root object in the JSON
file is an array
, we try to “cast” the QVariant
returned by the JsonDataAccess.load()
method into a QVariantList
object. Finally, the function uses the job
attribute for each data entry to determine the appropriate member container to update (either m_actors
or m_presidents
).void MyDataModel::load(QString source) {
bb::data::JsonDataAccess json;
QVariantList entries =
json.load(QDir::currentPath() + "/app/native/assets/" + source).toList();
if (!json.hasError()) {
for (int i = 0; i < entries.length(); i++) {
QVariantMap entry = entries[i].toMap();
if (entry["job"] == "actor") {
m_actors.append(entry);
}
else {
m_presidents.append(entry);
}
}
}
}
DataModel
interface.hasChildren()
method shown in Listing 6-31 returns true
for the root and header nodes, and false otherwise (the root node’s index path size is 0; the header node’s index path size is 1).bool MyDataModel::hasChildren(const QVariantList &indexPath) {
if ((indexPath.size() == 0) || (indexPath.size() == 1))
return true;
else
return false;
}
childCount
method shown in Listing 6-32 returns the children of a given data node. Since we want to keep the same hierarchical structure as the one defined in the original XML
structure, the childCount()
method will return 2 for the root item (this corresponds to the header items “Actors” and “US Presidents”. Also note that the header items do not actually exist in the JSON
file; the data model will dynamically create them). For items two levels deep in the data hierarchy with an index path of size 1, we return the number of elements in the m_actors
and m_presidents
list, respectively.int MyDataModel::childCount(const QVariantList &indexPath) {
if (indexPath.size() == 0) {
return 2; // for headers "Actors" and "US Presidents"
} else {
if (indexPath.size() == 1) {
if (indexPath.at(0).toInt() == 0) {
return m_actors.size();
} else if (indexPath.at(0).toInt() == 1) {
return m_presidents.size();
}
} else {
return 0;
}
}
}
data()
method (see Listing 6-33). The data nodes corresponding to header items—with an index path of size 1—are dynamically created. The data nodes—with an index path of size 2—are returned from the m_actors
and m_presidents
member variables (also, we keep the same structure as the original XML
document by returning the Actors’ values before the US Presidents values).QVariant MyDataModel::data(const QVariantList &indexPath) {
if (indexPath.size() == 1) {
if (indexPath.at(0).toInt() == 0) {
QVariantMap actorsHeader;
actorsHeader["value"] = "Actors";
return actorsHeader;
} else {
QVariantMap presidentsHeader;
presidentsHeader["value"] = "US Presidents";
return presidentsHeader;
}
} else if (indexPath.size() == 2) {
if (indexPath.at(0) == 0) {
return m_actors.at(indexPath.at(1).toInt());
} else {
return m_presidents.at(indexPath.at(1).toInt());
}
}
QVariant v;
return v;
}
itemType()
method shown in Listing 6-34 returns the data type of the node given by an index path.QString MyDataModel::itemType(const QVariantList &indexPath) {
if (indexPath.size() == 1)
return "category";
if (indexPath.size() == 2)
return "person";
return "";
}
MyDataModel::removeItem(const QVariantList& indexPath)
method can be associated with a DeleteActionItem
to remove an item (see Listing 6-35).void MyDataModel::removeItem(const QVariantList& indexPath){
if(indexPath.size() == 2){
if(indexPath.at(0) == 0){
m_actors.removeAt(indexPath.at(1).toInt());
}else{
m_presidents.removeAt(indexPath.at(1).toInt());
}
emit itemRemoved(indexPath);
}
}
itemRemoved()
signal is emitted in Listing 6-35 for notifying the ListView
that the data model has changed (if you omit the signal, the ListView
’s visual appearance would not be updated). In a similar way, you could implement methods for adding and updating items.MyDataModel
in QML, you will need to register it with the QML type system in main.cpp
(see Listing 6-36).#include <MyDataModel.h>
Q_DECL_EXPORT int main(int argc, char **argv)
{
qmlRegisterType<MyDataModel>("ludin.datamodels", 1, 0, "MyDataModel");
Application app(argc, argv);
// Create the Application UI object, this is where the main.qml file
// is loaded and the application scene is set.
new ApplicationUI(&app);
// Enter the application main event loop.
return Application::exec();
}
MyDataModel
in main.qml
(see Listing 6-37).import bb.cascades 1.2
import ludin.datamodels 1.0
Page {
id: page
Container {
ListView {
id: listview
dataModel: MyDataModel {
source: "people.json"
}
listItemComponents: [
ListItemComponent {
type: "category"
CustomListItem {
id: customListItem
dividerVisible: true
Label {
text: ListItemData.value
// Apply a text style to create a large, bold font with
// a specific color
textStyle {
base: SystemDefaults.TextStyles.BigText
fontWeight: FontWeight.Bold
color: Color.create("#7a184a")
}
} // Label
} // CustomListItem
},
ListItemComponent {
type: "person"
StandardListItem {
id: standardListItem
title: ListItemData.name
description: ListItemData.born
status: ListItemData.spouse
imageSource: "asset:///pics/" + ListItemData.pic
contextActions: [
ActionSet {
DeleteActionItem {
onTriggered: {
var myview = standardListItem.ListItem.view;
var datamodel = myview.dataModel;
var indexPath = myview.selected();
datamodel.removeItem(indexPath);
}
} // DeleteActionItem
} // ActionSet
] // ContextActions
} // StandardListItem
} // ListItemComponent
] // ListItemComponents
} // ListView
} // Container
} // Page
Asynchronous Data Models
ListView
must be responsive and be able to display its items as fast as possible. You must therefore ensure that the data model’s methods covered in the previous section are very fast and nonblocking. In practice, a method could block because you are trying to load a very large or a remote data set. As an immediate consequence, the Cascades UI will also freeze or behave extremely sluggishly. Therefore, to avoid any of these negative impacts on your Cascades UI, you will have to use asynchronous data model methods combined with signals such as DataModel::itemAdded()
to update the ListView
.-
Asynchronous data processing is covered by the document found at
http://developer.blackberry.com/native/documentation/cascades/ui/lists/asynch_data.html
. -
Managing very large data sets is covered by the document found at
http://developer.blackberry.com/native/documentation/cascades/device_platform/data_access/data_manager.html
.
Persistence
XmlDataModel
is an exception: you can load an XML
document by specifying the XmlDataModel
’s source
property, but you cannot save the document). Again this is not a limitation because you can easily subclass a data model to add persistence.Updating Data Items with Cascades Controls
[
{
"name" : "apple",
"description" : "fruit",
"available" : "false"
},
{ "name" : "ananas",
"description" : "fruit",
"available" : "true"
},
{ "name" : "avocado",
"description" : "fruit",
"available" : "false"
},
{
"name" : "banana",
"description" : "fruit",
"available" : "false"
},
{ "name" : "broccoli",
"description": "vegetable",
"available" : "true"
},
// more fruits and vegetables
]
checkChanged()
signal emitted by the check box and update the data model accordingly (see Listing 6-39).import bb.cascades 1.2
import bb.data 1.0
Page {
Container {
ListView {
id: listview
objectName: "listview"
dataModel: GroupDataModel {
id: groupDataModel
}
listItemComponents: [
ListItemComponent {
type: "myheader"
CustomListItem {
dividerVisible: true
Label {
text: ListItemData
textStyle {
base: SystemDefaults.TextStyles.BigText
fontWeight: FontWeight.Bold
color: Color.create("#7a184a")
}
}
}
},
ListItemComponent {
type: "myitem"
CustomListItem {
id: customItem
Container {
verticalAlignment: VerticalAlignment.Center
layout: StackLayout {
orientation: LayoutOrientation.LeftToRight
}
CheckBox {
id: checkBox
checked: ListItemData.available
onCheckedChanged: {
if (customItem.ListItem.initialized) {
var index = customItem.ListItem.indexPath;
console.log("Changing " + index);
var dataModel = customItem.ListItem.view.dataModel;
var val = dataModel.data(index);
val.available = checked;
dataModel.updateItem(index, val);
console.log("after update: "
+dataModel.data(index).name+
", available: "
+dataModel.data(index).available);
}
} // onCheckedChanged
}
Label {
text: ListItemData.name
}
} // Container
} // CustomListItem
} // ListItemComponent
] // ListItemComponents
} // ListView
attachedObjects: [
DataSource {
id: dataSource
source: "asset:///fruitsandvegetables.json"
onDataLoaded: {
for (var i = 0; i < data.length; i ++) {
groupDataModel.insert(data[i]);
}
}
}
]
onCreationCompleted: {
dataSource.load();
groupDataModel.sortingKeys = [ "name", "description" ];
}
}
}
ListItem
is initialized before handling the state update (otherwise, the ListView
might be in the process of recycling the visual and the check box might be in a transient state). If the ListItem
is effectively initialized, you can proceed by updating the data model. You can achieve this by first getting a copy of the data item, then updating the copy, and finally, replacing the original item with the copy in the data model (data items are returned as QVariant
s by the data model, and therefore you can only get a copy the original data item, as opposed to a reference to the original data).Summary
ListView
, which is one of Cascades’ most flexible controls. You can use a ListView
to display arbitrarily complex hierarchical information as a succinct list of items. ListView
s conveniently separate data from presentation using the MVC pattern. The ListView
plays the role of a controller. A DataModel
handles application data, and ListItemComponent
s define the visuals in charge of rendering a data item. Cascades also gives you standard visuals, such as StandardListItem
and Header
, to ensure a consistent look and feel across Cascades applications.ListView
communicates with its DataModel
using a tree abstraction, where each node in the tree is identified by an index path. The root node’s index path is an empty array. The ListView
will, at most, render two sublevels of your data under the root node. You can, however, set the root node anywhere in your data model, giving you effectively deeper than two levels of interaction.