0

I have a use case where depending on the presence or absence of a property value an object referencing it is either created or removed. I am using a Loader for the purpose, where the active property is bound to the property, that is when the property has a non-null value the loader is activated, when it is set to null it is deactivated.

However the problem is that the loader doesn't release its item immediately, so for a moment the item references a null property, thus is unable to access data, while the setting of the property to null triggers reevaluations that result in a swarm of cannot read property x of null.

Simple logic suggest this should not happen. So I thought that maybe the problem is the order of binding evaluations is wrong, resulting in the item's bindings being evaluated before the loader is deactivated. So I tried removing the binding for active and setting it up manually. The problem however persisted.

So here is a minimal representation to illustrate what's going on:

Window {
  id: main
  visible: true
  width: 500
  height: 300

  property QtObject object : QtObject {
    property QtObject subObject: null
  }

  QtObject {
    id: subo
    property int i : 1
  }

  Loader {
    id: ld
    active: false
    sourceComponent: Text {
      text: object.subObject.i
      font.pointSize: 20
    }
  }

  MouseArea {
    anchors.fill: parent
    onClicked: {
      if (object.subObject) {
        ld.active = false
        object.subObject = null
      } else {
        object.subObject = subo
        ld.active = true
      }
    }
  }
}

Note that in this case the loader is explicitly deactivated before the property is set to null, yet nonetheless, every time that this happens I get a type error in the console:

qrc:/main.qml:25: TypeError: Cannot read property 'i' of null

This doesn't seem like correct behavior. So maybe a bug? Or am I missing something? Any suggestions on how to work around that limitation? Note that this is somehow avoided when using views or repeaters.

Update: To clarify further, in my actual production code the loader's item cannot really exist without referencing that property. So the idea was that the object should only be created when the property has a value other than null and be destroyed when it is null.

dtech
  • 47,916
  • 17
  • 112
  • 190

4 Answers4

1

It's not really a bug, but more like a technical limitation. A slot must not delete the sender of the connected signal. It must use deleteLater() to avoid that the execution returns to a deleted object. Essentially for the same reason, Loader cannot immediately delete the loaded item, because the active property might be bound to something that is controlled from within the loaded hierarchy of items.

jpnurmi
  • 5,716
  • 2
  • 21
  • 37
  • That makes sense, but still doesn't suggest any work-around for the problem it poses. – dtech Jan 04 '17 at 22:20
  • It might be possible in the trivial example above, but in my production code the item cannot really exist without that stuff, thus the logical thing seemed to be binding its lifetime to the presence of the property. – dtech Jan 04 '17 at 22:26
  • Also shouldn't objects that have been `deleteLater()`ed stop processing events and such? Even if the object lives to the next event loop cycle, the bindings that would fail to resolve should not be executed after the call to destroy the object has been made. It seems like a design flaw. – dtech Jan 04 '17 at 22:34
  • Would it be an option to pass the object to the loaded item, and just let it keep the reference during its lifetime, instead of referencing to the outside? – jpnurmi Jan 04 '17 at 22:40
  • The item cannot exist or even be created without that value. Due to **yet another** design limitation, I have to use `Loader.setSource()` in order to avoid the broken recursion checks (https://bugreports.qt.io/browse/QTBUG-56292) and thus must use a binding to the data property, because otherwise I can only pass the initial `null`, and naturally, the loader will not update when the null changes to a valid data property. – dtech Jan 04 '17 at 22:44
  • It still has to execute `Component.onDestruction` or anything else that a component might perform during its destruction, so it cannot cease to exist. – jpnurmi Jan 04 '17 at 22:45
1

I came across this yesterday. I need two Loaders which share the same property. In C++ class, I update the two Loader's active property one by one, which will also lead to that problem. My solution for this is using a Binding and do some setting like this:

Binding {
        target: contentItemAlias
        property: 'currentIndex'
        value: if (header.status == Loader.Ready) 
        header.item.currentIndex //ensure the current status is not null
        when: content.status === Loader.Ready // the content is ready
    }

Actually maybe this will not help you, but this is the solution in my code. I hope you will see it and have a try at binding. Wish you success. here is the link to my project: https://github.com/begoat/qmlLive

Yilin Huang
  • 121
  • 1
  • 7
  • 2
    Welcome to Stack Overflow. A link to a potential solution is always welcome, but please add context around the link so your fellow users will have some idea what it is and why it’s there. Always quote the most relevant part of an important link, in case the target site is unreachable or goes permanently offline. Take a look at [Why and how are some answers deleted?](https://stackoverflow.com/help/deleted-answers) and [How do I write a good answer?](https://stackoverflow.com/help/how-to-answer) – Gary99 Aug 12 '17 at 02:27
  • @Gary99 Thanks a lot. I got to know the reason why this is an excellent platform. – Yilin Huang Aug 12 '17 at 11:02
0

Even though it wasn't new to me that QML objects' destroy() is a deleteLater() and the latter doesn't destroy the object until the next event loop cycle, I expected such objects to stop processing events after that point.

It seems the object keeps on living for a while, and its bindings keep on being evaluated, failing to resolve because unlike the object destruction, the property setting happens instantaneously.

And since it wasn't an option to ever have null in the item, I managed to work-around the issue by having a global dummy data object and using a conditional binding:

active: object.data // for loader

data: loader.active ? object.data : Globals.dummydata // in item

This seems to work, providing dummy data to the object in its final moments.

dtech
  • 47,916
  • 17
  • 112
  • 190
  • Relevant bug reports: https://bugreports.qt.io/browse/QTBUG-60344, https://bugreports.qt.io/browse/QTBUG-51995. – Mitch Jul 07 '17 at 20:22
  • Yes, it would be nice to be able to block binding expression reevaluations of essentially dead objects. But I am not holding my breath for this to see light anytime soon. – dtech Jul 07 '17 at 20:40
0

You can also use Connections to connect to the Loader's onActiveChanged-event and break the binding there:

Loader {
    id: ld
    active: false
    sourceComponent: Text {
        text: object.subObject.i
        font.pointSize: 20

        Connections {
            target: ld
            onActiveChanged: {
                if (active) return
                text = text // This will overwrite the binding by the last value of the property itself.
            }
        }
    }
}
  • There is a difference between the simplest few lines of code that reproduce the problem and actual production code. In the actual code the data object and its subobjects provide dozens of property values, many of which are critical and the application would actually hard-crash without. There are more than a few ways to fix the problem for the example, but there is only one that can fix the actual problem. – dtech Jan 05 '17 at 15:07
  • On your last statement I disagree: I don't think that there is *one way* that can fix the actual problem, as I don't think it is *one* problem. I think there are a lot of issues to cope with, that stem from the same origin, which itself is a necessity (as jpnumri pointed out). I can only provide help to solve one problem at a time, within the simplest few lines that reproduce the problem. I can't solve all the problems in the actual production code that are not covered by the example. However I made a mistake in my answer. I need to check wheter the Loader is still active. – derM - not here for BOT dreams Jan 05 '17 at 15:23
  • The thing I try to achive more or less builds uppon your own solution, but eliminates (in my eyes) the need of a dummy-object, as instead of some dummy-values I will take ownership of the data, just before it is wiped. – derM - not here for BOT dreams Jan 05 '17 at 15:28
  • It is really a no-brainer, the loader has to be tied to the existence of the data property value, and it is too slow to go away when that value is set to null, so the only possible solution for that is to provide a dummy value when the actual value is gone. And while it could be theoretically possible to instead insert a bunch of null checks and dummy values, it is absolutely impractical in practice, as those values propagate to hundreds of places and drive possibly thousands of other values through bindings. I am talking editing thousands of lines of code with thousands of dummy values... – dtech Jan 05 '17 at 15:30
  • On a side note, given those design limitations, it would have been nice to have something like this in Qt: `loader.active = false; eventLoopFence(); property = null;` Unfortunately, directly from QML it is not even possible to make queued connections. And while technically in this particular case one could deactivate the loader and bind the property setting to null to the loader's item's destruction, my design, as any sane design, has the UI totally abstracted away from the data and entirely optional. No easy way to tell if a particular data layer property is tied to a particular GUI element. – dtech Jan 05 '17 at 15:52