3

I have a variable number of components, so i'm trying to give each one its own model. In this example, i just create one, but the idea is the same.

GC() is a bit random, so in the example, i force the gc() after a click to flush out the problem. What happens is that the model is destroyed and becomes null. after that the click method cannot use it.

main.qml:

import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.2

import com.example.qml 1.0

ApplicationWindow
{
    visible: true
    width: 640
    height: 480

    // builder of dynamic models
    ModelFactory { id: maker }

    Column
    {
        anchors.fill: parent
        Repeater
        {
            // create dynamic model
            model: maker.makeModel();
            delegate: Label
            {
                id: label
                text: model.name

                MouseArea
                {
                    anchors.fill: parent
                    onClicked:
                    {
                        // works once until gc()
                        console.log("clicked on " + model.name)

                        // wont work anymore. model is destroyed
                        gc();
                    }
                }
            }

        }
    }
}

c++/mymodel.h:

#include <QAbstractListModel>
#include <QQmlApplicationEngine>
#include <QObject>
#include <QString>
#include <QDebug>

class BoxModel : public QAbstractListModel
{
    Q_OBJECT

public:

    ~BoxModel()
    {
        // see that it does get destroyed
        qDebug() << "~BoxModel()";
    }

    int rowCount(const QModelIndex& parent = QModelIndex()) const override
    {
        return 5;
    }  

    QVariant data(const QModelIndex &index, int role) const override
    {
        int ix = index.row();
        if (ix < 1) return "Larry";
        if (ix < 2) return "Barry";
        if (ix < 3) return "Gary";
        if (ix < 4) return "Harry";
        return "Sally";
    }

    QHash<int, QByteArray> roleNames() const override
    {
        QHash<int, QByteArray> roles;
        roles[Qt::UserRole+1] = "name";
        return roles;
    }

};

class ModelFactory: public QObject
{
    Q_OBJECT

public:

    Q_INVOKABLE BoxModel* makeModel()
    {
        return new BoxModel();
    }    
};

main.cpp just registers the types:

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <qqmlcontext.h>
#include <qqml.h>
#include <QtQuick/qquickitem.h>
#include <QtQuick/qquickview.h>
#include "mymodel.h"

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;

    qmlRegisterType<BoxModel>("com.example.qml", 1, 0, "BoxModel");
    qmlRegisterType<ModelFactory>("com.example.qml", 1, 0, "ModelFactory");

    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    return app.exec();
}

what you see:

runtime

Click on any of the names. it will work once and after that they will be undefined because model becomes null.

eg

qml: clicked on Sally
~BoxModel()
qml: clicked on undefined

My question is why is this, when i still have a reference to it?

In the example, onClicked could be changed to label.text rather than model.name to fix, but the real problem is that, in general, the model is accessed by the object at any time, for any data. For example, when the box needs to redraw. randomly the data is gone, depending on GC.

I've tried making c++ manage the life of the dynamic model. this could work if i know when exactly QML has finished with it.

thanks for info and ideas.

running on windows 8.1/qt5.6mingw

EDIT1: files as a gist, https://gist.github.com/anonymous/86118b67ec804e6149423c14792f312d

jkj yuio
  • 2,543
  • 5
  • 32
  • 49
  • This looks like a genuine bug, thank you for providing a good, clean test case. Do you still have the problem if you remove the factory, and create the instance directly in QML: `model: new BoxModel()`? – Kuba hasn't forgotten Monica May 19 '16 at 14:33
  • 2
    interesting. if i change `model: maker.makeModel()` to `model: BoxModel {}` it works. i'm going to see if this fixes my problem when i have multiple such boxes. thanks. – jkj yuio May 19 '16 at 14:45
  • Yes this way can work, providing some changes can be made to the underlying code mode. it requires the `BoxModel` to be able to exist on its own without any factory to "know" about it. The problem i had is that i needed to track these internally. had to be done with a singleton, but it can work this way. however, it would be good for the dynamic version to work too. thanks. – jkj yuio May 19 '16 at 17:24
  • What's stopping you from adding the instance to a global list in a constructor, and removing it in the destructor? You don't need a factory for that. – Kuba hasn't forgotten Monica May 20 '16 at 13:36

3 Answers3

0

As Kuba said, this does indeed seem like a bug. However, you can take another approach and take ownership of the models yourself via QQmlEngine::setObjectOwnership(). Specifically, changing

Q_INVOKABLE BoxModel* makeModel()
{
    return new BoxModel();
}

to

Q_INVOKABLE BoxModel* makeModel()
{
    BoxModel *model = new BoxModel(this);
    QQmlEngine::setObjectOwnership(model, QQmlEngine::CppOwnership);
    return model;
}

will fix this (remember to parent the returned model to BoxModel so it gets deleted appropriately). The reason for the behaviour is explained here:

Generally an application doesn't need to set an object's ownership explicitly. QML uses a heuristic to set the default ownership. By default, an object that is created by QML has JavaScriptOwnership. The exception to this are the root objects created by calling QQmlComponent::create() or QQmlComponent::beginCreate(), which have CppOwnership by default. The ownership of these root-level objects is considered to have been transferred to the C++ caller.

Objects not-created by QML have CppOwnership by default. The exception to this are objects returned from C++ method calls; their ownership will be set to JavaScriptOwnership. This applies only to explicit invocations of Q_INVOKABLE methods or slots, but not to property getter invocations.

Community
  • 1
  • 1
Mitch
  • 23,716
  • 9
  • 83
  • 122
  • This isn't really an answer. The meaning of `JavaScriptOwnership` is precisely what the asker expected: that the JS engine will manage the lifetime and **only destroy the object when there are no more references to it**. In asker's case, it's a clear bug since `model` **is a live reference** yet the referenced object gets destroyed. That should never be the case! – Kuba hasn't forgotten Monica May 19 '16 at 16:43
  • @Mitch, thanks for your answer. I did try `CppOwnership` originally. but don't know when to delete them. i have not found a QML notifier that tells me QML is done, for example when it destroys the enclosing view. – jkj yuio May 19 '16 at 17:27
  • @KubaOber that's a good point; it probably is a bug. Still, this is probably the most sane workaround. I'll update the answer. – Mitch May 19 '16 at 17:50
  • @jkjyuio if you parent the models that you create to `BoxModel`, they will get deleted when that does. That seems like a pretty reasonable way to do it. – Mitch May 19 '16 at 17:53
0

I just had the same problem with a ComboBox.

As a workaround, you may create your own property to keep a strong reference to it:

Repeater {
    property QtObject myModel: maker.makeModel();
    model: myModel
    // …
}
rom1v
  • 2,752
  • 3
  • 21
  • 47
0

I know this is an old question, but I've just faced similar issue, and found your question in process of writing mine. See QObject gets destroyed after being put into QML variable for full story, and I'll cite it here.

What I've figured out that if I set the parent of that QObject before I pass it into QML, then it doesn't get deleted. So, I've concluded that passing unparented QObject into QML scope makes that scope become a parent of QObject and call its destructor after scope ends.

Community
  • 1
  • 1
ProdoElmit
  • 1,067
  • 9
  • 22