QML: saving a ListModel element to DConf leads to object with duplicate keys

Hi,

I came across this phenomenon of duplicate keys when trying to persistently store a ListModel to a dconf key.

Semi-Minimal example which shows this below.

The “bug” happens here:

    // some model has data like [ { "text": ¨value" }, { "text": "othervalue" }, ... ]
    // what ends up in Dconf is: 
    // [ { "text": ¨value", "text": "value" },  { "text": "othervalue", "text": "othervalue" }... ]

    property ListModel secondModel: ListModel{}  

    function addToSecondModel(o) {
         secondModel.append(o); // duplication seems to EITHER happen here
    }

    // save model to persistent key
    function saveSecondModel() {
        var ds = [];
        for (var i=0; i<secondModel.count;i++) {
            const e = secondModel.get(i); // ... OR here
            ds.push(e);
        }
        // this is a dconf key:
        config.test = JSON.stringify(ds);
    }
    ConfigurationGroup  { id: config
            path:  "/some/app"
            property string test: '[]'
    }

Full woking example below, start as qmlscene minitest.qml:

    import QtQuick 2.6
    import Sailfish.Silica 1.0
    import Nemo.Configuration 1.0

    ApplicationWindow {
        id: app

        property ListModel secondModel: ListModel{}     // things to be saved are added to this

        // add something to the model
        function addToSecondModel(o) {
            console.debug("secondModel:", JSON.stringify(secondModel,null,2));
            console.debug("adding:", o, JSON.stringify(o,null,2));
            secondModel.append(o);
            console.debug("secondModel:", JSON.stringify(secondModel,null,2));
            for (var i=0; i<secondModel.count;i++) {
                const e = secondModel.get(i);
                console.debug("elements:", JSON.stringify(e,null,2));
            }
        }
        // save model to persistent key
        function saveSecondModel() {
            var ds = [];
            console.debug("secondModel:", JSON.stringify(secondModel,null,2));
            for (var i=0; i<secondModel.count;i++) {
                const e = secondModel.get(i);
                console.debug("pushing:", JSON.stringify(e,null,2));
                ds.push(e);
            }
            console.debug("saving:", JSON.stringify(ds,null,2));
            config.test = JSON.stringify(ds);
            console.debug("saved:", config.test);
        }

        Component.onCompleted: {
            var testData = [
                [ "first-string-is-this", "23107-4571-17794", "true", "active", 0, "", ],
                [ "second-string-is-this", "11479-28990-12380", "true", "passive", 1, "", ],
                [ "third-string-is-this", "6768-21821-19354", "false", "active", 1, "", ]
            ];
            // fill data model:
            testModel.fill(testData);
        }

        // application settings:
        ConfigurationGroup  {
            id: settings
            path: "/org/nephros/testing/" + "saveTest" 
        }
        ConfigurationGroup  {
            id: config
            scope: settings
            path:  "app"
            property string test: '[]'
        }

        ListModel { id: testModel
            function fill(data) {
                console.debug("fill model got: " + data.length  + " entries.");
                const o = {};
                data.forEach(function(e) {
                    o = {
                        "text1": e[0],
                        "text2": e[1],
                        "text3": e[2],
                    }
                    append(o);
                });
                console.debug("model has now: " + count  + " entries.");
            }
        }

        initialPage: Component { Page { id: page
            SilicaFlickable { id: flick
                anchors.fill: parent
                contentHeight: col.height
                Column { id: col
                    width: parent.width - Theme.horizontalPageMargin
                    anchors.horizontalCenter: parent.horizontalCenter
                    PageHeader { id: head ; title: qsTr(Qt.application.name); }
                    Repeater {
                        model: testModel
                        delegate: Component {
                            ListItem {
                                contentHeight: content.height
                                hidden: false
                                menu: ContextMenu {
                                    MenuItem { text: qsTr("save this");
                                        onClicked: {
                                            hidden=true ;
                                            console.debug("click-adding:", model.text1, JSON.stringify(model.text1,null,2));
                                            app.addToSecondModel({ "text1": model.text1 });
                                            app.saveSecondModel()
                                        }
                                    }
                                }
                                Column { id: content
                                    width: parent.width
                                    spacing: Theme.paddingSmall
                                    Label { text: text1 ; elide: Text.ElideLeft; font.pixelSize: Theme.fontSizeSmall}
                                    Label { text: text2 ; elide: Text.ElideRight; width: parent.width}
                                    Label { text: text3 ; font.pixelSize: Theme.fontSizeSmall}
                                }
                            }
                        }
                    }
                }
            }
        } }
    }

    // vim: ft=javascript expandtab ts=4 sw=4 st=4

which results in this output:

11:10:08.484 qml: file:///home/nemo/devel/git/systemd-watcher/test/minitest.qml:97 onClicked click-adding: second-string-is-this "second-string-is-this"
11:10:08.485 qml: file:///home/nemo/devel/git/systemd-watcher/test/minitest.qml:12 addToSecondModel secondModel: {
  "objectName": "",
  "count": 0,
  "dynamicRoles": false
}
11:10:08.485 qml: file:///home/nemo/devel/git/systemd-watcher/test/minitest.qml:13 addToSecondModel adding: [object Object] {
  "text1": "second-string-is-this"
}
11:10:08.487 qml: file:///home/nemo/devel/git/systemd-watcher/test/minitest.qml:15 addToSecondModel secondModel: {
  "objectName": "",
  "count": 1,
  "dynamicRoles": false
}
11:10:08.487 qml: file:///home/nemo/devel/git/systemd-watcher/test/minitest.qml:18 addToSecondModel elements: {
  "text1": "second-string-is-this",
  "text1": "second-string-is-this"
}
11:10:08.488 qml: file:///home/nemo/devel/git/systemd-watcher/test/minitest.qml:24 saveSecondModel secondModel: {
  "objectName": "",
  "count": 1,
  "dynamicRoles": false
}
11:10:08.488 qml: file:///home/nemo/devel/git/systemd-watcher/test/minitest.qml:27 saveSecondModel pushing: {
  "text1": "second-string-is-this",
  "text1": "second-string-is-this"
}
11:10:08.489 qml: file:///home/nemo/devel/git/systemd-watcher/test/minitest.qml:30 saveSecondModel saving: [
  {
    "text1": "second-string-is-this",
    "text1": "second-string-is-this"
  }
]
11:10:08.490 qml: file:///home/nemo/devel/git/systemd-watcher/test/minitest.qml:32 saveSecondModel saved: [{"text1":"second-string-is-this","text1":"second-string-is-this"}]

Did you figure it out? it is a bit contrived to follow without a debugger:)
You have [ "first-second-string-is-this", in two entries, could it be this?

Also, that first log with “elements:” that shows the duplicate, shouldn’t it be preceded by two “secondModel:” loggings?

Nope, still not clue.

You are right, that is only a typo however, and doesn’t change the outcome. I have corrected code and example log above.

And sorry if that is confusing - but flow is basically from bottom to top.

  1. an object array is created
  2. a model is filled with data from that array
  3. it is displayed
  4. another model is initialized
  5. user clicks a menu comand
  6. element from first model is added to the second model
  7. all entries of the second model are saved to a dconf string

I could have squashed 1-3 but I wanted to be as close to the acutal program in case the problem is created already in the model creation steps. (In which 1. is an XMLhttpRequest DBus typedCall and 2. is its result handler callback).

I don’t see why. The log output looks correct to me.

But of course

Thanks, I managed to read and understand the whole sample now:) Your comments where this happens are spot on but…

It doesn’t seem to be dconf related, but QML.
Here’s what I tried to understand:

  1. if there are bugs around this:
    https://bugreports.qt.io/browse/QTBUG-38907?jql=text%20~%20"QQmlListModel"%20
    (I haven’t found something)

  2. the .get and .append calls
    https://github.com/qt/qtdeclarative/blob/5.6/src/qml/types/qqmllistmodel.cpp#L2329
    https://github.com/qt/qtdeclarative/blob/5.6/src/qml/types/qqmllistmodel.cpp#L2250

They seem to be using a “QV4::” namespace which is probably a wrapper (and a wordplay) around V8 (but only in 4 cylinders?), the JS engine from Chromium. This I can tell from the API, I haven’t seen the wrappers.

There is a possiblity that the enumerable bit is set for more than one attribute for QV4::Object, but again, I am not seeing it.

Maybe try some other method than stringify, maybe Object.keys() et co?

Also, tried to look in the stringify implementation but it seems to use the enumerable bit, unless propertyListSize is set >0 which I didn’t find where (and is public…)

L.E. There are a couple of strinigify + duplicate key bugs https://bugreports.qt.io/browse/QTBUG-54285?jql=text%20~%20"stringify"

1 Like

Thank you very much for the deep dive. Bug 54285 does indeed look very simiar.

I’ll look into inspecting the objects using other methods, for now I just filter duplicates on load.

1 Like

The workaround seems indeed to be:

// save model to persistent key
        function saveSecondModel() {
            var ds = [];
            console.debug("secondModel:", JSON.stringify(secondModel,null,2));
            for (var i=0; i<secondModel.count;i++) {
                const e = secondModel.get(i);
                console.debug("pushing:", JSON.stringify(e,null,2));
                // workaround for item duplication
                // ds.push( e );
                ds.push( { "text": e["text1"] } );
            }
            console.debug("saving:", JSON.stringify(ds,null,2));
            config.test = JSON.stringify(ds);
            console.debug("saved:", config.test);
        }