Welcome!

Welcome to the official BlackBerry Support Community Forums.

This is your resource to discuss support topics with your peers, and learn from each other.

inside custom component

Native Development Knowledge Base

Using a Custom DataModel to Filter Data for a ListView

by BlackBerry Development Advisor (Retired) on ‎06-20-2012 05:45 PM - edited on ‎04-24-2013 10:43 AM by BlackBerry Development Advisor (13,786 Views)

Introduction

This article explores using a custom DataModel to dynamically filter the data displayed by a ListView.

 

In this example, we'll build a simple ListView with expandable headers.  The headers are collapsed by default; clicking on a header expands the header to show its contents. Clicking again hides the contents.  Only one header may be expanded at a time.

 

This is implemented by subclassing DataModel to expose only some of the data from an underlying data model.  We will use the data model from the KB article "Using your own Data Model" as our underlying data model, as this illustrates subclassing DataModel and is suitably simple.

 

About ListView and DataModels

ListView is a commonly used UI widget to display data that it gets from a data model. There are many samples that illustrate common ways of using ListView to display XML or JSON data; see https://bdsc.webapps.blackberry.com/cascades/sampleapps, in particular ScrollableLists and Stamp Collector. These illustrate the use of GroupDataModel and QListDataModel.

 

Hello World Filtered List – step by step

We will build up the example in several stages.  We assume you know how to create a project, add a new class, and that you understand the basics of Momentics and C++.

 

To create the app as we go, start by creating a new BlackBerry® Cascades™ C++ project as a Standard empty project.  Add the HelloWorldDataModel.h and .cpp from the github project located here:  https://github.com/blackberry/Cascades-Samples/tree/master/filtereddatamodel

 

FilteredDataModel declaration

The filtered data model is a subclass of DataModel that will fetch appropriate data from the underlying HelloWorldDataModel. It merely needs to keep track of the currently expanded header, and return 0 for the number of children of any other header.

 

Create class FilteredDataModel, subclassed from bb::cascades::DataModel. Declare the required interface functions childCount(), hasChildren(), and data().

 

We need a variable to hold the underlying data model, and another variable to hold the current expanded header:

private:
    bb::cascades::DataModel* m_fullDataModel;
    int m_expandedIndex;  // currently expanded header by index, or -1 if none expanded

 

We only ever have one header expanded at a time, so a simple int variable will suffice.  We can use -1 to indicate neither header expanded.

 

We don’t need to know the specific type of the underlying data model, so we hold a pointer to the data model by the superclass.  We’ll simplify and assume that the underlying data model object lives at least as long as this object, so we can use a raw pointer instead of a QPointer or QSharedPointer.  The declaration does not depend on HelloWorldDataModel.

 

We will need a method to select which header is to be expanded, a method to check if the header is expanded, and some helper functions.

 

Your filtered data model declaration should look something like this:

 

/*
 * FilteredDataModel.h
 */

#ifndef FILTEREDDATAMODEL_H_
#define FILTEREDDATAMODEL_H_

#include <bb/cascades/DataModel>

class FilteredDataModel : public bb::cascades::DataModel
{
public:
    FilteredDataModel();
    virtual ~FilteredDataModel();

    // Required interface implementation
    virtual int childCount(const QVariantList& indexPath);
    virtual bool hasChildren(const QVariantList& indexPath);
    virtual QVariant data(const QVariantList& indexPath);

    void expandHeader(int headerIndex, bool selected);
    bool isHeaderExpanded(int headerIndex) const {return headerIndex == m_expandedIndex;}

private:
    bool isFiltered(const QVariantList& indexPath) const;
    void setExpandedHeader(int headerIndex);

private:
    bb::cascades::DataModel* m_fullDataModel;
    int m_expandedIndex;  // currently expanded header by index, or -1 if none expanded
};

#endif /* FILTEREDDATAMODEL_H_ */

 

FilteredDataModel implementation

The implementation needs construction, implementing the required interface, and managing the filter.

 

Initialize the data to have all headers collapsed.   For the purpose of this article, we will hard-code the data model to use our simple underlying hello world : set m_fullDataModel to a new instance of HelloWorldDataModel, and delete the instance in the destructor.  In a production version, the underlying data model would be set by the ListView qml or corresponding C++.

 

#include "FilteredDataModel.h"
#include "HelloWorldDataModel.h"

FilteredDataModel::FilteredDataModel()
: m_fullDataModel(new HelloWorldDataModel) // Hard-coded to HelloWorldDataModel
, m_expandedIndex(-1) // no header expanded
{
}

FilteredDataModel::~FilteredDataModel()
{
    if (m_fullDataModel != 0) // always, in initial version of the software
    {
        delete m_fullDataModel;
    }
}

 

The crux of the filtering is childCount().  This must return 0 children for any header that is not currently expanded, but otherwise defer to the underlying data model.  We’ll extract the query of whether the indexPath is filtered or not into its own helper routine:

 

/*
 * Return true if we are filtering this indexPath.
 * Return false if we are using the underlying data as-is.
 */
bool FilteredDataModel::isFiltered(const QVariantList& indexPath) const
{
    return indexPath.size() == 1 &&
           indexPath[0].toInt() != m_expandedIndex;
}

/*
 * Return the number of children.
 * Defer to the underlying data model unless the header is filtered.
 * Note: assumes m_fullDataModel is initialized
 */
int FilteredDataModel::childCount(const QVariantList& indexPath)
{
    if (isFiltered(indexPath))
    {
        // Unexpanded header
        return 0;
    }
    return m_fullDataModel->childCount(indexPath); // pointer always initialized
}

 

We could implement hasChildren() exactly as in HelloWorldDataModel, just returning childCount(indexPath) > 0.  But in this case we will stay general and allow for the case where it is expensive to get the exact child count in the underlying data model:

 

bool FilteredDataModel::hasChildren(const QVariantList& indexPath)
{
    if (isFiltered(indexPath))
    {
        // Unexpanded header
        return false;
    }
    return m_fullDataModel->hasChildren(indexPath); // pointer always initialized
}

 

Our final required overload is data(). We can return the requested data if we are asked for it, whether we think we should be filtered or not, although we could add defensive code to ensure that we are not filtering the data.

 

QVariant FilteredDataModel::data(const QVariantList& indexPath)
{
    return m_fullDataModel->data(indexPath); // pointer always initialized
}

 

Finally, the data model needs to know when its filter has changed. When we click a header, we will arrange for expandHeader to be called.  This should set m_expandedIndex appropriately. When we change m_expandedIndex, we need to inform the ListView. We do this with the loose coupling provided by Qt’s signals and slots, and emit a signal that the ListView handles. We are currently hooked into a small dummy data model, but if we switch to another data model we could very well be adding and removing a lot of data from the ListView.  So use a broad-brush signal that indicates a potentially large change.

 

/*
 * Expand or collapse the specified header.
 * We only support one header expanded at a time, so expanding
 * a different header will collapse the previous.
 * Requests that keep us in the current state are ignored.
 */
void FilteredDataModel::expandHeader(int headerIndex, bool expand)
{
    if (!expand)
    {
        if (headerIndex == m_expandedIndex)
        {
            // Collapse the header
            setExpandedHeader(-1);
        }
    }
    else
    {
        setExpandedHeader(headerIndex);
    }
}

/*
 * Set the currently expanded header (or none if index == -1)
 * Inform the ListView that we made a possibly large change,
 * but only if we do make a change.
 */
void FilteredDataModel::setExpandedHeader(int index)
{
    if (m_expandedIndex != index)
    {
        // Only emit if we actually make a change
        m_expandedIndex = index;
        emit itemsChanged(bb::cascades::DataModelChangeType::AddRemove);
    }
}

 

Use the filtered data model

To use the filtered data model, register it, and change the data model used by the ListView.

 

In app.cpp:

#include "FilteredDataModel.h"

using namespace bb::cascades;

App::App()
{
    qmlRegisterType<FilteredDataModel>("custom.lib", 1, 0, "FilteredDataModel");

 

and in main.qml, set the dataModel parameter:

dataModel: FilteredDataModel {
}

 

Along with the dataModel parameter, you also need to specify a name for the ListView so we can connect it to our slot later on.  The rest of main.qml should look the same as the one in the KB article "Using your own Data Model":

 

import bb.cascades 1.0
import custom.lib 1.0

Page {
    content: Container {
        background: Color.create("#272727")
        layout: DockLayout {
        }
        
        ListView {
            id: myList
            objectName: "myList"
            layoutProperties: DockLayoutProperties {
                horizontalAlignment: HorizontalAlignment.Center
            }
            dataModel: FilteredDataModel { }
            
            // Treat outer level as header, inner level as item
            function itemType(data, indexPath) {
                if (indexPath.length == 1)
                {
                    return "header";
                }
                return "item";
            }
        }
    }
}

 

 

If you run the program now, you will see both headers collapsed but they don’t expand when we touch them. That’s because we haven’t hooked up the selection to the expandHeader method of the data model.  We could use a property to control this, but choose to connect this in C++.  In app.h, add a slot:

 

public slots:
    void onSelectionChanged(const QVariantList, bool);

 

In app.cpp, implement the slot:

 

void App::onSelectionChanged(const QVariantList indexPath, bool selected)
{
    if (indexPath.size() != 1 || !selected)
        return; // Not interested

    // Selected a header item!
    // Inform the database to change what it gives us
    ListView* list = dynamic_cast<ListView*>(sender());
    if (list)
    {
        FilteredDataModel* dm = dynamic_cast<FilteredDataModel*>(list->dataModel());
        if (dm)
        {
            int selection = indexPath[0].toInt();
            bool expand = true;
            if (dm->isHeaderExpanded(selection))
            {
                expand = false;
            }
            dm->expandHeader(selection, expand);
        }
    }
}

 

Finally, connect the ListView signal to this implementation of the slot:

 

App::App()
{
    qmlRegisterType<FilteredDataModel>("custom.lib", 1, 0, "FilteredDataModel");

    QmlDocument *qml = QmlDocument::create("main.qml");
    qml->setContextProperty("cs", this);

    AbstractPane* root = qml->createRootNode<AbstractPane>();
    if (root != 0)
    {
        ListView* list = root->findChild<ListView*>("myList");

        connect(list, SIGNAL(selectionChanged(const QVariantList, bool)),
                this, SLOT(onSelectionChanged(const QVariantList, bool)));

        Application::setScene(root);
    }
}

 We could add defensive code to ensure list is non-null before attempting the connect, but the connect will appropriately ignore this case.

 

Adjust the ListView header

If we run the code now, the headers expand and collapse. But we have two issues:

  • the headers look exactly the same as every header that is not selectable, so we might not know to click.
  • The headers are hard to select. They are small by default precisely because they are not normally selectable.

Let’s adjust the headers to be larger yet still have a different look from the selectable items beneath them.  In main.qml, override the HeaderListItem handling of ListView:

 

listItemComponents: [
    // Standard header height is too short to be selectable.
    // Use a larger font for the header 
    ListItemComponent {
        type: "header"
        Label {
            text: ListItemData
            textStyle {
                base: SystemDefaults.TextStyles.BigText
                fontWeight: FontWeight.Bold
                color: Color.White
            }
        }
    }
    // Default StandardListItem for "item" is fine
]

 

This is a simplistic look for the header, but serves the purpose.

 

Left to the reader…

 

If you select “hello_0” then collapse the header and expand it again, the selection is cleared.  The selection is part of the ListView, and we removed the item from the list view.  If you want to preserve the selection, the onSelectionChanged  handler would need to preserve the selection and restore it as appropriate if no other item selection has been made.

 

The header look and background look are simplistic.  These could be spruced up to give sparkle to the ListView.

 

The FilteredDataModel constructor hard-codes the underlying data model.  It also owns the underlying data model, creating it in the constructor and destroying it in the destructor.  The underlying data model could be passed to the filter through a property, or the filter could be converted to a templated class.  Defensive code should be added through-out to allow for m_fullDataModel being 0.

 

 

Source Code

The source code for this sample can be downloaded from the BlackBerry® project on Github® here: https://github.com/blackberry/Cascades-Samples/tree/master/filtereddatamodel

Comments
by Developer on ‎04-03-2013 09:10 AM

There is no zip file as the code was moved to github.

The correct link to the github sample would be

https://github.com/blackberry/Cascades-Samples/tree/master/filtereddatamodel

by Administrator on ‎04-03-2013 09:48 AM

Thanks, the links in the article should be fixed soon.

by Developer on ‎01-28-2014 08:12 AM

 

Files "HelloWorldDataModel.h and .cpp" are not present in filtereddatamodel repository.

Users Online
Currently online: 4 members 776 guests
Recent signins:
Please welcome our newest community members: