How are QML property dependencies determined? (and how to manipulate them)

601 Views Asked by At

A property that is bound to an expression is updated when something in the expression changes. This is called a dependency.

EDIT:

To clarify:

  • I'm interested in details on how Qt determines a list of dependencies
  • Dependencies on simple bindings such as x: y are more or less obvious
  • The question is about less obvious cases such as x: myItemId["y"] and x: myFunction(z) where myFunction(p) { if (p) return myItemId.y }

Sometimes QML engine is able to detect change even if the expression is a function call without arguments, other times it cannot do that (for example mapToItem(item,0,0).x).

Another example of imperfection is that setting JS array item value without reassigning the array itself doesn't normally produce onXxxxxChanged signal or update anything referring to that array value.

An expression with unused result (x: {myForcedDependency; return myActualCalculation()}) is sometimes suggested to force a dependency.

According to this KDAB article and Qt source code, a binding expression is not only evaluated but any properties "accessed" during that are "captured" in something called a "guard", then every guard properties onXxxxxChanged() signals are connected, but actual details of this process are unclear.

So my questions are:

  • Are there any defined rules of dependency resolution?

  • How does it really work?

    • How deeply does QQmlEngine/V8 scan "accesses" into functions called by the binding expression and what may prevent it from doing that?
    • Is dependency-detection only based on the first attempt at property resolution?
    • Are all possible code paths checked even if execution never reached there yet?
      • Are non-trivial accesses determined in those cases, such as object["property"] syntax?
      • What if some unexecuted code is (currently) erroneous (and does not produce an error but cannot be properly analyzed)?
  • How can the dependency resolution process be influenced?

    • Is there a way to avoid or block a dependency?
      • As far as I understand an intermediate "filter" property that only actually changes its value when it's necessary to update is the intended way, correct?
    • Is there an intended way to force a dependency?
      • Is manually emitting "XxxxxChanged" signal the correct/supported way to force an update?
      • Is adding an unused reference a legal/intended way to do it or undefined behavior based on the current implementation quirk?

Any information would be useful, although I did read the official documentation on QML properties, QML bindings and JavaScript expressions and didn't find any concrete explanation - if you refer to the official documentation please quote relevant parts.

Please note that I'm not asking you to test if any of this works on your system, but if it's supposed to work - if it can be relied on

2

There are 2 best solutions below

3
On

It makes more sense if you just think of bindings as connected signals. If you have something like this:

property int x: y

It's just like doing this in C++:

connect(this, &SomeClass::yChanged, [this]() { x = y; });

The same goes for expressions:

property int x: y + z

would be equivalent to:

connect(this, &SomeClass::yChanged, [this]() { x = y + z; });
connect(this, &SomeClass::zChanged, [this]() { x = y + z; });

And the same with function calls:

property int x: someFunc()
function someFunc() {
    return y;
}

The only time bindings don't update is when there is no onChanged signal to connect to, or the onChanged signal doesn't get emitted for whatever reason.

property int x: cppObject.invokable()

In the above case, the only property that x is able to connect to is cppObject. If invokable references other properties, those won't be connected to x and therefore the binding won't update.

property var array: [1, 2, 3]
property int x: array[0]

function updateArray() {
    array = [2, 4, 6]
    arrayChanged()  // Manually call the onChanged signal to update `x`
}

var properties do not notify by default (for some reason). So in this case, we have to manually call the changed signal, but then the binding will still work.

4
On

For a property var, onChanged is emitted only when there is a direct assignment to the var itself, not to a property of some object it refers to. This also excludes modification of array contents, as JS arrays are JS objects.

This is consistent with QML being a JS extension. In JS you can modify prop in this code, because const only means variable will always refer to the same object:

const variable = { prop: 'value' };

Just like only direct assignments to const variables are regarded as change attempts when JS enforces const, QML only emits onChanged on direct assignments to a property var.

Coming from C++, I like to compare JS variables with object value to pointers:

SomeClass *variable = new SomeClass();
SomeClass *const variable = new SomeClass();  //const pointer to mutable object

Again, a change in the referred object is not regarded as a change in the variable.