How to retrieve a QModelIndex for a custom data object

The QAbstractItemModel interface provides two things: structure and data.

The structure of the model is defined by how you implement the rowCount, columnCount, index and parent methods.

The data in represented in the model is accessible through the data method. data allows the use of custom roles to retrieve objects of custom types. Rows in the EntityTreeModel can represent things like emails or addressee objects.


Item item = model->data(index, ItemRole).value<Item>();
Message::Ptr email = item.payload<Message::Ptr>();

The nice thing about data is that it works even through proxy models which know nothing about the email types. So the model object in the example could be an EntityTreeModel, or any of many proxy models that we have these days.

But what if we already have the email, but we need to get a QModelIndex that represents it in the model? Reasons we need that include storing the open email folder on application close and restoring it on application start. (By the way: KViewStateSaver)

The model itself knows how to map between them, so the tempation would be to implement EntityTreeModel::indexForCollection. In fact, that’s exactly what CollectionModel, the predecessor to EntityTreeModel did. The reason that is a bad thing is that it only works on the base model. It doesn’t work through proxy models. If you use model-view flexibly in your API, you will most likely just have a QAbstractItemModel pointer at the point where you want to get the correct QModelIndex. You won’t know if it is the base model or a proxy model on top of the base model.

Someone might try to always have an instance of the base model around, use its indexForCollection method, and then try to map the result to the proxy at hand. That requires either putting the knowledge of the proxy chain in all methods that use indexForCollection (and maintaining that when another proxy is added or removed), or introspecting it using KModelIndexProxyMapper. Neither is a very good solution.

One way around that is to use the match() method. match allows queries for custom roles too, but it requires using Qt::RecursiveMatch, performing a linear search over the model. If proxies are used, the call is even more expensive.

At first I wrote a custom match implementation to forward the calls through proxies making that cheaper, and implemented it in the base model to do return a fast mapping. The problem is that it requires an arcane use of match to work, and that it doesn’t scale. Each proxy needs to have the custom match boilerplate, and if one proxy is added which does not have that implementation, the solution breaks down and I get bug reports.

The solution is only slightly more elegant.

Applications do know that when they deal with a QAbstractItemModel they are dealing with an abstraction of the base model. In Akonadi applications, they are either dealing with an EntityTreeModel or a proxy on top of a EntityTreeModel. By adding a static method to the EntityTreeModel, we can use a private implementation to create an index for the collection in the EntityTreeModel, and do all the mapping required to convert that index to an index in the proxy model.

That’s why EntityTreeModel has indexForCollection and indexesForItem as static methods.

However…

Static methods are bad for unit testing. If applications are using

idx = EntityTreeModel::indexForCollection(model, cId);

then the model at the base must be an EntityTreeModel. We can’t swap it out for a FakeEntityTreeModel for the purpose of unit testing. Well, actually with a bit more introspection I will be able to do just that, but then EntityTreeModel will have knowledge of the FakeEntityTreeModel. That is also not ideal, but it’s the best compromise so far to enable the back-modelling of objects to QModelIndexes.

Speaking of unit testing model-view, I will be speaking of unit testing model-view at Akademy this year.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


%d bloggers like this: