Skip to content

Commit 9591f1b

Browse files
fix(treeview): CI — add typed-tree fluents + robust container-hosting 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>
1 parent 3ecac96 commit 9591f1b

3 files changed

Lines changed: 61 additions & 23 deletions

File tree

src/Reactor/Core/Reconciler.Mount.cs

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2007,10 +2007,19 @@ private WinUI.TreeView MountTemplatedTreeView(TemplatedTreeViewElementBase el, A
20072007
treeView.ItemInvoked += TemplatedTreeItemInvoked;
20082008
treeView.Expanding += TemplatedTreeExpanding;
20092009

2010-
// The internal TreeViewList ("ListControl") only exists once the
2011-
// template applies (after the control enters the visual tree). Hook its
2012-
// ContainerContentChanging on Loaded, then populate the already-realized
2013-
// initial containers (which materialized before we could subscribe).
2010+
// Hook the internal TreeViewList ("ListControl") ContainerContentChanging
2011+
// so node views are mounted into their realized containers. The
2012+
// ListControl only exists once the template applies (after the first
2013+
// layout, in-tree), so we can't subscribe at mount the way the typed
2014+
// ListView does. Drive the attach from a bounded dispatcher retry rather
2015+
// than relying solely on Loaded: Loaded depends on the element being
2016+
// connected to a live/activated visual tree (it does not fire reliably in
2017+
// headless hosts), whereas a queued callback drains on the next dispatcher
2018+
// pump — by which point the first layout has created the ListControl and
2019+
// realized the initial containers. Loaded stays as a backup for the
2020+
// show-later case (e.g. a tree inside an unselected tab). The attach is
2021+
// idempotent (presence in _typedTreeListControls guards it).
2022+
ScheduleTypedTreeHosting(treeView, requestRerender, attemptsLeft: 12);
20142023
treeView.Loaded += (s, _) => AttachTypedTreeHosting((WinUI.TreeView)s!, requestRerender);
20152024

20162025
el.ApplyControlSetters(treeView);
@@ -2033,26 +2042,48 @@ private static WinUI.TreeViewNode BuildTemplatedTreeNode(TemplatedTreeViewElemen
20332042
return node;
20342043
}
20352044

2045+
/// <summary>
2046+
/// Attempts to attach hosting; if the internal list isn't realized yet,
2047+
/// re-queues itself on the dispatcher (up to <paramref name="attemptsLeft"/>
2048+
/// times) so the subscription lands as soon as the first layout creates the
2049+
/// ListControl — without depending on the Loaded event.
2050+
/// </summary>
2051+
private void ScheduleTypedTreeHosting(WinUI.TreeView treeView, Action requestRerender, int attemptsLeft)
2052+
{
2053+
if (TryAttachTypedTreeHosting(treeView, requestRerender)) return;
2054+
if (attemptsLeft <= 0) return;
2055+
// DispatcherQueue can be null in odd hosting/teardown states — Loaded
2056+
// remains the backup path there.
2057+
treeView.DispatcherQueue?.TryEnqueue(
2058+
() => ScheduleTypedTreeHosting(treeView, requestRerender, attemptsLeft - 1));
2059+
}
2060+
2061+
/// <summary>Loaded-backup entry point (see <see cref="ScheduleTypedTreeHosting"/>).</summary>
2062+
private void AttachTypedTreeHosting(WinUI.TreeView treeView, Action requestRerender) =>
2063+
TryAttachTypedTreeHosting(treeView, requestRerender);
2064+
20362065
/// <summary>
20372066
/// Subscribes the typed TreeView's internal list to ContainerContentChanging
2038-
/// (once) and populates any containers that realized before the
2039-
/// subscription. Idempotent across Loaded/Unloaded cycles.
2067+
/// (once) and populates any containers that realized before the subscription.
2068+
/// Returns true once attached. Idempotent — presence in
2069+
/// <see cref="_typedTreeListControls"/> marks "already subscribed".
20402070
/// </summary>
2041-
private void AttachTypedTreeHosting(WinUI.TreeView treeView, Action requestRerender)
2071+
private bool TryAttachTypedTreeHosting(WinUI.TreeView treeView, Action requestRerender)
20422072
{
2043-
// FindTypedTreeListControl caches; if it was already cached the
2044-
// subscription is already in place — don't double-subscribe.
2045-
if (_typedTreeListControls.TryGetValue(treeView, out _)) return;
2046-
var list = FindTypedTreeListControl(treeView);
2047-
if (list is null) return;
2073+
if (_typedTreeListControls.TryGetValue(treeView, out _)) return true; // already subscribed
20482074

2075+
var list = FindDescendantListView(treeView);
2076+
if (list is null) return false; // template not applied yet
2077+
2078+
_typedTreeListControls.Add(treeView, list); // mark subscribed + cache for Update
20492079
list.ContainerContentChanging += (_, args) =>
20502080
OnTypedTreeContainerContentChanging(treeView, args, requestRerender);
20512081

2052-
// Populate the initial visible containers that realized before we hooked.
2082+
// Populate any containers that already realized before we subscribed.
20532083
for (int i = 0; i < list.Items.Count; i++)
20542084
if (list.ContainerFromIndex(i) is WinUI.TreeViewItem container)
20552085
PopulateTypedTreeContainer(treeView, container, list.Items[i], requestRerender);
2086+
return true;
20562087
}
20572088

20582089
/// <summary>

src/Reactor/Core/Reconciler.cs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2026,17 +2026,16 @@ private void UnmountTypedTreeViewContainersRecursive(DependencyObject node)
20262026

20272027
/// <summary>
20282028
/// Finds the typed TreeView's internal <c>TreeViewList</c> (template part
2029-
/// "ListControl", a public <see cref="WinUI.ListView"/> subclass) by walking
2030-
/// the visual subtree. Cached per TreeView in <see cref="_typedTreeListControls"/>.
2029+
/// "ListControl", a public <see cref="WinUI.ListView"/> subclass). Returns
2030+
/// the cached instance when hosting is already attached; otherwise walks the
2031+
/// visual subtree without caching — only <c>AttachTypedTreeHosting</c> writes
2032+
/// <see cref="_typedTreeListControls"/> (its presence marks "subscribed", so
2033+
/// a read-side cache write here would suppress the real subscription).
20312034
/// </summary>
2032-
internal WinUI.ListView? FindTypedTreeListControl(WinUI.TreeView treeView)
2033-
{
2034-
if (_typedTreeListControls.TryGetValue(treeView, out var cached))
2035-
return cached;
2036-
var found = FindDescendantListView(treeView);
2037-
if (found is not null) _typedTreeListControls.Add(treeView, found);
2038-
return found;
2039-
}
2035+
internal WinUI.ListView? FindTypedTreeListControl(WinUI.TreeView treeView) =>
2036+
_typedTreeListControls.TryGetValue(treeView, out var cached)
2037+
? cached
2038+
: FindDescendantListView(treeView);
20402039

20412040
private static WinUI.ListView? FindDescendantListView(DependencyObject root)
20422041
{

src/Reactor/Elements/ElementExtensions.Events.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,14 @@ public static TreeViewElement ItemInvoked(this TreeViewElement el, Action<TreeVi
291291
public static TreeViewElement Expanding(this TreeViewElement el, Action<TreeViewNodeData>? handler) =>
292292
el with { OnExpanding = handler };
293293

294+
/// <summary>Wires the item-invoked handler for the typed tree (receives the developer's <c>T</c>). Passing <c>null</c> clears.</summary>
295+
public static TemplatedTreeViewElement<T> ItemInvoked<T>(this TemplatedTreeViewElement<T> el, Action<T>? handler) =>
296+
el with { OnItemInvoked = handler };
297+
298+
/// <summary>Wires the expanding handler for the typed tree (receives the developer's <c>T</c>). Passing <c>null</c> clears.</summary>
299+
public static TemplatedTreeViewElement<T> Expanding<T>(this TemplatedTreeViewElement<T> el, Action<T>? handler) =>
300+
el with { OnExpanding = handler };
301+
294302
/// <summary>Wires the selected-index-changed handler. Passing <c>null</c> clears.</summary>
295303
public static FlipViewElement SelectedIndexChanged(this FlipViewElement el, Action<int>? handler) =>
296304
el with { OnSelectedIndexChanged = handler };

0 commit comments

Comments
 (0)