Skip to content

fix(treeview): typed data-driven TreeView<T> with per-container hosting (#447)#453

Open
codemonkeychris wants to merge 4 commits into
mainfrom
fix/447-treeview-typed-tree
Open

fix(treeview): typed data-driven TreeView<T> with per-container hosting (#447)#453
codemonkeychris wants to merge 4 commits into
mainfrom
fix/447-treeview-typed-tree

Conversation

@codemonkeychris
Copy link
Copy Markdown
Collaborator

Summary

Closes #447 — a TreeView whose TreeViewNodeData nodes carry a ContentElement rendered blank rows. A node-mode WinUI TreeView stringifies TreeViewNode.Content and cannot host a pre-built UIElement; rich per-node visuals must come from a template (a data → Element function), never an element instance.

This adds a typed, data-driven TreeView<T> — the hierarchical peer of ListView<T>/GridView<T>/FlipView<T>:

TreeView(
    items,
    keySelector:      n => n.Id,
    childrenSelector: n => n.Children,        // the hierarchy
    viewBuilder:      n => /* data → Element */)   // the per-node "template"
// + IReactorKeyed overload that drops keySelector

Heterogeneous nodes with per-shape visuals are a switch inside the viewBuilder (the C# equivalent of WinUI's ItemTemplateSelector). OnItemInvoked / OnExpanding are Action<T> and hand back the developer's own T.

TreeViewNodeData.ContentElement is marked [Obsolete] pointing at TreeView<T>; the legacy path stays functional (CS0618 suppressed at the internal use sites).

Hosting — aligned with WinUI's model

Hosting mirrors the typed ListView<T>, which is how WinUI itself drives rich item content:

  • The ItemTemplate is an empty ContentControl shell.
  • Each node's view is mounted imperatively into the realized container via the internal TreeViewList's ContainerContentChanging — a fresh view on realize, unmounted on recycle. (We don't set args.Handled, so the TreeViewList's own handler keeps doing its indentation/selection work.)
  • node.Content holds the developer's data item; the ItemInvoked / Expanding trampolines read T back from it.

This keeps expand/collapse robust under container recycling, fixing the "every other expand/collapse blanks the first child row(s)" regression that an earlier declarative {Binding Content} approach suffered (a recycled/collapsed container retained the element's visual parent, so a different pooled container couldn't re-host it).

Update reconciles only the realized containers' views in place, with a minimal-mutation node reorder so unchanged-order updates touch the live node collection not at all; unrealized nodes rebuild from node.Content on next realization. Full-tree unmount walks realized containers so each node-view Component's cleanups run.

Files

  • src/Reactor/Core/Element.csTemplatedTreeViewElementBase (object-erased base so the reconciler switch matches one type) + TemplatedTreeViewElement<T>; OwnPropsEqual arm. Value-type T is boxed once; reference-type T flows through the covariant IReadOnlyList<object> conversion.
  • src/Reactor/Elements/Dsl.cs — the two TreeView<T> factory overloads.
  • src/Reactor/Core/Reconciler*.cs — empty-shell ItemTemplate, ContainerContentChanging hosting on the internal TreeViewList (found + cached per TreeView), keyed in-place node diff, full-tree unmount walk.
  • src/Reactor/Core/V1Protocol/ChildrenStrategy.cs — CS0618 suppression around the legacy ContentElement reads.
  • samples/Reactor.TestApp/Demos/DataTemplateDemo.cs — section 4 migrated from ContentElement to TreeView<T> with a discriminated PetNode model (distinct templates via switch).

Tests

tests/Reactor.AppTests.Host/SelfTest/Fixtures/TemplatedTreeViewFixtures.cs (TTV_): rich-content render, heterogeneous per-shape templates, in-place keyed reconcile, structural add, expand/collapse cycles stay rendered, event-erasure T resolution, value-type T, IsExpanded selector, unmount teardown.

  • Builds clean on ARM64 (src/Reactor, TestApp, self-test host) — 0 errors, no CS0618 leakage.
  • Full self-test suite green (4454 ok, 0 failures), including a fixture that drives several collapse→expand cycles and asserts every child row stays rendered.
  • The headless harness renders at full window size and can't fully reproduce virtualization-recycle behavior, so the expand/collapse fix should also be eyeballed in the TestApp (DataTemplate Demo → section 4).

🤖 Generated with Claude Code

…ng (#447)

A node-mode WinUI TreeView stringifies TreeViewNode.Content and cannot host
a pre-built UIElement, so TreeViewNodeData.ContentElement rendered blank
rows (#447). Add a typed, data-driven TreeView<T> — the hierarchical peer of
ListView<T> — that renders each node from a data -> Element viewBuilder (the
WinUI ItemTemplate equivalent), with heterogeneous nodes handled by a switch
in the viewBuilder (the ItemTemplateSelector pattern).

Hosting mirrors the typed ListView<T>: the ItemTemplate is an empty
ContentControl shell and each node view is mounted imperatively into the
realized container via the internal TreeViewList's ContainerContentChanging
(fresh mount on realize, unmount on recycle). This keeps expand/collapse
robust under container recycling — fixing the "every other expand/collapse
blanks the first child row(s)" regression that the earlier declarative
{Binding Content} approach suffered (a recycled container retained the
element's visual parent). node.Content holds the data item; the ItemInvoked
and Expanding trampolines read T back from it.

TreeViewNodeData.ContentElement is marked [Obsolete] pointing at TreeView<T>;
the legacy path stays functional (CS0618 suppressed at internal use sites).

- Element.cs: TemplatedTreeViewElementBase (object-erased base) +
  TemplatedTreeViewElement<T>; OwnPropsEqual arm.
- Dsl.cs: TreeView<T> factory overloads (explicit + IReactorKeyed).
- Reconciler: empty-shell ItemTemplate, ContainerContentChanging hosting on
  the internal TreeViewList, keyed in-place node diff that reconciles only
  realized containers, full-tree unmount walk.
- Samples: DataTemplateDemo section 4 migrated to a discriminated PetNode model.
- Tests: TemplatedTreeViewFixtures (TTV_) — render, heterogeneous templates,
  keyed update, expand/collapse cycles, events, value-type T, unmount.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a typed, data-driven TreeView<T> that renders rich per-node visuals via a viewBuilder, aligning TreeView with existing typed collection controls and avoiding legacy TreeViewNodeData.ContentElement hosting issues.

Changes:

  • Introduces TemplatedTreeViewElement<T> and DSL factory overloads.
  • Adds reconciler mount/update/unmount support for per-container TreeView node hosting.
  • Migrates sample/demo usage and adds self-test fixtures for rendering, updates, events, value types, expansion, and unmount.
Show a summary per file
File Description
src/Reactor/Core/Element.cs Adds typed TreeView element model and obsoletes legacy ContentElement.
src/Reactor/Elements/Dsl.cs Adds TreeView<T> factory overloads.
src/Reactor/Core/Reconciler.Mount.cs Mounts typed TreeView nodes and hosts realized container content.
src/Reactor/Core/Reconciler.Update.cs Adds keyed hierarchical update/reconcile for typed TreeView.
src/Reactor/Core/Reconciler.cs Adds typed TreeView container cache and unmount traversal.
src/Reactor/Core/V1Protocol/ChildrenStrategy.cs Suppresses obsolete warnings for legacy TreeView content reads.
samples/Reactor.TestApp/Demos/DataTemplateDemo.cs Updates demo to use typed TreeView<T>.
tests/Reactor.AppTests.Host/SelfTest/* Registers and adds typed TreeView self-test fixtures.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 9/9 changed files
  • Comments generated: 2

Comment on lines +4161 to +4162
bool expanded = newEl.GetIsExpanded(newItem);
if (node.IsExpanded != expanded) node.IsExpanded = expanded;
public bool CanDragItems { get; init; }
public bool AllowDrop { get; init; }
public bool CanReorderItems { get; init; }
internal Action<WinUI.TreeView>[] Setters { get; init; } = [];
codemonkeychris and others added 2 commits May 29, 2026 14:00
… attach

Two CI failures on the typed TreeView<T> change:

Unit Tests — PublicApiSurfaceGuardTests.EveryCallbackPropertyHasMatchingFluent
required every Action/Action<T> callback property to have a matching fluent
extension. Add `.ItemInvoked<T>` / `.Expanding<T>` for TemplatedTreeViewElement<T>
in ElementExtensions.Events.cs (mirrors the existing TreeViewElement fluents).

AOT Selftests — every visual-render TTV_ assertion failed while element-level
ones passed: node views were never hosted. Hosting was deferred to the
TreeView's Loaded event, which (unlike the typed ListView, which subscribes to
ContainerContentChanging at mount) does not fire reliably in the headless AOT
host before the fixture asserts. The internal TreeViewList only exists after the
template applies, so we can't subscribe at mount — instead drive the attach from
a bounded dispatcher retry that lands on the first pump (creating the
subscription before/around the first realization and catching any later
realization within the same render), with Loaded kept only as a backup for the
show-later case (e.g. a tree in an unselected tab). FindTypedTreeListControl no
longer writes the subscription-marker cache (read-only on the Update path).

Full self-test suite green (4454 ok, 0 failures).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(AOT)

The AOT selftest failed deterministically on every TTV_ fixture that asserted
rendered node text after a single Render(), while JIT passed. Root cause is not
a product bug: per-node views host into their containers when the TreeView
realizes them, which lands a dispatcher cycle after mount — and the NativeAOT
host consistently needs one more pump cycle than JIT to get there (visible in
that TTV_AddChild_NewNodeRenders, which already did two render passes, passed
under AOT while the single-Render fixtures did not). Asserting after exactly one
Render() was a test-harness assumption, not a runtime contract.

Fix at the test layer: add a bounded WaitFor(condition) helper that pumps render
passes until the rendered content appears, and use it for the render-dependent
assertions. A genuinely blank/missing row never appears within the budget, so
the regression coverage (including the expand/collapse-cycle test) is preserved.

Also reverts the previous attempt to make hosting robust via a dispatcher-retry
attach: JIT selftests passed with the plain Loaded subscription, so Loaded does
fire in the headless host — the retry addressed a non-issue and is removed.
Hosting is back to subscribing the internal TreeViewList's
ContainerContentChanging on Loaded (idempotent), with no runtime side-effects
added for the test's sake.

Full self-test suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
H.Check("TTV_IsExpanded_SelectedNodeExpanded",
tv!.RootNodes.Count == 2 && tv.RootNodes[0].IsExpanded);
H.Check("TTV_IsExpanded_OtherNodeCollapsed",
!tv.RootNodes[1].IsExpanded);
H.Check("TTV_IsExpanded_TreeFound", tv is not null);
// First root ("docs") is expanded; the second ("pics") is not.
H.Check("TTV_IsExpanded_SelectedNodeExpanded",
tv!.RootNodes.Count == 2 && tv.RootNodes[0].IsExpanded);
Temporary # TTVDIAG TAP-comment logging in the typed TreeView hosting path to
capture why initial node containers don't host under the NativeAOT selftest
host. To be reverted once root cause is confirmed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] TreeView does not render per-node ContentElement (ListView/GridView do) -

2 participants