Corristo
Corristo

Reputation: 5520

Using QDeclarativeContext hierarchies with QDeclarativeView

Right now in my C++/QML application, even though every QML view only uses a subset of all available properties, all of them are exposed via QDeclarativeContext::setContextProperty in the root context of the QDeclarativeEngine of the QDeclarativeView displaying the QML files - which is kind of ugly.

According to the Qt documentation,

Additional data that should only be available to a subset of component instances should be added to sub-contexts parented to the root context.

So I'd like to do that. However, I have not found further documentation or information how to actually use sub-contexts. Unfortunately, simply creating a new sub-context is not enough.

Example:

I have a very simple QML file

import QtQuick 1.0
Rectangle {
    visible: true

    width: 800
    height: 600

    Text {
        text: message.text
        anchors.centerIn: parent
    }
}

that accesses a context property called message, which is set inside a sub-context of the root context of the engine of a QDeclarativeView in main:

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    QWidget widget{nullptr, Qt::FramelessWindowHint};
    widget.resize(800, 600);

    MessageHolder message{"Hello World"};

    // ceeate the new context and add the message to context properties
    QDeclarativeContext message_context{view.engine()};
    message_context.setContextProperty("message", &message);
    
    // what needs to go here so that the new context is actually used?

    view.setSource(QUrl{"path/to/main.qml"});
    view.show();
    widget.show();

    return app.exec();
}

The MessageHolder used above is a simple class that does nothing but provide a constant QString that is set on construction of such an object:

class MessageHolder : public QObject {
    Q_OBJECT
    Q_PROPERTY(QString text READ text CONSTANT)
public:
    MessageHolder(QString text);

    QString text();
private:
    QString _text;
};

As already stated above, this unfortunately doesn't work and QML emits the warning

/path/to/main.qml:9: ReferenceError: Can't find variable: message`

If I instead add the message property to the root context everything works fine. What do I need to do between creating the context and setting the source of the view to make this work?


More in-depth explanation of what I'm really trying to do

In my application I use a MVVM architecture where each QML view has a corresponding C++ viewmodel (in the sense of MVVM and not the Qt terminology).

At any given moment there is only one of the many different viewmodels active, however all of them are instantiated during startup of the application and then exposed to QML via context properties.

The core issue I have with this is the need for a unique name for each of the viewmodels, thus also strongly coupling the QML views to a single viewmodel. It would be much nicer to have a single generic context property with the name viewmodel instead.

However, here is the problem: When a new viewmodel becomes active I have to re-set the viewmodel context property to the new viewmodel either before or after changing the QML view. But changing the context property leads to reevaluation of all bindings currently active. Therefore, when I first change the context property and then the view, the old view will try to update even though the new viewmodel doesn't have the required properties, and if I first change the view and then the context property the new view will evaluate all of its bindings when it is loaded, but the necessary properties are not yet available since the context property hasn't been updated yet.

Visually this is not a problem, since in the first case the now broken view is about to be unloaded anyway and in the second case the new view will immediately reevaluate all properties once the new context property is set. Nevertheless, in either case QML will emit a warning because of missing properties, which is not nice firstly because it clutters the log files and secondly because I treat warnings as fatal errors in debug builds via a Q_ASSERT in order to spot bugs in QML views or viewmodels early in my continuous integration system without manual inspection.

But then I stumbled across the following fact in the Qt documentation:

While QML objects instantiated in a context are not strictly owned by that context, their bindings are. If a context is destroyed, the property bindings of outstanding QML objects will stop evaluating.

This is exactly what I need in order to stop QML from reevaluating now invalid bindings in the dying view. Therefore I'd like to set the viewmodel context property in a sub-context which I'll destroy and recreate whenever the view changes.

Upvotes: 0

Views: 337

Answers (1)

dtech
dtech

Reputation: 49319

Well, instead of setting the property to the root (object) context, you can add it to the context of a particular object that is not the root object.

You can get the context for any object using this static method:

auto ctx = QQmlEngine::contextForObject(someQObjectPtr);

Each QML object has its context, obviously if you create some arbitrary context as you do in your code, that doesn't correspond to anything, understandably it will not work as expected.

Although what you want to do sounds a little backward.

What I would do is to have another model or at least a basic list of all models, then have a property model activeModel and set that to whichever model I want to display currently, and use activeModel for the view, thus changing the active model will automatically update the view. If you want to be more explicit you can manually destroy the view before changing the model.

Furthermore, as this recent question indicates, object destruction itself doesn't happen immediately in QML, it is delayed and that will still cause reevaluations for bindings of objects that are supposedly no longer relevant. So your original plan might not even work, aside from being a needless patch for a design problem that you shouldn't have in the first place.

All in all, I would not recommend using context properties for anything more dynamic than exposing a single object to QML. It is just not scalable and maintainable when you have a bunch of stuff, each of it requiring its own line of code to register and its own name.

UPDATE:

Additional data that should only be available to a subset of component instances should be added to sub-contexts parented to the root context.

You are interpreting this wrongly. It doesn't tell you to go about and manually create your own contexts. I am not familiar with any means to manually specify which context you want from QML, the context is implicit - the context you are in - the context of the current object, or if that fails to find the property, then search the contexts all the way down to the root context. It doesn't mean that you should create context trees willy-nilly and then deleting them, pulling the rug under the qml engine.

Thus as my initial answer stated, if you don't want to register properties in the root context, you should pick the context of a branch. Then that property will only be visible to objects in that sub-tree. However this will not really solve the problem you are having, at least not in a reasonable, efficient and elegant way.

The solution, at the risk of repeating myself, is strikingly simple and obvious - collapse the view, change the model, rebuild the view. There is no need to try and do impossible things you have zero knowledge of. The only way to destroy a context is to destroy its object, when the view is destroyed there will be no objects to update their bindings with invalid data. Crudely deleting QML objects from C++ will not help you to avoid QML's design limitations in any way, it will only cause you application to crash.

This simple example, built around a generic object model, is a clear illustration that those "residual" bindings are just a product of QML's internal design, which in my personal experience is lousy with bugs, flaws and design limitations. I myself have had my (rather unfair) share of struggle and toil with that, but if experience has taught me anything, it is to try and work around it, because sometimes the way QML works has nothing to do with logic and reason, so how reasonable and logical your expectations are (if at all in this particular case) is entirely irrelevant.

  List {
    id: models
    List {
      property Component delegate : Rectangle {
        width: 200
        height: 50
        color: object.apple
      }
      QtObject { property color apple: "green" }
      QtObject { property color apple: "red" }
      QtObject { property color apple: "yellow"}
    }
    List {
      property Component delegate : Rectangle {
        width: 200
        height: 50
        color: "red"
        Text {
          anchors.centerIn: parent
          text: object.pepper
        }
      }
      QtObject { property string pepper: "sweet" }
      QtObject { property string pepper: "spycy" }
      QtObject { property string pepper: "hot" }
    }
  }

  Row {
    ListView {
      width: 200
      height: 200
      model: models
      delegate: Rectangle {
        width: 200
        height: 50
        color: "grey"
        Text {
          anchors.centerIn: parent
          text: index
        }
        MouseArea {
          anchors.fill: parent
          onClicked: {
            currentModel.model = null // remove line and suffer
            currentModel.model = object
          }
        }
      }
    }
    ListView {
      id: currentModel
      width: 200
      height: 200
      model: null
      delegate: model ? model.delegate : null
    }
  }

The implementation is a list of lists, or a model of models if you will. Each model in the model list comes with its own delegate, and objects with their own set of properties. There are two list views, the first lists all available models, the second lists the contents of the active model, if any. Without the currentModel.model = null line, every time the current model is switched you will get console errors about undefined properties, because that's just how QML works. It is actually extremely lucky that in this case the solution is as simple as nulling the model before switching to another, with that line everything is just fine, the view elements are destroyed before the model is switched, without it the items linger on for an event loop cycle, which is enough to produce the errors, and those don't come from the current list view items, but from the ones that are going away, so even if you deleted the data in C++, those objects, being managed by the list view, would have lingered on enough to make trouble, and in the case of a delete it would not be console errors, but a hard application crash.

Upvotes: 1

Related Questions