Skip to content

QML Component Design The Two-Way Binding Problem and How to Avoid It

In a well-designed QML application, the UI is built using re-usable components, while the data and logic live in C++ based components we call controllers here. The QML part of the application uses these components (that themselves may be written in QML or C++) to build up the user interface and connect these components with the controllers. In this setup, the controllers provide the data as well as receive input from the UI. How hard can it be?

The Problem

Let’s look at an example to see where the problems start.

We have the following components:

  • a controller in C++ that we have made available to QML, exposing a read/write boolean property isBlue. We will assume that the checkbox isn’t the only thing in the application manipulating this property;
  • a Checkbox component that we have created from scratch in QML that we want to use to represent and manipulate this isBlue state, which looks something like this:
// CheckBox.qml (simplified)
Item {
    id: root

    property bool checked: false;
    property alias text: label.text

    Rectangle {
         color: root.checked ? 'black' : 'white'
    }

    Text {
        id: label
    }

    MouseArea {
        anchors.fill: parent
        onClicked: root.checked = !root.checked
    }
}
  • and, finally, a main.qml file that uses this Checkbox:
// in main.qml
CheckBox {
    id: colorCheckbox

    anchors {
        //... position the checkbox
    }

    text: "Make it Blue"

    //Todo: hook up to _controller's isBlue property in both directions
}

Your first instinct may be to write this for the todo above:

CheckBox {
    id: colorCheckbox
    // other properties, positioning, ...

    checked: _controller.isBlue
    onCheckedChanged: _controller.isBlue = checked;
}

All seems fine at first, until you notice that the checkbox no longer changes state when the isBlue property is changed in some other way than via the checkbox. What happened?

What happened is perhaps the biggest plague we have in QML: we have a broken binding. On line 5 in the snippet directly above, we set up a binding between the checked property of the checkbox and the isBlue property in the controller. But at the moment we click on the checkbox, the onClicked handler of the MouseArea inside the Checkbox component overwrites that binding! The checkbox itself is still updated, the controller is still notified of the changes but, if the value of the property on the controller changes from some other change, you will notice the connection has been broken.

This is the problem we’re going to address in this blog: how to write our component, main.qml and/or controller in such a way that we avoid this trap, yet have a good, clear API.

It’s good to keep in mind that sometimes the problem can actually become even more complicated. The controller may, for instance, need to communicate with some (slow) back-end, represent a physical device that changes it’s state only slowly, or may even just reject some changes because of business logic constraints. How do you deal with those situations?

The Proposed Value Approach

Idea: The component does not update its main state

The main problem we had in the example above is that the CheckBox component was updating it’s own state. Setting the checked property from inside the component itself broke any binding on that property. So, what if we don’t do that? What if we only propose a new value?

//Checkbox.qml, Proposed Value version (simplified)
Item {
    id: root

    property bool checked: false;
    property alias text: label.text

    signal checkedProposed(bool proposedChecked)

    //UI related code...

    MouseArea {
        anchors.fill: parent
        onClicked: root.checkedProposed( !root.checked )
    }
}

If we then use this component, we have to make a slight change to our code. Instead of responding to the checked property’s changing, we have to respond to the checkedProposed signal:

 CheckBox {
     id: colorCheckbox

     checked: _controller.isBlue
     onCheckedProposed: _controller.isBlue = proposedChecked;
}

This approach works fine. In effect, we let the connection from the controller to the component run through the checked property of the component, while the communication in the other direction goes via the proposedChecked signal. We don’t update the checkbox state; we wait for the controller to do that. We can, of course, change to some intermediary state or at least acknowledge the click with some subtle animation or the like, in case we want to deal with the controller’s (potentially) being slow or rejecting changes.

Note that, depending on your requirements, proposedChecked could also be a property that is read-only from the perspective of the user of the CheckBox component. The approach is simple, flexible, and light-weight (no additional objects needed) but it also has some downsides:

  • It only works on your own controls; standard Qt components are not structured like this.
  • It is easy to get wrong by accident at the usage site, where you may accidentally end up responding to the checked property’s changing.
  • If you need to handle unresponsive back-ends, you will find yourself replicating that for every control.

Still, all-in-all, it’s quite a good solution that we have applied with success in real-world projects.

The Unbreakable Binding Approach

If you have used Qt’s own components, you will find that often (but not always) these actually don’t break the binding if you use them like we did in our original example!

Idea: Learn from Qt’s own components and avoid breaking the binding

It turns out that it is possible to avoid breaking the binding if we move some of the component to C++ and are careful how we use the internals from our QML-based graphical representation. Simply doing this, then, just works out of the box:

CheckBox {
    id: colorCheckbox
    // other properties, positioning, ...

    checked: _controller.isBlue
    onCheckedChanged: _controller.isBlue = checked;
}

To make this work, we do need to modify our CheckBox component and put its state in a C++-based internal helper:

Item {
    id: root

    property alias checked: internal.isOn
    property alias text: label.text

    //UI related code...

    BooleanValue {
        id: internal
    }

    MouseArea {
        anchors.fill: parent
        onClicked: {
            internal.toggle(); // works, using convenience function on BooleanValue
            // internal.setOn(!internal.isOn) // works too
            // internal.isOn = !internal.isOn // Wrong: breaks the binding
        }
    }
}

Where BooleanValue is a simple QObject subclass, exported as a creatable helper to QML:

class BooleanValue : public QObject
{
    Q_OBJECT
    Q_PROPERTY(bool isOn READ isOn WRITE setOn NOTIFY isOnChanged)

public:
    explicit BooleanValue(QObject *parent = nullptr);
    bool isOn() const;

public slots:
    void setOn(bool isOn); // Be sure to make it a slot or Q_INVOKABLE

    //convenience API is now possible
    void toggle();

signals:
    void isOnChanged(bool isOn);

private:
    bool m_isOn = false;
};

In this case, the helper only contains data. For more complex controls, I would recommend that you move all the control’s state as well as its logic to such a helper. This results in faster and easier-to-test and easier-to-reuse code.

The reason this design works is that the approach of not directly setting the property from the clicked eventhandler but instead calling a method on the helper circumvents the mechanism in Qt that would otherwise break the binding because it would detect a write to a property that already had a binding set. The result is a component that is quite robust and very easy to use, as it feels native to how the rest of the Qt-provided components work. It is also possible to extend the logic of the C++ helper to deal with a slow backend, which can then show some intermediary state and perhaps return to the original state after some time out. The only slight downsides are:

  • it may be a bit confusing to understand how and why this works
  • you always need an additional object instantiated to contain the data (and any logic)

How Not to Solve this Issue

The approaches above have proven themselves in real-world projects, but that does not mean we didn’t also run into or experimented with some other approaches that proved to be not quite as workable. Let’s go over a few approaches to avoid and one that can sometimes be useful when in a pinch.

Don’t: Explicitly Re-create the Binding

Idea: If it breaks, fix it!

If we know that the binding breaks, we could, of course, explicitly re-create it:

CheckBox {
    //BAD: don't create bindings in an imperative way
    function isBlueBinding() {
        return _controller.isBlue;
    }

    checked: isBlueBinding()

    onCheckedChanged: {
        _controller.isBlue = checked;
        checked = Qt.binding(isBlueBinding);
    }
}

Technically, this works but it is very easy to forget, hard to read, and clutters up your application code.

Don’t: Use Aliased-in Value

Idea: If there is no property, we can’t break bindings on it either.

The idea here is to avoid breaking the binding by not having a property in the component to break a binding to. Instead of having the checked property already in the component, we (ab-)use a property alias to set one from outside the component, which then also can be used from inside it:

SomeController {
    id: colorController
}

CheckBox {
    //BAD: API is unclear and inflexible
    property alias checked: colorController.isBlue
}

While clever, there are some serious problems with this approach. First of all: it doesn’t work with controllers that are instantiated on the C++ side. You can only create aliases to properties on objects created on the QML side. That rules out controllers you access via a context property or from a singleton or singleton instance, which happen to be the most common ways to expose controller objects to QML. You’d have to instantiate it in the QML as shown above on line 1.

Furthermore, it is extremely un-intuitive. CheckBox has no way to really say that the user needs to alias in this checked value. You’d have to rely on documentation only to explain this.

Don’t: Use the Model Approach

Idea: learn from item models

A last approach to avoid is making your values “heavy” model types, where you wrap the value in a QObject-derived class like BooleanValue above and then use instances of these on the controller side:

class SomeController : public QObject
{
    Q_OBJECT
    //BAD: Wastefull and overly bloated
    Q_PROPERTY(BooleanValue *isBlue READ isBlue CONSTANT)

public:
    explicit SomeController(QObject *parent = nullptr);
    BooleanValue *isBlue() const;

private:
    BooleanValue *m_isBlue;
};

Then, the component can take an instance of such a model. This actually results in a simplification at the usage site, as you only need to set the model on the component without having to bother setting up the return direction:

CheckBox {
    model: _controller.isBlue
}

The control can directly set new values on the model, and this doesn’t break the binding as it sets a property or calls methods on the set model, but does not change the model itself. But it results in quite bloated, heavy controllers that become awkward to work with. It is very rarely worth going this direction.

Bonus: Use a Binding or a Connections Component

One last approach that I wanted to mention is one that sometimes can help you get out of a corner, though I would not recommend to use it for often-used components like check boxes and the likes. You can also prevent the binding from breaking by not making the binding directly, but by using the QML Binding type:

CheckBox {
    id: colorCheckbox

    onCheckedChanged: _controller.isBlue = checked;

    Binding {
        target: colorCheckBox
        property: "checked"
        value: _controller.isBlue
    }
}

Alternatively, you could achieve the same effect by using a Connections type:

CheckBox {
    id: colorCheckbox
    onCheckedChanged: _controller.isBlue = checked;

    Connections {
        target: _controller
        onIsBlueChanged: colorCheckbox.checked = isBlue;
    }
}

In both cases we avoid the property binding breaking at the cost of being more verbose and an additional object. It’s too easy to forget and too much bloat to recommend using for the general case, but it can be good to realize that the Binding and Connections types can be used for cases like this.

Summary

It’s clear that there are several ways to approach this issue, and it’s not completely trivial to come up with an approach that matches the criteria we set out with: easy to use, hard to get wrong, and as light as possible.

If you were to ask me to recommend an approach to use for your components for the general case, I would suggest that you use the unbreakable binding approach as a first go-to solution. Even though it requires you to create C++ based helper classes to contain your data, the result at the usage site looks and feels most like the existing Qt components. And being forced to use C++ could be a blessing in disguise as it also provides an opportunity to move all your logic there, resulting in faster, easier to test and easier to re-use components.

About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

FacebookTwitterLinkedInEmail

Categories: KDAB Blogs / KDAB on Qt / QML / QtDevelopment

Tags: /

9 thoughts on “QML Component Design”

  1. Thanks for the article. I have experienced these problems a few years ago and after a lot of googling and experimenting finally arrived at the “unbreakable binding” approach. Two-way binding is commonly needed so it really ought to be addressed better in the QML framework. The current “workarounds” are not really intuitive and most developers sooner or later bump into these same problems.

    1. I did consider also adding a TwoWayBinding component as a possible solution (we do have such a thing, and I actually presented it in my Qt World Summit talk in 2019 on the same topic), but it also has its drawbacks and decided against it. But yes, I agree a better solution is needed. It would be nice to have a language feature to express that you want to bind a property in two directions, but it would only work for simple bindings. As soon as you want to use an expression in between, it breaks down.

    2. I’ve written such a component but I want to add some more API to it, I think I’ll publish a first draft to get more feedback about the API and naming from the community.
      Here is a preview : https://streamable.com/n59vrt

      It supports bidirectional binding with optional delay before writing back to the controller
      It can also handle slow or unresponsive controllers with a timeout period.

      1. Cool, thanks. That looks like a fleshed out version of what we have for our TwoWayBinding component. Looking forward to seeing it published!

  2. Doesn’t the drawback “you always need an additional object instantiated to contain the data (and any logic)” actually applies to the “Proposed Value” approach and not the “Unbreakable Binding” approach?

    The “Unbreakable Binding” approach seems to be the best solution but that could be modified to have an interaction signal rather than just using the property change signal. `onToggled` vs `onCheckedChanged` like what’s done for QQC2 (toggled, textEdited, valueModified vs checkedChanged, textChanged, valueChanged). You don’t want to call the backend setter again when the change comes from the backend.
    Also the BooleanValue could be generalized to be a VariantValue instead, minus the toggle() convenience function.
    But you lose the type-correctness so. Should we add a Value class for every type that we want to bind on?

    The “Proposed Value” approach doesn’t seem to be applicable since the Component doesn’t hold any value and assume it has to be used with a backend which you don’t always want.

    The “Unbreakable Binding” approach is a bit complicated (more complex Component, additional C++ classes needed) but actually somewhat usable.

    Most of the time I use the Binding approach or the “Explicitly Re-create the Binding” one.
    Even if it breaks the DRY principle, I prefer to do it this way than defining a standalone function, I find it easier to read:

    “`
    CheckBox {
    checked: backend.isBlue
    onToggled: {
    backend.isBlue = checked;
    checked = Qt.binding(() => backend.isBlue);
    }
    }
    “`

    1. No, the comment does not apply to the Proposed Value approach. In that approach, the value _is_ held as a property in the component, it does not require additional QObjects to be created. The component _does_ contain the state and can be used both with and without a controller backend object.

      Of course you could give the (Unbreakable Binding) component additional API to signal changes, and how they came to be as you suggest. But that is not strictly needed, and as long as you don’t have a concrete use case for it, I suggest you don’t. The property setter of the controller should check if the setter is called with the current value anyway, and that will stop any loops right there. So, I don’t mind calling that setter again all that much. What’s more, your Unbreakable Binding internal objects should do the same. I prefer to keep APIs lean in order to keep them easier to maintain and easier to test, as well as easier to understand.

      You could come up with a generic VariantValue, but I don’t think it would yield much benefit. As suggested in the article, you would usually have more complex and or more state anyway, so you would be better off making a custom internal logic component for each UI component. The checkbox was of course just a very simple example, for which you would normally just use CheckBox from QtQuick Components.

      As to using your re-create bindings: I would rather hide that complexity the components once, instead of repeating that logic everywhere I use the component. It is easier to maintain and harder to get wrong due to say copy/paste errors where you change the property you bind to but forget to adjust the binding re-creation.

      1. We might have misunderstood eachother about the “additional object to contain the data”.
        Were you talking about the BooleanValue? I see that only as an implementation detail.
        I was talking about the control always needing another “controller”/”backend” object to contain the logic/data.

        I may have missed something but as I see it the “Proposed Value” approach as you wrote it don’t work as a standalone control holding its own value.
        Clicking on it will only emit the checkedProposed signal.
        You have to add `onCheckedProposed: checked => checkbox.checked = checked` in the user code for it to work. It kind of defeats the purpose.

        The use case for the API to signal changes in the Unbreakable Binding is when having an expensive setter or getter in the backend.
        Let’s say we have:
        checked: _controller.isBlue
        onCheckedChanged: _controller.isBlue = checked

        if the controller’s isBlue changed on the controller side, it will call onCheckedChanged and thus the controller’s isBlue setter which might be expensive (a sdk call, a network call, you might not want to cache those).

  3. Specifically for checkbox I think there is a cleaner solution: use nextCheckState. You use that to trigger the change on the model side, and then return the new value in the model from your function. Unfortunately e.g. TextField or RadioButton don’t have such a hook, I prefer to use a Binding there. The arguments against it seem to be a bit hand-wavy? 1. It’s extra code and verbose. But the proposed solution here is to actually completely rewrite the component + add a C++ helper class… 2. It’s bloated. See 1. I even wonder how Checkbox + Binding compares against the custom qml and C++, I’m not sure it’s actually heavier given that the QtQuick Checkbox itself is entirely implemented in C++?

    1. None of the solutions I discussed are needed for the standard QtQuick components. I only choose to (re-) implement a Checkbox because it’s the simplest one to implement that still shows the issue I wanted to discuss. I don’t want to propose to re-write existing components just to get around this problem, I do propose that when you do write your own custom components you consider using a solution like the Unbreakable Binding approach to facilitate it’s easy use in the rest of your application.

Leave a Reply

Your email address will not be published.