1

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

Jack White
  • 896
  • 5
  • 7

2 Answers2

1

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.

JarMan
  • 7,589
  • 1
  • 10
  • 25
  • Thanks for your answer. My question was supposed to be more about how complex expressions dependencies are determined - that is, which variables Qt considers dependencies. I edited the question to clarify that. – Jack White Feb 22 '21 at 19:00
  • There are evidently cases when QML is unable to correctly capture dependencies. I cannot easily isolate them from the rest of my huge project, but there appear to be cases where complex functions dependent on context properties do not cause updates even though `xxxxxChanged()` signals for those properties are emitted. It may be because the context initialization code ran after dependencies were determined or something like that. I'm trying to first understand how all of this is supposed to work before trying to blindly change things around to fix it. – Jack White Feb 22 '21 at 19:09
  • Most `var` properties do in fact notify - example: `Button{ property var p:0; text:p; onClicked: p=1 }`. It is only some types that can be assigned to `var` don't notify or aren't captured for some reason. Array items not causing array update signal is a known quirk of Qt-javascript relationship - see answer to this question: https://stackoverflow.com/questions/19583234/qml-binding-to-an-array-element/19653693 – Jack White Feb 22 '21 at 19:13
0

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.

really
  • 93
  • 7
  • Thank you for answering. Since then (question was asked 2 years ago) I did understand about as much myself. However what I would really like to know is how it really works. For example, if i bind to a JS expression that produces an error in non-evaluted branch (such as `y: (a == null) ? 0 : a.something.something`) and `a` happens to be `null` when loading, a change in value of `a.something.something` does not always call `yChanged()`. There are many more complicated edge cases which may make sense from some angle, but not until I wasted a day to debug them. – Jack White Aug 12 '22 at 18:35
  • Yeah, in any language I prefer to write code to avoid edge cases than to spend much time figuring them out. I think the result is more readable and maintainable this way, despite the extra lines, because then I and other readers don't have to remind or figure them out again. About the answer timing, I didn't even expect you to see it 2 years later, I left it for other readers and myself. Thanks for touching base! – really Aug 13 '22 at 18:10
  • Unfortunately it is often simply not possible because of Qt/QML limitations. Readability does not have to do anything with that because it does not matter how well something is written, explained and documented if it does not work. – Jack White Aug 14 '22 at 10:33
  • Maybe I'm a little obsessed about what I let get in my code base, but actually I find ways around almost everything I don't like lol – really Aug 15 '22 at 03:45