Danik
Danik

Reputation: 404

QObject properties initialized too late for binding to dynamically inserted properties

I'm implementing a system for syncing data from and to QML and QtScript using a QObject as the backing model and facade objects in QML, QtScript and C++, to simplify data synchronization between different subsystems.

The idea is that one could instantiate a facade object in QML and set a reference to the model QObject, and the facade gets properties added dynamically to allow binding to them in QML. So far it's working quite well, with one problem. The setter for the model QObject gets called too late, which makes bindings fail. If the bindings are done in Component.onCompleted instead it works as expected:

    StateQMLFacade {
        id: stateFacade
        stateObj: myStateObj; // accessible as a context property
    }
    Text {
        id: testText
        text: "myInt: " + stateFacade.myInt // Prints "myInt: undefined"
        Component.onCompleted: {
            // With this line the binding works as expected
            testText.text = Qt.binding(function() { return "myInt: " + stateFacade.myInt })
        }
    }

The facade item is a subclass of QQmlPropertyMap, and gets all properties of the model object added dynamically in the WRITE function for the property stateObj, and connects notify signals in both directions to sync the two. Which is why the properties do not exist when the bindings are attempted before stateObj is set.

The QML is created using QQmlComponent::create(QQmlContext* context)

When investigating further I found that setters for QObject* properties get called after all other bindings are done. For some reason properties that are integers etc. get set in QQmlComponentPrivate::doBeginCreate(), while properties that are QObject*'s get set in QQmlComponentPrivate::completeCreate()

One way to solve it is to use a delayed Qt.binding like in the example above, or delay the instantiation of the components that bind to the facade object, but I want the mechanism to be seamless for the users. It's all working well except for this last timing/order issue.

I am wondering if there is any way to get more control over the order things are initialized in, or if the issue can be solved in some other way? Maybe it's possible to instantiate and initialize the facade early, and somehow inject it into the other QML?

This is using Qt version 5.15.

Any help and insight is appreciated.

Upvotes: 0

Views: 378

Answers (1)

Stephen Quan
Stephen Quan

Reputation: 26309

One of the issues you're dealing with is you have a notion of order:

  1. myStateObj
  2. StateQMLFacade
  3. testText

However, when you are doing property binding, you can never be sure that things will be initialized in that prescribed order and once. So, imagine the property binding went in this order:

  1. StateQMLFacade
  2. testText
  3. myStateObj

You will need to ensure that your components deal with the initialization state and may go thru periods of temporary undefined values, and, as well as that, omit corresponding signals so that the thing finally settles, i.e.

  1. StateQMLFacade
  2. testText but initially undefined
  3. myStateObj
  4. StateQMLFacade reacts to stateObj change and emits myIntChanged
  5. myIntChanged emit
  6. testText updated again

Lastly, the undefined state can stop the property binding to continue to evaluate, so, it's best to absorb undefined values with default values, e.g.

    Text {
        id: testText
        text: "myInt: " + (stateFacade.myInt ?? "")
    }

Upvotes: 0

Related Questions