Itemviews data redundancy

Encouraged by some recent posts on Qt Centre I decided to point out a common issue when dealing with Qt model-view framework.

Many people often treat models as a burden that needs to be carried to be able to display some data using Itemviews. One needs to perform a tedious task of implementing the QAbstractItemModel interface or filling one of the ready made models such as QStandardItemModel with data. Most often the model needs to represent some data which is contained in some structure and it is not rare to spot people write code similar to this one:

struct MyStruct {
  int x;
  int y;
};
 
class Model : ... {
  //...
  QList <MyStruct> export() const { ... }
  void import(const QList <MyStruct> &){  ... }
  //...
};

What happens here is that they provide export and import functions to convert between their own storage format and the model. It’s a similar thing if they instantiate QStandardItems and feed it with data from some datasource. In both cases the data is duplicated – it is kept both in the model and in the external container.

Instead the model should be perceived only as an interface to an existing data – no matter if it is kept inside or outside the model object, a good example being QSqlTableModel and family – however the data is cached within the model, it is really kept on some remote server.

Instead of the above snippet of code, the skeleton could look like the following:

class Model : public QAbstractItemModel {
public:
  Model(QList<MyStruct> *dat, QObject *parent=0)
  : QAbstractItemModel(parent){
    m_data = dat;
  }
  QVariant rowCount(const QModelIndex &parent) const {
    if(parent.isValid() || m_data.isNull()) return 0;
    return m_data->count();
  }
  //...
private:
  QPointer<QList <MyStruct> > m_data;
};

As you can see the model operates on an external list of items and as long as the list is valid, it will return correct data. Of course the same applies to the rest of the interface – one can add rows and modify data kept in the list.

The only problem that remains is that the list can be modified outside the model, causing all components using the model (views included) to not update themselves upon changes in the list. Fortunately there are ways to overcome it.

The first good solution is to use a data source that can emit changes made to it using Qt signals. Then one can connect those signals to the model to be able to emit proper signals from the model to its environment.

The second solution is to disallow access to the data source outside the model, but then it makes more sense to embed the source within the model itself, either with “has-a” or “is-a” relation (remember, multiple inheritance is a beautiful thing 😎 ). It is probably the best solution if you fully control the data source and you can modify the code that uses it. It is worth mentioning that you don’t have to use the methods provided by QAbstractItemModel to access the data if it feels odd or difficult in your situation. You can define custom methods to access the data through the model – both in read and write mode (for example using MyStruct as the carrier). As long as you emit proper model signals from those methods, your views will continue to work properly.

The third option is to trigger manual or periodic updates of the model – for flat models things like row and column count can be cached and compared against the real source. If they are out of sync, one can try emiting one of generic signals – layoutChanged() or modelReset() to save the situation. Just remember that calling modelReset() will cause all your views to loose selections and item visibility. For simpler models (or better data sources) it might be possible to trace exact changes in the source and emit proper signals upon detecting changes.

Remember, the more complex the data gets, the easier to fall out of sync when using two separate data structures. Save yourself time and nerves and use a single data set instead of two whenever you can, even if it doesn’t seem obvious from the start how to do it. A good design always pays off.

Tags: , ,

5 Responses to “Itemviews data redundancy”

  1. niko says:

    Why do you use a QPointer for m_data? Reading the QPointer-docs it can only be used for QObjects.

  2. wysota says:

    Yes, you are right. It’s just a crude example and QPointer is to point out that the list might be destroyed somewhere in the process. You can use any non-hijacking smart pointer class here or even a regular pointer if you are willing to take the risk. Another possibility (which was my original thought then abandoned to show the volatile nature of the data source) is to use a reference instead of a pointer.

  3. Sunil Thaha says:

    The code snippet font is too small to be read on my firefox, windows
    Could you do something about it.

  4. Laurent says:

    Is there any new approach that is recommended for this problem in the mean time? As you said: the only problem that remains is that the list can be modified outside the model, causing all components using the model (views included) to not update themselves upon changes in the list.
    Solution 1 raises the question on how to translate a change in the data source in the index used by the model to represent this piece of information to be able to do something like emit(dataChanged(index, index));
    Solution 2 does, as you said, not make any sense to not embed the source within the model itself.
    Solution 3 is kind of not very efficient if we have a rapidly changing model on some datas of the source.

Leave a Reply