3
votes

I have an Item with a property. This property contains an array of JavaScript objects wich in turn contain other properties.

When I set binding for one of object's properties to some variable and its (variable) value changes triggering the binding then all properties in the array are reset to their initial values. I've created a small demo to show the problem.

C++ code:

// main.cpp
#include <QGuiApplication>
#include <QQuickWindow>
#include <QQmlApplicationEngine>
#include <QQmlContext>

class Test : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString value READ value NOTIFY valueChanged)
public:
    Test(QObject* parent = 0) : QObject(parent) {}
    QString value() const { return ""; }
    Q_SIGNAL void valueChanged();   
};

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;
    Test test;
    engine.rootContext()->setContextProperty("__test__", &test);
    engine.load(QUrl(QStringLiteral("qrc:/ui/main.qml")));
    return app.exec();
}

#include "main.moc"

QML code:

// main.qml
import QtQuick 2.4
import QtQuick.Controls 1.3

ApplicationWindow {
    id: container
    visible: true

    width: 640
    height: 480

    property int clicksCounter: 0

    Item {
        id: testObject
        property var myArray : [{
            name : "CustomName" + __test__.value,
            boolFlag : false
        }]
    }

    Rectangle {
        x: 10
        y: 10

        width: 100
        height: 100
        color: "red"

        MouseArea {
            anchors.fill: parent
            onClicked: {
                container.clicksCounter++

                console.log("CLICK #" + container.clicksCounter + "[RED SQUARE] : Set testObject.myArray[0] to TRUE\n")
                testObject.myArray[0].boolFlag = true
                console.log("CLICK #" + container.clicksCounter + "[RED SQUARE] : DONE\n")
            }
        }
    }

    Rectangle {
        x: 120
        y: 10

        width: 100
        height: 100
        color: "blue"

        MouseArea {
            anchors.fill: parent
            onClicked: {
                container.clicksCounter++

                console.log("CLICK #" + container.clicksCounter + "[BLUE SQUARE] : Triggering notify by calling C++ <Test::valueChanged> method \n")
                console.log("CLICK #" + container.clicksCounter + "[BLUE SQUARE] : [BEFORE] testObject.myArray[0].name: " + testObject.myArray[0].name + ', testObject.myArray[0].boolFlag: ' + testObject.myArray[0].boolFlag)
                __test__.valueChanged()
                console.log("CLICK #" + container.clicksCounter + "[BLUE SQUARE] : [AFTER] testObject.myArray[0].name: " + testObject.myArray[0].name + ', testObject.myArray[0].boolFlag: ' + testObject.myArray[0].boolFlag)
            }
        }
    }
}

Here is what I get:

qml: CLICK #1[RED SQUARE] : Set testObject.myArray[0] to TRUE qml: CLICK #1[RED SQUARE] : DONE

qml: CLICK #2[BLUE SQUARE] : Triggering notify by calling C++ <Test::valueChanged> method

qml: CLICK #2[BLUE SQUARE] : [BEFORE] testObject.myArray[0].name: CustomName, testObject.myArray[0].boolFlag: true qml: CLICK #2[BLUE SQUARE] : [AFTER] testObject.myArray[0].name: CustomName, testObject.myArray[0].boolFlag: false

So what happens here is that after I set testObject.myArray[0].boolFlag from false to true and call test.valueChanged() method, my flag automatically resets to its initial value. Same goes for any other type used - int, string, etc.

Why does this happen?

Visual Studio has update 4 installed.

1
I've simplified your test case a bit. The visible property lets you show the window on startup. All signals are invokable by definition, so you don't need to forward them to another invokable method. It might be a Qt bug in the binding implementation in QML...Kuba hasn't forgotten Monica
I've posted this as a bug: bugreports.qt.io/browse/QTBUG-47407geniuss99

1 Answers

1
votes

Very tricky, but this is intended behavior of QML. What you're suffering from is a conflict in binding assignment vs. procedural assignment, which is a common pitfall of QML.

First, you've set up a binding to myArray that (in effect) says that any time __test__.value changes, re-assign a literal array to myArray. And later you subvert that binding by procedurally modifying that array. Remember that the literal array is really JavaScript code that's being evaluated every time a signal fires in the binding.

So, what you're seeing in the last console.log is not that the bool has been reverted, but instead that the entire myArray has been reset to a new value, rebuilt from the binding.

What you really want is a new bool property that you can assign to, and your literal array gets bound to this property. Like this:

property bool myBoolFlag: false
property var myArray : [{
        name : "CustomName" + __test__.value,
        boolFlag : myBoolFlag
    }]

Then, when you assign to myBoolFlag the array gets rebuilt. And when you fire valueChanged the array also gets rebuilt, but this time the bool value is pulled from the property and has the right value.