Problem
Ein Eingabefeld (<value-input value="ch">) soll einen aus einem anderen Kanal abgeleiteten Wert anzeigen, den der Benutzer überschreiben kann. Typischer Fall: Oberhalb einer Tabelle steht ein Feld, das den gespeicherten Wert eines Attributs des gewählten Objekts zeigt (z.B. das Budget des gewählten Projekts); der Benutzer trägt einen neuen Wert ein, der per <on-submit> gespeichert wird; wird ein anderes Objekt gewählt, zeigt das Feld dessen Wert.
Dafür braucht es einen Kanal, der sowohl vom Eingabefeld beschrieben wird als auch bei Änderung eines anderen Kanals neu befüllt wird. Keine der beiden Kanalarten des View-Layers leistet das:
- <channel name="…"/> ist beschreibbar, startet aber leer und wird von nichts nachgefüllt. Es gibt keine Deklaration der Form „wenn sich project ändert, setze diesen Kanal auf das Budget des Projekts“. Die Aktion <write-channel> läuft nur innerhalb eines vom Benutzer ausgelösten Kommandos, nicht als Reaktion auf einen anderen Kanal.
- <derived-channel inputs="project" expr="…"/> wird bei jeder Selektionsänderung (und bei Änderung des beobachteten Objekts) neu berechnet, ist aber nur lesbar. Beschreibbar wird er nur mit reverse, und reverse schreibt sein Ergebnis per Vertrag in den ersten Eingangskanal. Für ein Budget müsste reverse aus einer Zahl ein Projekt machen und in den Kanal project schreiben.
Der einzige Ausweg ist ein Hilfskanal, der als erster Eingang des abgeleiteten Kanals nur dazu dient, das Rückschreiben aufzufangen:
{{{#!xml <channel name="enteredBudget"/> <derived-channel name="budget"
expr="entered -> p -> $p.get(tl.demo.projectManagement:ProjectScope#budget)"
inputs="enteredBudget, project"
reverse="b -> $b"
/> }}}
Das ist ein Workaround: Der Hilfskanal wird von nichts gelesen, und der Kanal budget, an den das Feld gebunden ist, hält weiter den gespeicherten Wert, während das Feld den eingetippten zeigt (die Neuberechnung liefert den alten Wert, es gibt keine Benachrichtigung). Kanal und Feld laufen auseinander; die Konstruktion funktioniert nur, weil das Submit-Kommando seinen Wert vom Feld und nicht vom Kanal bekommt.
Erwartet
Eine Deklaration für einen beschreibbaren Kanal, dessen Wert aus einem Ausdruck über andere Kanäle neu gesetzt wird, sobald sich einer dieser Kanäle (oder das darin gehaltene Objekt) ändert, etwa:
{{{#!xml <channel name="budget"
inputs="project"
init="p -> $p.get(tl.demo.projectManagement:ProjectScope#budget)"
/> }}}
Zwischen zwei Neubefüllungen hält der Kanal, was ein Element oder eine Aktion zuletzt hineingeschrieben hat. Ein daran gebundenes Eingabefeld zeigt den vorbelegten Wert, nimmt Eingaben an, und folgt der Selektion.
Anwendung in der Demo
com.top_logic.demo.react/src/main/webapp/WEB-INF/views/projects/overview.view.xml: Das mit Ticket #29808 eingeführte Feld „Neues Budget des gewählten Projekts“ (<value-input submit-on="change" value="budget"> oberhalb der Projekttabelle) ist an den einfachen Kanal budget gebunden und startet daher leer. Mit der neuen Deklaration soll es das gespeicherte Budget des in der Tabelle gewählten Projekts vorbelegen und bei Auswahl eines anderen Projekts dessen Budget zeigen.
Lösung
- <channel> hat die optionalen Eigenschaften inputs (Kanalreferenzen wie bei <derived-channel>), init (TL-Script-Funktion über die Eingangswerte) und observed-types (wie bei <derived-channel>). inputs oder observed-types ohne init ist ein Konfigurationsfehler; init ohne inputs gibt dem Kanal einen Anfangswert.
- Ein so konfigurierter Kanal bleibt beschreibbar. Er wird beim Anlegen aus init befüllt und neu befüllt,
- sobald ein Eingangskanal einen neuen Wert annimmt (immer, auch wenn init dasselbe liefert), und
- sobald ein in einem Eingang gehaltenes Objekt (bzw. ein Objekt der observed-types) geändert wird und init dadurch ein anderes Ergebnis liefert als bei der letzten Befüllung. Eine Änderung, die das Ergebnis nicht ändert (z.B. eine Umbenennung des Projekts), lässt einen eingetippten Wert stehen. Beim erneuten Anzeigen der View wird init neu berechnet und nur bei geändertem Ergebnis befüllt.
- Dazwischen hält der Kanal den zuletzt geschriebenen Wert, mit der Veto-/Dirty-Semantik eines normalen Kanals. Ungespeicherte Änderungen am Kanal blockieren das Schreiben eines Eingangs (Veto-Weiterleitung wie beim abgeleiteten Kanal).
- Die Anbindung an die Eingangskanäle (Listener, Veto-Weiterleitung, Beobachtung der Eingangsobjekte) ist in ChannelInputBinding zusammengefasst und wird von <derived-channel> und dem initialisierten Kanal gemeinsam genutzt.
- Demo (tl-demo-react, Projekte): das Budget-Feld wird aus dem gewählten Projekt vorbelegt, folgt der Auswahl und zeigt nach dem Absenden den gespeicherten Wert.
- Doku: Artikel view-layer/channels.