Enginio C++ Examples - Cloud Address Book
The Cloud Address Book example shows sorting, filtering and the full text search functionality
This example explains how to use the full text search feature of Enginio and how to sort and filter data showed from the EnginioModel. To present that. a simple address book like application, will be created. This example doesn't cover security or multi-user management, for such topics please refer to Social Todo example.
Preconditions
To start the example, a backend id and a backend secret have to be provided to an existing and configured Enginio backend. The backend can be created using dashboard, the Cloud Address Book pre-configured backend can be chosen.
Backend description
We recommend to use pre-configured backend, because it contain already all data and structures that are needed to run this examples, but it is not difficult to create the backend from scratch too. The backend should contain one custom object type "objects.addressbook" having properties:
- firstName
- lastName
- phone
- address
All properties are of string type and have to be indexed, because only indexed properties will be searched by the full text search.
Application design
The application's ui mainly consist of a table showing all addresses and a text filed where a query can be typed. A user should be able to sort addresses or highlight an address containing a specified phrase.
Implementation
From a developer point of view the application from a few simple components:
- EnginioClient which encapsulates all information that are needed to keep connection to the backend
- EnginioModel which queries all addresses
- QSortFilterProxy which sorts the data
- QTableView which shows the data
The first thing to be done is to construct connection to Enginio service. We need to specify backend id as well as backend secret.
client = new EnginioClient(this); client->setBackendId(EnginioBackendId);
The second step is to create EnginioModel which queries all data from the backend. The query is quite simple, it specifies an object type of which all objects needs to be returned.
model = new AddressBookModel(this); model->setClient(client); QJsonObject query; query["objectType"] = QString::fromUtf8("objects.addressbook"); model->setQuery(query);
EnginioModel can sort or filter data only initially, which means that for example a newly added item will not be placed correctly. To solve the problem QSortFilterProxy has to be used.
sortFilterProxyModel = new QSortFilterProxyModel(this); sortFilterProxyModel->setSourceModel(model); tableView->setSortingEnabled(true); tableView->setModel(sortFilterProxyModel);
Now it is a time to look deeper into EngnioModel. EnginioModel should define data roles.
enum Roles { FirstNameRole = Enginio::CustomPropertyRole, LastNameRole, EmailRole, PhoneRole, AddressRole }; QHash<int, QByteArray> AddressBookModel::roleNames() const { QHash<int, QByteArray> roles = EnginioModel::roleNames(); roles.insert(FirstNameRole, "firstName"); roles.insert(LastNameRole, "lastName"); roles.insert(AddressRole, "email"); roles.insert(PhoneRole, "phone"); roles.insert(AddressRole, "address"); return roles; }
and as always data() setData() functions have to be overridden to provide Qt::DisplayRole in the way it is needed to nicely cooperate with QTableView.
QVariant AddressBookModel::data(const QModelIndex &index, int role) const { if (role == Qt::DisplayRole) { // we assume here that column order is constant and it is the same as in AddressBookModel::Roles return EnginioModel::data(index, FirstNameRole + index.column()).value<QJsonValue>().toString(); } if (role == Qt::FontRole) { // this role is used to mark items found in the full text search. QFont font; QString id = EnginioModel::data(index, Enginio::IdRole).value<QString>(); font.setBold(_markedItems.contains(id)); return font; } return EnginioModel::data(index, role); } bool AddressBookModel::setData(const QModelIndex &index, const QVariant &value, int role) { bool result = role == Qt::EditRole ? EnginioModel::setData(this->index(index.row()), value, FirstNameRole + index.column()) : EnginioModel::setData(this->index(index.row()), value, role); // if the data was edited, the marked items set may not be valid anymore // so we need to clear it. if (result) _markedItems.clear(); return result; }
Usage of QTableView requires to provide columns headers, so they can be shown in the user interface
int AddressBookModel::columnCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : 5; } QVariant AddressBookModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { switch (section) { case 0: return QStringLiteral("First name"); case 1: return QStringLiteral("Last name"); case 2: return QStringLiteral("Email"); case 3: return QStringLiteral("Phone number"); case 4: return QStringLiteral("Address"); } return QVariant(); } return EnginioModel::headerData(section, orientation, role); }
Integration of the full text search is the last step in this tutorial. The goal is to highlight items that contains a given phrase. The highlighting is done by data(), which returns a bold font for Qt::FontRole, if an item is matching the search query. This example for simplicity assumes that matching items count is not big and can be kept in a QSet, which would be recreated on each search.
To do a full text search, a JSON query needs to be constructed.
// construct JSON object: // { // "objectTypes": ["objects.addressbook"], // "search": { "phrase": "*PHRASE*", // "properties": ["firstName", "lastName", "email", "phone", "address"] // } // } QJsonObject query; { QJsonArray objectTypes; objectTypes.append(QString::fromUtf8("objects.addressbook")); QJsonArray properties; properties.append(QString::fromUtf8("firstName")); properties.append(QString::fromUtf8("lastName")); properties.append(QString::fromUtf8("email")); properties.append(QString::fromUtf8("phone")); properties.append(QString::fromUtf8("address")); QJsonObject searchQuery; searchQuery.insert("phrase", "*" + search + "*"); searchQuery.insert("properties", properties); query.insert("objectTypes", objectTypes); query.insert("search", searchQuery); }
The query contains "objectTypes" property (mark 's' at the end) which is an array of all object types which have to be searched. In this case, it is only one type "objects.addressbook". Next "search" property has to be filed. It is an object that is required to have "phrase" property. The property is exactly the search phrase that has to be found. Please mark '*' characters, without them the full text search would find only full words. To avoid founding substrings, for example in on object id, which is not visible for a user the search has to limit scanned property list, by "properties" array.
When the search query is constructed it is enough to call fullTextSearch():
_searchReply = client()->fullTextSearch(query); QObject::connect(_searchReply, &EnginioReply::finished, this, &AddressBookModel::searchResultsArrived); QObject::connect(_searchReply, &EnginioReply::finished, _searchReply, &EnginioReply::deleteLater);
The result will be delivered to the searchResultsArrived slot. In which from result array all objects ids are gathered in markedItems set.
void AddressBookModel::searchResultsArrived() { // clear old marks. _markedItems.clear(); // update marked ids. QJsonArray results = _searchReply->data()["results"].toArray(); foreach (const QJsonValue &value, results) { QJsonObject person = value.toObject(); _markedItems.insert(person["id"].toString()); } QVector<int> roles; roles.append(Qt::FontRole); // We do not keep id -> row mapping, therefore it is easier to emit // data change signal for all items, even if it is not optimal from // the performance point of view. emit dataChanged(index(0), index(rowCount() - 1, columnCount() - 1) , roles); _searchReply = 0; emit searchFinished(); }
Files: