Beginner's guide to property bindings

Introduction

In QML, state propagates through properties. They can be linked to expressions, thus forming property bindings. In the simplest case, the linked expression is another property. Whenever its stored value changes, bound properties get notified so they can react to these changes. This mechanism is known as Active Updating [1]. Property bindings allow us to establish communication channels between objects.

Property bindings

Here are some quick examples of property bindings. We can see how the left-hand side — the bound property — reads its value from the right-hand expression.

propA: propB                        // propA reads the value of propB whenever it changes
propC: 37                           // propC reads the value '37' once
propD: { return propE + propF; }    // propD reads the return value of the code block
                                    // whenever either propE or propF changes
propE: propE + propF                // shorthand for the above
propG: cond ? propE + propF : propG // read value of `propE + propF` whenever cond becomes
                                    // true, otherwise keep the value of propG unchanged
propH: (param0, param1) => { ... }  // propH is bound to an anonymous function and can
                                    // be called like a function now

In contrast, this is a property assignment:

propA = propB;

We can find assignments in JavaScript expressions, and they have a destructive side effect: They replace propA's original property binding with a one-time value assignment, and propA will no longer update when propB changes.

We generally prefer property bindings over assignments to avoid the surprise of breaking bindings by accident. Even a formerly unbound property might become bound in a quick refactor, and the property assignment that worked fine initially will now cause a bug.

More ways to bind

In JavaScript, we cannot use the regular property binding syntax. Instead we assign a Qt.binding instance, which accepts a callback as parameter:

propD = Qt.binding(() => { return propE + propF; });

Sometimes, we want to express conditional bindings, or have to switch between multiple bindings, carefully activating one at a time. The Binding QML type [2] makes this possible:

Rectangle {
    width: 320
    height: 200
    color: "gold"

    TapHandler { id: tapHandler }
    Binding on color {
        when: tapHandler.tapCount === 2
        value: "teal"
    }
    Binding on color {
        when: tapHandler.tapCount === 3
        value: "hotpink"
    }
    Binding on color {
        when: tapHandler.tapCount > 3
        value: "honeydew"
    }
    Text {
        anchors.centerIn: parent
        text: `tap count: ${tapHandler.tapCount}`
    }
}

Here, the color property of Rectangle has not three but four bindings: The initial value, "gold" and the three Binding instances, each with their own value for the color property. It would be four even, as the default value of the property still counts as a binding.

As we can guess from the code, the color property cycles depending on whether we manage to double-, triple- or quadruple-tap the rectangle before it reverts back to color: "gold" once the tapCount resets.

We are responsible for making sure that each of the when conditions define a unique state. If more than one binding is active, we have to consider the behaviour undefined.

The above example could easily be replaced with a switch statement in a JavaScript expression, but when more complex property bindings are involved, the declarative approach will be easier to maintain.

Properties

Now that we've seen the basics of property bindings, we should take a closer look at properties again. They form the primary interface for QML types. Once bound and linked to other properties or property-dependent expressions, we get to enjoy their versatility in full.

Reacting to property changes

Each property comes with its own on...Changed signal. Attaching a slot to the signal looks strangely similar to a property binding:

someProperty: <expression>
onSomePropertyChanged: console.log("someProperty now reads", someProperty)

The right-hand side will be run whenever someProperty changes.

In rare situations, we cannot directly attach to the signal. In those cases, we can use a Connections object instead:

Connections {
   target: root.model
   function onCountChanged() {
       // do something
   }
}

Here, we attach to a signal inside the target, but technically we also break encapsulation by reaching through objects. Use of Connections components are almost always workarounds and can be avoided by breaking down components and properties even further.

Qualifiers and how to use them

Here is the shortened syntax for defining a property:

<property>    ::= <reqProperty> | <rdProperty> | <defProperty>
<reqProperty> ::= "required property " <type> <identifier>
<rdProperty>  ::= "readonly property " <specifier> <identifier> ": " <expression>
<defProperty> ::= ( "default " )? "property " <type> <identifier>
                  ( ": " <expression> )?
<specifier>   ::= <type> | "alias "
<type>        ::= "var " | "int " | "real " | "string " | "list<" <type> "> " | <qmlType>

The definition of <identifier>, <expression> and <qmlType> is left as exercise, but this grammar eventually accepts these definitions, and more:

Control {
    id: root

    required property var model
    readonly property alias count: root.model.length
    property Component contentItem: Text { ... }
    ...
}

Note

Properties, signals and functions defined at the root level of a component, together with derived members from the parent component, describe the public interface of the component.

Let's ignore default properties [3] for now. They are useful but also a bit dangerous. The other qualifiers, required and readonly help us to build robust interfaces.

  • required means we expect the user of this component to provide the property value on instantiation. This is useful for declaring the component's dependencies. For instance, a view that depends on its model would mark the model property as required. The example above uses var as the model type, but it's good practice to restrict the model type to something more concrete. A component cannot be instantiated unless all required properties are set up.
  • readonly makes a property's binding constant such that it cannot be changed to another property binding nor overwritten by assignment. The active update mechanism remains however. This is useful for exposing inner dependencies to users of the component where our component shall remain in control over the property.
  • The third mode, when no qualifier is specified, can be used by properties that we consider to be a read-write channel. Both the component itself and users of the component can read or write to the property at any time. Look out for those properties, as they become more difficult to reason about over time.

Masking properties for improved access control

One case is missing though: What if we wanted to write to public properties from the component itself but allow only restricted access from the outside? A property that is partially readonly, so to speak? There is no such qualifier, but we can implement the semantics ourselves like so:

Control {
    id: root

    readonly property alias page: internal.page

    QtObject {
        id: internal

        property Item page
        readonly property Component pageComponent: Rectangle {
            width: 240
            height: 80
            color: "teal"
        }
    }

    Component.onCompleted: {
        internal.page = internal.pageComponent.createObject(root.contentItem)
    }
}

The internal QtObject is accessible through its id identifier within the component's scope. To the outside, the QtObject is hidden and not trivially accessible. With the use of an alias property, we map internal properties to a public read-only property. We've masked the internal property with a readonly property, thus providing restricted access to the same page instance.

Attempts to write to the aliased property, either from within or the outside, are disallowed by the QML engine:

Invalid property assignment: "page" is a read-only property

Conclusion

We've learned about property bindings in QML and how to build clean component interfaces using (qualified) properties. Together, they allow us to decouple components and encapsulate state or behaviour, which are major themes in object-oriented programming.

References

[1]In computer programming, suppose we have a data item A whose value depends on data item B, i.e., the value of A must be changed after the value of B changes and before the value of A becomes necessary. (from Wikipedia)
[2]Binding QML type
[3]QML default property
[4]Code example on GitHub