enhancement
major
minor
major
minor
major
#29574
TL Views: Veto eines verschachtelten Channel-Schreibzugriffs bricht die Benachrichtigung des äußeren Channels ab (Tabelle und Detailbereich bleiben nach "Verwerfen" auf dem alten Objekt)
Betrifft 8.0.0-SNAPSHOT, tl-layout-view / tl-layout-react. Beobachtet bei der Verifikation von #29552 (dort unter "Notizen" festgehalten, nicht geändert).
Reproduktion
React-Demo, Seite Objektliste (Panel "Tickets"): ein Ticket auswählen, im Kommentar-Editor (Composer) Text eingeben, dann in der Tabelle ein anderes Ticket anklicken. Der Dialog "Ungespeicherte Änderungen" erscheint; auf "Verwerfen" klicken.
Erwartet: Adresszeile, Tabellenmarkierung und Verlaufsbereich zeigen das neu gewählte Ticket.
Beobachtet: Die Adresszeile zeigt das neue Ticket, Tabellenmarkierung und Verlaufsbereich bleiben beim alten, bis das Ticket noch einmal ausgewählt wird.
Analyse
DefaultViewChannel.set() befragt seine Veto-Listener, schreibt den Wert und benachrichtigt dann seine Listener. Schreibt ein Listener dabei einen zweiten Channel, und wird dieser Schreibzugriff durch ein schmutziges Formular mit Veto belegt, läuft die ChannelVetoException durch die Benachrichtigung des äußeren Channels nach oben: der äußere Channel hält bereits den neuen Wert, seine noch nicht erreichten Listener werden nie benachrichtigt, und die Continuation des Dialogs wiederholt nur den inneren Schreibzugriff.
Auf der Tickets-Seite: der Ticket-Channel benachrichtigt zuerst den abgeleiteten Channel für die Adresszeile, dann die <object-list> des Verlaufs. Diese setzt beim Containerwechsel ihren Entwurfs-Channel zurück (ObjectListControl.resetNewElement), was das schmutzige Composer-Formular verbietet. Die Tabellenbindung (TableSelectionBinding) und das <form input="ticket"> kommen nicht mehr an die Reihe.
Dieselbe Struktur haben ReactAdaptiveDetailControl (löscht seinen Auswahl-Channel, wenn ein reset-on-Master wechselt), FlowDiagramElement (ein Neuaufbau des Diagramms bei Änderung eines Eingabe-Channels löscht die Auswahl) und DerivedViewChannel, dessen addVetoListener bisher stillschweigend nichts tat, sodass ein an einen abgeleiteten Channel gebundenes Formular nie ein Veto einlegte.
Ein zweiter, davon unabhängiger Defekt in der Tabelle: TableViewControl ändert seine Auswahl (_selectedKeys, Cursor, Anker), benachrichtigt dann seine SelectionListener und schickt erst danach die Zeilenzustände an den Client. Wirft die Tabellenbindung beim Schreiben des Channels das Veto, behält die Tabelle serverseitig die neue Auswahl, während der Client die alte zeigt. Nach "Verwerfen" findet TableSelectionBinding.applyChannelValue() den neuen Wert bereits "angezeigt" und ruft selectRow nicht auf; nach "Abbrechen" hält die Tabelle eine Auswahl, die weder der Client noch der Channel zeigt.
Lösung
Das Veto wird vor dem äußeren Schreibzugriff eingeholt, transitiv über die Schreibzugriffe der Listener - so wie OpenDialogAction bereits ViewChannel.dirtyHandlers() der gebundenen Channels vor dem Öffnen befragt.
- ViewChannel.VetoListener.checkVeto und checkDirty liefern eine Liste von StateHandlern (leer = erlaubt) statt eines einzelnen, sodass ein Listener mit allem antworten kann, was ein von ihm geschriebener Channel beanstanden würde. DefaultViewChannel und DerivedViewChannel sammeln die Antworten und melden jeden Handler einmal (VetoCollector); FormControl antwortet mit sich selbst.
- VetoForwarder.forward(source, target) (Paket com.top_logic.layout.view.channel) registriert auf source einen Veto-Listener, der target.dirtyHandlers() beantwortet, und liefert das Aufräum-Runnable. Jede Komponente, die target aus einem ChannelListener von source schreibt, verwendet ihn; die Continuation der ChannelVetoException wiederholt dann den Schreibzugriff auf source, sodass nach "Verwerfen" alle Listener laufen.
- ObjectListControl leitet vom Container-Channel auf den Entwurfs-Channel weiter, solange die Liste eine <new-element>-Vorlage hat. ReactAdaptiveDetailControl leitet von jedem reset-on-Master auf seinen Auswahl-Channel weiter, FlowDiagramElement von jedem Eingabe-Channel auf den Auswahl-Channel.
- DerivedViewChannel verwaltet seine Veto-Listener und leitet von jedem Eingabe-Channel auf sich selbst weiter (checkDirty-Semantik, da der abgeleitete Wert vor dem Schreiben der Eingabe nicht bekannt ist). Ein Formular an einem abgeleiteten Channel legt damit korrekt ein Veto ein; der bidirektionale set() erreicht dieselben Listener über den Eingabe-Channel.
- Ein Listener, der ohne Weiterleitung einen anderen Channel schreibt, bleibt ein Programmierfehler und ist an ViewChannel.ChannelListener dokumentiert; eine halb gelaufene Benachrichtigung wird nicht dynamisch zurückgerollt.
- Die Auswahländerung der Tabelle ist eine Transaktion, die ihre Listener ablehnen können. TableViewControl.pushSelection() schreibt die Auswahl in die TableView, benachrichtigt die SelectionListener und übernimmt erst dann den Zustand (Schlüssel, Cursor, Anker) als bestätigt. Wirft ein Listener eine ChannelVetoException, stellt die Tabelle den zuletzt bestätigten Zustand wieder her und wirft weiter; der Client zeigt damit genau das, was die Tabelle hält. Pfade, die die Auswahl ohne Listener ändern (verschwundene Zeilen nach einem Refresh, Cursor auf einem Gruppenkopf), bestätigen den Zustand ebenfalls, damit eine aufgegebene Zeile nicht durch die Wiederherstellung zurückkehrt. TableSelectionBinding merkt sich die angezeigten Schlüssel erst nach dem geglückten Schreibzugriff; der wiederholte Schreibzugriff nach "Verwerfen" findet den neuen Wert nicht angezeigt und wählt die Zeile über selectRow.
Damit wird der Ticketwechsel bei schmutzigem Composer vor dem Schreiben des Ticket-Channels abgelehnt; nach "Verwerfen" wiederholt die Continuation den äußeren Schreibzugriff, und Adresszeile, Tabellenmarkierung, Ticket-Text und Verlauf zeigen das neue Ticket. Nach "Abbrechen" bleibt alles beim alten, der Composer behält seinen Text.
Nicht geändert: TreeElement schreibt den Auswahl-Channel aus dem SelectionListener eines SelectionModel (anderer Mechanismus); ob ReactTreeControl bei einem Veto seine Auswahl behält, wurde nicht geprüft.
Tests: TestTransitiveChannelVeto (verschachtelter Schreibzugriff, Weiterleitung, Deduplizierung, zwei Ebenen, abgeleiteter Channel, bidirektional), TestObjectListVeto (Containerwechsel mit schmutzigem Entwurfsformular), TestTableSelectionBinding (abgelehnte und abgebrochene Auswahl) in tl-layout-view; TestTableSelectionVeto (Wiederherstellung der Auswahl, verschwundene Zeile) in tl-layout-react. docs/faq/react-view-layer.md beschreibt den Mechanismus. Im Browser auf der Objektliste-Seite der React-Demo verifiziert (Verwerfen, Abbrechen, Wechsel ohne Entwurf); keine Fehler im Server-Log und in der Browser-Konsole.