enhancement
major
minor
major
minor
major
#29533
TL Views: executability rules and derived channels must follow changes of the object on a channel, not only of the channel value
Problem
Two mechanisms of the view layer (com.top_logic.layout.view) react to a change of a channel's value only. When the object on the channel stays the same but one of its attributes changes - the common case after a command or a form save - they do not re-evaluate, although their result depends on that attribute.
1. Executability rules of commands
ViewCommandModel registers itself as ChannelListener on the command's input channel only (_inputChannel.addListener(this)) and re-evaluates its ViewExecutabilityRule in the listener. A rule such as
{{{#!xml <generic-command input="ticket" ...>
<executability>
<visible-if expr="t -> $t.get(tl.dev:Ticket#status).get(tl.dev:TicketStatus#category) == tl.dev:StatusCategory#Open"/>
</executability>
...
</generic-command> }}}
is therefore evaluated when a different ticket arrives on the channel, but not when the command itself (or anything else) changes the ticket's status. A workflow button "Accept" stays visible after it has been executed.
The workaround found in practice: declare the commands inside the <form input="ticket"> instead of on the enclosing <panel>, because FormElement revalidates the command models it contributed on every form-state change, and FormControl fires one when the displayed object is updated. That couples the placement of a command to an accidental property of the form and does not help a command in a panel without a form (a table toolbar, an app bar).
2. Derived channels
DerivedViewChannel attaches a refresh listener to its input channels only and recomputes on their value changes (guarded by Objects.equals). A derived channel whose expression reads an attribute of the input object -
{{{#!xml <derived-channel name="draft" inputs="target"
expr="x -> new(tl.dev:Ticket, context: $x.get(tl.dev:TicketTarget#project), transient: true)"/>
}}}
- never recomputes when a form field writes TicketTarget#project, because the channel still holds the same target object. The elements that do react to model changes (<table>, <switch>, <tree>, <object-list>) each carry their own observed-types handling; channels have no such seam, and a form field cannot publish its value to a channel either.
The workaround found in practice: a dialog that cannot rebuild its draft when the project field changes is split into two dialogs (choose the project, then edit the draft).
Solution
Shared observation seam
A single class ChannelObjectObserver (com.top_logic.layout.view.model) observes, in the window's ModelScope, the TLObject`s currently held by a list of channels (a single object or a collection of objects) together with an optional set of ''observed types''. When a channel value changes, the observer re-points its object listeners at the new value; every model change of an observed object or type is forwarded to a callback. Lifecycle: `attach(ModelScope) / detach(), like RowSourceObserver. The helper ObservedTypes.resolve turns an observed-types configuration into the observed TLStructuredType`s and replaces the identical private copies in the table, tree, switch, object-list and calendar elements. `ReactSwitchControl and the scroll link's target observation use the shared observer instead of their own bookkeeping.
Commands
ViewCommandModel.attach(ModelScope) observes the objects on the command's input channel through the shared observer and re-evaluates the executability rule whenever an observed object changes, in addition to a new channel value; every command carrier (panel and menu toolbars, buttons, app bar, form-contributed commands) passes the window's model scope when its control is attached. TLObject inputs are always observed; the optional observed-types attribute on a command adds types for rules that navigate beyond the input object (e.g. to its container):
{{{#!xml <generic-command input="ticket" observed-types="my.module:Project">
<executability>
<visible-if expr="t -> ..."/>
</executability>
</generic-command> }}}
A workflow command declared on a panel or app bar toolbar now hides or appears when its own execution changes the input object's state.
Derived channels
<derived-channel> observes the objects on its input channels the same way and recomputes when one of them changes; listeners are still notified only if the recomputed value differs. The optional observed-types attribute is available here too. Observation follows the display: DerivedViewChannel implements the interface ObservingChannel (attach(ModelScope) / detach()), and ViewElement attaches the channels it instantiates when its root control is attached and detaches them when it disappears; a channel catches up by recomputing once when it is attached.
Tests
Knowledge-base backed tests in tl-layout-view (TestChannelObjectObserver, TestViewCommandModelObservation, TestDerivedViewChannelObservation, based on AbstractDBKnowledgeBaseTest with a GlobalModelEventForwarder as model scope): an executability rule and a derived channel over an attribute re-evaluate after the attribute is set in a transaction while the channel value is unchanged; a detached observer, a change of an unrelated object, a recomputation to an equal value, and the previous object after the channel switched to another one do not trigger a re-evaluation; attaching catches up with a change made while detached. The module gained the test-webapp configuration (src/test/webapp/WEB-INF/conf) needed to run knowledge-base tests.
Demo
tl-demo-react, view "Object list" (tickets.view.xml): demo.tickets:Ticket gets a status (enum TicketStatus: open, closed), the ticket panel's toolbar gets the commands "Close" / "Reopen" guarded by <visible-if> on the status, and a derived channel ticketState shows the status as text above the filter. Closing a ticket hides "Close", shows "Reopen" and switches the text while the selection stays unchanged.