PDA

View Full Version : model/view design advice appreciated



zaphod.b
29th May 2013, 18:45
Hi all,

I have a custom item model that wraps around QuaZip and is associated with a QTreeView. Think of a ZIP client of sorts. In the view, I am triggering actions on a selection of model indexes that need to act on the according QuaZipFile contents.

On first impulse, I wanted to iterate the model indexes in the view and request QIODevice data from the model for each index. However, I'd need to Q_DECLARE_METATYPE QIODevice or QuaZipFile, and neither provides a copy c'tor as required. Indeed, Qt strongly discourages copy c'tors on *all* QObjects.

Of course I could return the file contents as QByteArray data. This, however, would duplicate potentially very large data when all I need might be to stream the data from one device to another - think of exporting files from the archive.

If any of you could come up with an elegant, well-behaved solution, it would be very much appreciated. Using Qt5 btw. Thanks for your consideration.

anda_skoa
30th May 2013, 13:34
One thing to keep in mind is that the model is not the data but an interface to the data.
I.e. the data can usually also be accessed directly.

I don't know about QuaZip, but for example if you consider a model operating on a normal file system, each file can be addressed by QModelIndex through the model or by filename through the file system API.

So if the model can map a QModelIndex to a filename, the program can use any of the usual file access facilities to work with the data.

This approach is used by QFileSystemModel, which has methods to map its indexes to all kinds of file related data types, e.g. filename, fileinfo, filepath, etc.

Another approach is to have data access methods in the model, e.g. a method that takes a QModelIndex and returns a QFIle*.

Cheers,
_

zaphod.b
31st May 2013, 17:46
I may have managed to obscure my intents by getting too abstract.... :o

My model wraps the data alright. I overloaded the data() method and introduced custom roles to return e.g. zip paths. This is all fine.

That which isn't: I wanted to add one additional role, say DeviceRole, to return a QuaZipFile, or even better, a QIODevice (its base class), through the same data() method - for coherence, if you will. To do so, the data returned for this role must cast to QVariant, data()'s return type. As I understood, this requires to Q_DECLARE_METATYPE the custom type in question, which in turn requires the custom type to provide public default c'tor, d'tor, and copy c'tor. However, QIODevice inherits QObject, and QObjects should not provide a copy c'tor (and both QIODevice and QuaZipFile don't), so I'm in a catch-22 here. Please correct me if I'm wrong in any respect.

I agree this is a very specific home-brewed sort of a problem...


This approach is used by QFileSystemModel, which has methods to map its indexes to all kinds of file related data types, e.g. filename, fileinfo, filepath, etc.

Thanks for pointing me to QFileSystemModel. I had a look at its sources: it defines 4 custom roles, of which all but FileIconRole = Qt::DecorationRole relate to types that are represented by QVariant out of the box. What I don't understand is how QIcon casts to a QVariant? But then, QIcon is not a QObject, so the approach sketched out above might have been used (neither in header nor implementation though).


Another approach is to have data access methods in the model, e.g. a method that takes a QModelIndex and returns a QFIle*.

This is what I did, but I deem it inelegant at the least with respect to the model/view paradigm.

anda_skoa
1st June 2013, 10:59
I guess the question is why you want to return a file through data().

Does the code that reacts to your selection changes not have access to your model type? I.e. you can't do something like


m_model->fileForModelIndex(modelIndex);

because you don't have access to m_model as a pointer of your model type?

And you also don't have access to the actual data, as in you can't do


QString filename = modelIndex.data(MyRoles::FilenameRole);
... m_zipFile->file(filename);

because there is no mappable identifier?

Cheers,
_

zaphod.b
2nd June 2013, 16:39
I guess the question is why you want to return a file through data().

Indeed it is. I may have insufficient grasp of model/view, or some kind of mental block. But as I understand it, calling data() is the preferred means to retrieve any data from the model.

To have the model return a device through data(index, DeviceRole) would allow me to make as weak assumptions as possible (see below).


Does the code that reacts to your selection changes not have access to your model type? I.e. you can't do something like


m_model->fileForModelIndex(modelIndex);

because you don't have access to m_model as a pointer of your model type?

Yes, I currently do not explicitly hold a pointer to the model within the view but access it via view->model() - else I'd lose flexibility, wouldn't I? Explicit knowledge of the model within the view I consider a stronger assumption than to provide data of a certain custom role.


And you also don't have access to the actual data, as in you can't do


QString filename = modelIndex.data(MyRoles::FilenameRole);
... m_zipFile->file(filename);

because there is no mappable identifier?

The zip archive currently is private to the model.
As you wrote before, the model is an interface to the data. So the data can be accessed bypassing the model. But I'd much rather not for style, flexibility and maintainability reasons.

I should add that future extensions will likely associate other data with the same view. Other than this data "contains" devices, I do not know at this time.


PS: I very much appreciate this discussion, as it helps me clarify my thoughts and requirements. It is, however, somewhat academic I have to admit... unless there is a way to return device data, that is :)

anda_skoa
3rd June 2013, 12:50
But as I understand it, calling data() is the preferred means to retrieve any data from the model.

Yes, correct, for any data the view or a delegate needs from the model.
My initial interpretation was that you wanted to have the device for some processing of sorts, in which case direct access to the data is usually the way to go.
If you need it in a delegate or for some custom view visualizations that data() would be my choice as well.



To have the model return a device through data(index, DeviceRole) would allow me to make as weak assumptions as possible (see below).

You can always return a QIODevice*. What kind of view related processing do you plan on doing with the data?



Yes, I currently do not explicitly hold a pointer to the model within the view but access it via view->model() - else I'd lose flexibility, wouldn't I? Explicit knowledge of the model within the view I consider a stronger assumption than to provide data of a certain custom role.

Right. As written above, I had initially assumed you wanted to do some processing depending on user actions. If you indeed need the data in the view, then getting it from the model is the best choice.



The zip archive currently is private to the model.
As you wrote before, the model is an interface to the data. So the data can be accessed bypassing the model. But I'd much rather not for style, flexibility and maintainability reasons.

If this is model/view internal then this doesn't make much sense indeed. Sorry for the confusion, "trigger action on selection" sounded to me like doing data processing based on user interaction, something that usually falls outside of the model/view components.



I should add that future extensions will likely associate other data with the same view. Other than this data "contains" devices, I do not know at this time.

Hmm, and your data does not have any interface to access those "devices"? or no way of communicating a "handle/identifier"?

Cheers,
_

zaphod.b
3rd June 2013, 14:15
Yes, correct, for any data the view or a delegate needs from the model.
My initial interpretation was that you wanted to have the device for some processing of sorts, in which case direct access to the data is usually the way to go.
If you need it in a delegate or for some custom view visualizations that data() would be my choice as well.

Ah light falls into darkness at last! :cool:
So you basically say that data() should only be used for view/delegate related data, whereas other data should (or at least may) be returned by specialized accessors other than data()? This is indeed different from my approach, which is to return all data through data(), no matter if view/delegate related or not.

Yes, if I follow your approach then there is no (technical) problem at all.


What kind of view related processing do you plan on doing with the data?

The problem initially arose when I wanted to drag items from the view and drop its related device data e.g. in a file manager outside my application.



Right. As written above, I had initially assumed you wanted to do some processing depending on user actions. If you indeed need the data in the view, then getting it from the model is the best choice.
[...]
If this is model/view internal then this doesn't make much sense indeed. Sorry for the confusion, "trigger action on selection" sounded to me like doing data processing based on user interaction, something that usually falls outside of the model/view components.

Sorry but is there a contradiction here? If there is a view, user interaction normally happens with this view, doesn't it?
Anyway, in your terminology, my drag example is a user interaction on a view's selection, isn't it?


Hmm, and your data does not have any interface to access those "devices"? or no way of communicating a "handle/identifier"?

What I by now assume you mean by "data" (in my example the QuaZip object) certainly does have such accessors, and so will any future "device container", by a façade if need be.

anda_skoa
3rd June 2013, 17:32
So you basically say that data() should only be used for view/delegate related data, whereas other data should (or at least may) be returned by specialized accessors other than data()?


Yes, or even bypassing the model and going directly for the actual content API.
QAbstractItemModel (and subclasses) provide an abstraction only necessary to be able to re-use the same views and delegates on any kind of content.

Applications sometimes even have multiple model implementations working on the same context, e.g. only providing access to a subset, etc.
Maybe consider the model to be an adapter between the actual content API and the view. It shouldn't "hold" the content, merely provide an interface to it that is digestable by Qt's views.



The problem initially arose when I wanted to drag items from the view and drop its related device data e.g. in a file manager outside my application.

Sorry but is there a contradiction here? If there is a view, user interaction normally happens with this view, doesn't it?
Anyway, in your terminology, my drag example is a user interaction on a view's selection, isn't it?

Ah, I see. I was mostly thinking in terms of handling signals on of the view or item selection model, in which case the processing usually happens outside the view.
Drag&Drop does indeed require some handling inside the view.

Hmm. I guess it mainly boils down to whether the Qt item view you have will be the only user interface to the content.

I usually try to avoid implementing any kind of processing inside a view or slots connected to its signals but instead delegate to some other object that knows nothing about model/view.
The view or related slot would then only "dereference" the model indexes, basically create non-model/view input for the actual processing class.

But it of course always depends on the scope of the application, how likely it is to get different UI, etc.

Cheers,
_