enhancement
major
minor
major
minor
major
#29502
<tree> does not follow model changes: a delete never arrives, a create collapses the whole tree
Reported from the Consulting application on 8.0.0-alpha7 (react view UI, Issue #94), verified in the browser on 2026-08-31.
We replaced a two-table master-detail (tasks, and the actions of the selected task) with a single <tree> over consulting:Project#tasks / consulting:Task#actions. Reading works well. Writing does not: the tree and the model drift apart, and nothing in the element's configuration can close the gap.
1. A delete never reaches the tree
Removing the selected node's business object from its composition inside a <with-transaction> commits correctly — the detail form clears, the object is gone from the database — but **the node stays in the tree**. It disappears only later, when something unrelated rebuilds the root. Until then the tree shows an object that no longer exists, and it can still be selected.
observed-types is set to both types:
{{{#!xml <tree observed-types="consulting:Task, consulting:Action" selection="selected">
<inputs><input channel="project"/></inputs>
<root>project -> $project</root>
<children>project -> node -> ...</children>
</tree> }}}
ObservableTreeModel's own class comment says it "rebuilds the full tree for creates", so a removal from a composition appears not to be a case it handles at all.
2. A create collapses every expanded node
Creating an object of an observed type does reach the tree, but only as a full rebuild of the root. The consequences, all visible to the user in one step:
- every expanded task collapses — in our project that is 39 top-level nodes, and the user has to find and re-open the one they were working in;
- the new node is **not revealed**: it sits under a collapsed parent;
- the new node is **not selected in the tree**, even though the creating command writes it to the selection channel. The detail form beside the tree shows the new object while the tree shows no selection at all — the two disagree on screen.
3. There is nothing to fix it with
TreeElement's own Javadoc:
Note: The optional config properties isLeaf, supportsNode, modelForNode, parents, and nodesToUpdate are declared for future use and currently not wired into the runtime. They parse correctly but have no effect. ... Incremental update support will be added when the view system gains model event integration.
So the two properties that would carry exactly this — parents (to walk from a new object to the nodes that must be expanded) and nodesToUpdate (to update a subtree instead of the root) — parse and do nothing. An application cannot reach the tree model from outside the element either.
What we would expect
- A removal from a composition removes the node, like a create adds one.
- An incremental update where the model event allows it, rather than a root rebuild.
- The expansion state surviving a rebuild where one is unavoidable.
- Writing an object to the tree's selection channel selecting (and revealing) that node, so a created object is where the user is looking. Today the channel is written by the tree but not read back by it.
What we did instead
Nothing — no workaround. The view is shipped with the gap documented in place, and reloading the page always shows the correct state. Filed here rather than worked around, in line with #29500, #29501, #29491.
Related
- #29501 — activating a row from the list; same area, the interaction side.
- #29500 — detail as an overlay.
Lösung
Incremental tree updates. The tree's model observer (ObservableTreeModel) keeps its tree model across model events instead of rebuilding it from the root. On a model event it reconciles the child list of every node whose children are materialized, keyed by business object: nodes whose object is still in the recomputed list are kept, so their expansion and identity survive; new objects get a node at their position; vanished objects lose theirs. One reconcile serves all event kinds: a delete removes the node and reconciles the node it hung in; an update of an observed object reconciles that node's children and the child list of the node it hangs in (the removal from a composition, and a removal over a backwards reference such as Milestone#tickets, where only the child is reported as changed) and, with parents configured, the node holding the object now, so a move lands where it went; a create of an observed type reconciles every materialized node. Only a change of an input channel replaces the model; there the expansion is saved and restored through tree-node helpers (TreeNodes) shared with the view designer's tree. When the tree resumes observing after it was hidden (inactive tab), it reconciles everything, like the row observer of tables and lists does since #29610.
Two-way selection channel. TreeSelectionBinding also reads the channel, like the table's binding since #29535: a value written to the channel is selected in the tree, its ancestors are expanded so the node is revealed, and the client scrolls it into view. The value is applied again after every structure change, so an object a command creates and writes to the channel is selected as soon as its node exists. When a selected node vanishes, the surviving selection is written back to the channel, null when nothing is left. A value the tree has no node for leaves both the channel and the tree's selection alone. A deleted object is never handed to the node lookup (nothing can be computed over it), so it drops out of the selection instead of breaking the channel's derived values. The node for an object is found by a NodeLocator: among the materialized nodes first, then by a search that materializes children on the way down; with the optional parents expression (node -> parent) configured, the reveal helper for large or unbounded trees, the tree walks from the object up to the root and expands along that path instead of searching.
Configuration. parents is wired as described. The unused properties isLeaf, supportsNode, modelForNode and nodesToUpdate are removed together with the Javadoc note; nodesToUpdate is superseded by the reconcile driven by the model event.
Resource cell. ReactResourceCellControl, which re-resolves its own display on a change of the object it shows, leaves a deleted object alone instead of resolving a color over an object that has no type any more.
Verification. The tree demo of tl-demo-react observes its types, names the parents of its nodes and has commands to create a milestone in the selected project scope (through a dialog), create a ticket in the selected milestone (without a dialog), detach the selected ticket from its milestone (removal without delete) and delete the selection. Checked in the browser: created nodes appear under the still-expanded parent and are selected, a detached or deleted node disappears in place with the selection cleared, and a change made in another view is shown when the tree is displayed again. The table observation of comment 2 was checked on the tickets demo's delete command: the row disappears immediately; the table's row observer already re-reads its rows on every event of a displayed row, so no change was needed there.
Migration
The <tree> attributes isLeaf, supportsNode, modelForNode and nodesToUpdate no longer exist; a view declaring one of them fails to load and must drop it (they had no effect). ObservableTreeModel takes the parents function as an additional constructor argument, and TreeSelectionBinding is constructed with the tree control, its selection model, the current tree model, a NodeLocator and the channel.