Skip to content

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

Open
codemonkeychris wants to merge 2 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 2 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

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; } = [];
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);
…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); heterogeneous nodes use a switch in the
viewBuilder (the ItemTemplateSelector pattern). TreeViewNodeData.ContentElement
is marked [Obsolete] pointing at TreeView<T>; the legacy path stays functional
(CS0618 suppressed at internal use sites).

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)" issue a declarative {Binding Content} approach suffered
(a recycled container retained the element's visual parent). node.Content holds
the data item; the ItemInvoked / Expanding trampolines read T back from it.

The internal TreeViewList only exists once the template applies (in-tree), so
we subscribe its ContainerContentChanging on Loaded. Initial containers can
realize before that subscription, and under NativeAOT they may not be ready yet
(ContentTemplateRoot null; the realized container is even still the base
ListViewItem) — so the initial population filters on ContentControl (not
TreeViewItem) and re-attempts on the list's LayoutUpdated (bounded) until each
realized container is ready. Update reconciles the realized containers' views
via the flattened ListView.Items by index (ContainerFromItem does not resolve
under AOT), decoupled from the keyed node-structure diff.

- Element.cs: TemplatedTreeViewElementBase (object-erased base) +
  TemplatedTreeViewElement<T>; OwnPropsEqual arm.
- Dsl.cs: TreeView<T> factory overloads (explicit + IReactorKeyed).
- ElementExtensions.Events.cs: .ItemInvoked<T> / .Expanding<T> fluents.
- Reconciler: empty-shell ItemTemplate, ContainerContentChanging hosting on the
  internal TreeViewList, LayoutUpdated initial-host catch-up, index-based Update
  reconcile, 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; render
  assertions pump via WaitFor for async realization.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codemonkeychris codemonkeychris force-pushed the fix/447-treeview-typed-tree branch from 1db6826 to 09f1765 Compare May 30, 2026 07:02
Adds an 'aot' target (and 'all' coverage) to the CI Stress workflow that
publishes the NativeAOT selftest host once per shard and runs --self-test N
times, flagging any iteration with a nonzero exit or a 'not ok' TAP line and
uploading the failing TAP. Optional 'filter' input scopes to a fixture prefix
(e.g. TTV_). Used to hunt intermittent AOT-only failures.

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