enhancement
major
minor
major
minor
major
#29609
TL Views: a `<progress>` (ChannelObjectObserver) evaluates its expression on a deleted object and logs an ERROR when an observed object is deleted
Problem
<progress input="nextMilestone" done="m -> …" total="m -> …" label="m -> $m.get(`tl.dev:Milestone#name) …" observed-types="tl.dev:Ticket"/>` in the issue tracker tl-dev (project overview, phase 8) follows changes of the object on its channel through the shared ChannelObjectObserver (#29533, #29546). Deleting that milestone through the UI (Roadmap tab, Delete) logs
ERROR [qtp…]: com.top_logic.model.listen.impl.EventBuilder - Failed to deliver change event to listener: ChannelObjectObserver … com.top_logic.basic.TopLogicException: Evaluation of '$m.name' failed … Caused by: com.top_logic.knowledge.wrap.WrapperRuntimeException: Target object is deleted at … ProgressElement.progressOf(…)
The observer forwards the deletion event of the observed object as a change, and the element re-evaluates its expressions on an object that no longer exists. The derived channel that yields the milestone recomputes afterwards, so the display heals; only the exception in the log is wrong (two ERROR lines per deletion).
Betroffen sind alle Halter des Observers, die den Kanal erneut lesen und einen Ausdruck auswerten: ProgressElement, TextElement, DerivedViewChannel und die Ausführbarkeit von Kommandos (ViewCommandModel). Der <switch> (ReactSwitchControl) hatte bereits eine eigene, lokale Prüfung auf gelöschte Eingabeobjekte; der <scroll-link> ist der einzige Halter, der das Löschereignis tatsächlich braucht (er blendet den Link aus).
Lösung
ChannelObjectObserver unterscheidet das Löschen eines beobachteten Objekts von einer Änderung: Trifft ein Ereignis ein, während ein Kanal ein nicht mehr gültiges Objekt (tValid() ist false) hält, wird der Änderungs-Callback nicht ausgeführt, sondern ein optionaler Lösch-Callback (viertes Konstruktorargument, standardmäßig ohne Wirkung). Geprüft werden die gehaltenen Objekte selbst, nicht der Inhalt des Ereignisses, sodass auch ein Ereignis zu einem anderen Objekt (über observed-types), das nach dem Löschen eintrifft, abgefangen wird. Die Halter behalten ihre aktuelle Anzeige, bis der Kanal seinen nächsten Wert liefert – der Eigentümer des Kanals (abgeleiteter Kanal, das löschende Kommando) schreibt ihn kurz darauf, und der gewöhnliche Kanal-Listener wertet dann neu aus. Damit wird kein Ausdruck mehr auf einem gelöschten Objekt ausgewertet, und ein <switch> baut seinen Inhalt nicht zweimal um.
- ReactSwitchControl verliert seine private Prüfung hasDeletedInput(), die der Observer nun für alle Halter übernimmt.
- ScrollLinkElement blendet den Link im Lösch-Callback aus (ChannelObjectObserver.IGNORE_CHANGE als Änderungs-Callback).
- Tests in tl-layout-view: TestChannelObjectObserver löscht ein beobachtetes Objekt in einer Transaktion und prüft, dass nur der Lösch-Callback läuft, kein ERROR protokolliert wird (AssertNoErrorLogListener), ein Ereignis zu einem anderen Objekt bei weiterhin gehaltenem gelöschtem Objekt keine Änderung meldet und eine spätere Änderung nach Neuschreiben des Kanals wieder gemeldet wird; TestTextElementObservation deckt den <text>-Halter durchgängig ab.
- Demo tl-demo-react, Switch-Demo: Fortschrittsbalken und abgeleiteter Text über dem ausgewählten Objekt, sodass „Löschen“ der Auswahl die betroffenen Halter im Browser auslöst (kein ERROR im Log, Anzeige bleibt bis zum Leeren der Auswahl).