Qt 5.4/Qml: Prevent binding loop

18,318

Solution 1

Don't bind it. Because the check box does not fully depend on Setting.someSetting.

When a user clicked the checkbox, the CheckBox.checked is changed by itself. At the same time, the property binding is no longer valid. Settings.someSetting cannot modify the CheckBox after it is clicked by user. Therefore, the checked: Settings.someSetting binding is wrong.

If you want to assign an initial value to the check box when the component is ready, use Component.onCompleted to assign it:

CheckBox {
    id: someSettingCheckBox 

    Component.onCompleted: checked = Settings.someSetting
    onCheckedChanged: Settings.someSetting = checked; 
}

If you are working on a more complex scenario, the Setting.someSetting may be changed by some other things during runtime and the state of the check box is required to be changed simultaneously. Catch onSomeSettingChanged signal and explicitly changed the check box. Submit the value of someSettingCheckBox to Settings only when the program/widget/dialog/xxx finished.

CheckBox { id: someSettingCheckBox }

//within the Settings, or Connection, or somewhere that can get the signal.
onSomeSettingChanged: someSettingCheckBox.checked = someSetting

Solution 2

I prefer this solution

// Within the model
Q_PROPERTY(bool someSetting READ getSomeSetting WRITE setSomeSetting NOTIFY someSettingChanged)

void SettingsModel::setSomeSetting(bool checkValue) {
    if (m_checkValue != checkValue) {
        m_checkValue = checkValue;
        emit someSettingChanged();
    }
}

// QML
CheckBox {
    checked: Settings.someSetting                         
    onCheckedChanged: Settings.someSetting = checked
}

The trick is you protect the emit with an if check in the model. This means you still get a binding loop but only a single one, not an infinite one. It stops when that if check returns false thereby not emitting to continue the loop. This solution is very clean, you do not get the warning, and yet you still get all the benefits of the binding.

I want to talk about the limitations of the other solutions presented

CheckBox {    
    Component.onCompleted: checked = Settings.someSetting
    onCheckedChanged: Settings.someSetting = checked; 
}

In this solution you lose your binding. It can only have a default setting on creation and be changed by the user. If you expand your program such that other things change the values in your model, this particular view will not have a way to reflect those changes.

Settings {
    id: mySettings
    onSomeSettingChanged: checkBox.checked = someSetting
}
CheckBox {
    id: checkBox
    onCheckedChanged: mySettings.someSetting = checked
}

This solution was mentioned to address these problems but never written out. It is functionally complete. Model changes are reflected, the user can change the data, and there are no binding loops because there are no bindings; only two discrete assignments. (x: y is a binding, x = y is an assignment)

There are a couple problems with this. The first is that I think its ugly and inelegant, but that is arguably subjective. It seems fine here but if you have a model representing 10 things in this view, this turns into signal spaghetti. The bigger problem is that it does not work well with delegates because they only exist on demand.

Example:

MyModel {
    id: myModel

    // How are you going to set the check box of a specific delegate when
    // the model is changed from here?
}
ListView {
    id: listView
    model: myModel.namesAndChecks
    delegate: CheckDelegate {
        id: checkDelegate
        text: modelData.name
        onCheckStateChanged: modelData.checkStatus = checked
    }
}

You can actually do it. I've made up custom QML signals and connections to do it, but the code complexity makes me want to hurl, and even worse you could possibly be forcing creation of a delegate when it is not necessary.

Solution 3

If you don't want to make a binding loop - don't make a binding, use a proxy variable, for example. Other simple solution can be to check the value:

CheckBox {
    checked: Settings.someSetting                         
    onCheckedChanged: {
        if (checked !== Settings.someSetting) {
            Settings.someSetting = checked;
        }
    }
}

Solution 4

You can also make two-way binding to resolve this issue:

CheckBox {
    id: checkBox

    Binding { target: checkBox; property: "checked"; value: Settings.someSetting }
    Binding { target: Settings; property: "someSetting"; value: checkBox.checked }
}

Solution 5

Sometimes it is useful to separate input and output values in control. In this case control always displays real value and it can also show a delay to the user.

CheckBox {
    checked: Settings.someSetting
    onClicked: Settings.someSetting = !checked
}
Share:
18,318

Related videos on Youtube

Hyndrix
Author by

Hyndrix

Updated on October 15, 2022

Comments

  • Hyndrix
    Hyndrix over 1 year

    I have a global singleton "Settings" which holds application settings. When I try to run the following code I get a QML CheckBox: Binding loop detected for property "checked":

    CheckBox {
        checked: Settings.someSetting                         
        onCheckedChanged: {
            Settings.someSetting = checked;
        }
    }
    

    It is obvious why this error occurs, but how can I correctly implement this functionality without a binding loop? E.g. I want to save the current checked state of the checkbox in the settings singleton.

    I am using Qt 5.4 and Qml Quick 2.

    Regards,

  • Hyndrix
    Hyndrix about 7 years
    Interestingly this works most of the time but with the Qt Quick Controls 2 this sometimes causes a binding loop as well.
  • DisplayName
    DisplayName over 3 years
    This is a 4 year old answer but I tried this today and it caused a binding loop. It could be something got updated. I am using controls 2.
  • machinekoder
    machinekoder over 3 years
    If the target (Settings.someSetting) property is correctly implemented with value checks the result is still the same.