Skip to content

Commit 1db6826

Browse files
fix(treeview): AOT-robust Update reconcile + bounded initial-host handler
Two follow-ups after the initial-hosting fix landed under AOT: 1. TTV_KeyedUpdate failed under AOT because the Update path reconciled a matched node's view via list.ContainerFromItem(node) cast to TreeViewItem — ContainerFromItem doesn't resolve under NativeAOT and the container can be the base ListViewItem. Rework: decouple the structure diff from the view reconcile. DiffTemplatedTreeNodes now only updates the node hierarchy + node.Content; a separate RefreshRealizedTreeContainers pass walks the flattened ListView.Items by index (the AOT-robust lookup) and reconciles each realized container's view against its node's current data, at any depth. 2. Bound the initial-population LayoutUpdated handler to a fixed number of passes so it always detaches (CCC covers anything not yet hosted), avoiding any dangling per-list subscription. JIT self-test green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 60a8b8c commit 1db6826

2 files changed

Lines changed: 51 additions & 43 deletions

File tree

src/Reactor/Core/Reconciler.Mount.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2058,13 +2058,16 @@ private void AttachTypedTreeHosting(WinUI.TreeView treeView, Action requestReren
20582058
// later — observed under NativeAOT, where the realized container is even
20592059
// still the base ListViewItem at Loaded time), so re-attempt on
20602060
// LayoutUpdated until every realized container is hosted, then detach.
2061-
// Everything realized AFTER this point flows through CCC.
2061+
// Everything realized AFTER this point flows through CCC. The pass count
2062+
// is bounded so the handler always detaches (no dangling subscription),
2063+
// and CCC still covers anything not hosted by then.
20622064
if (!PopulateRealizedTreeContainers(treeView, list, requestRerender))
20632065
{
2066+
int passesLeft = 8;
20642067
EventHandler<object>? onLayout = null;
20652068
onLayout = (_, _) =>
20662069
{
2067-
if (PopulateRealizedTreeContainers(treeView, list, requestRerender))
2070+
if (PopulateRealizedTreeContainers(treeView, list, requestRerender) || --passesLeft <= 0)
20682071
list.LayoutUpdated -= onLayout;
20692072
};
20702073
list.LayoutUpdated += onLayout;

src/Reactor/Core/Reconciler.Update.cs

Lines changed: 46 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4104,12 +4104,13 @@ private void ReconcileTreeNodeContent(
41044104
/// </summary>
41054105
private UIElement? UpdateTemplatedTreeView(TemplatedTreeViewElementBase o, TemplatedTreeViewElementBase n, WinUI.TreeView tv, Action requestRerender)
41064106
{
4107-
// The internal list (if realized) lets us reconcile the views of
4108-
// currently-realized containers. Unrealized nodes need no work — their
4109-
// view is (re)built fresh from node.Content when they next realize.
4110-
var list = FindTypedTreeListControl(tv);
4111-
4112-
DiffTemplatedTreeNodes(tv.RootNodes, list, o, o.GetRoots(), n, n.GetRoots(), requestRerender);
4107+
// Diff the node hierarchy (structure + each node's data item). The view
4108+
// reconcile is a separate flat pass over the realized containers below —
4109+
// keeping the two concerns decoupled, and using the index-based container
4110+
// lookup that works under NativeAOT (ContainerFromItem does not resolve
4111+
// there, and a freshly-realized container can still be the base
4112+
// ListViewItem rather than TreeViewItem).
4113+
DiffTemplatedTreeNodes(tv.RootNodes, o, o.GetRoots(), n, n.GetRoots());
41134114

41144115
tv.SelectionMode = n.GetSelectionMode();
41154116
tv.CanDragItems = n.GetCanDragItems();
@@ -4118,9 +4119,45 @@ private void ReconcileTreeNodeContent(
41184119

41194120
SetElementTag(tv, n);
41204121
n.ApplyControlSetters(tv);
4122+
4123+
// Reconcile the view of every currently-realized container against its
4124+
// node's (now-updated) data. Unrealized nodes need no work — their view
4125+
// is (re)built fresh from node.Content when they next realize via CCC.
4126+
RefreshRealizedTreeContainers(tv, FindTypedTreeListControl(tv), n, requestRerender);
41214127
return null;
41224128
}
41234129

4130+
/// <summary>
4131+
/// Reconciles the hosted view of every realized container against its node's
4132+
/// current <c>node.Content</c> data. Iterates the flattened
4133+
/// <see cref="WinUI.ListView.Items"/> via index (the AOT-robust lookup), so
4134+
/// it covers visible nodes at every depth in one pass.
4135+
/// </summary>
4136+
private void RefreshRealizedTreeContainers(WinUI.TreeView tv, WinUI.ListView? list, TemplatedTreeViewElementBase n, Action requestRerender)
4137+
{
4138+
if (list is null) return;
4139+
for (int i = 0; i < list.Items.Count; i++)
4140+
{
4141+
if (list.ContainerFromIndex(i) is not ContentControl container) continue;
4142+
if (container.ContentTemplateRoot is not ContentControl cc) continue;
4143+
if (list.Items[i] is not WinUI.TreeViewNode node || node.Content is not { } data) continue;
4144+
4145+
var newView = n.BuildView(data);
4146+
if (cc.Content is UIElement existing && GetElementTag(cc) is Element oldView && CanUpdate(oldView, newView))
4147+
{
4148+
var replacement = Update(oldView, newView, existing, requestRerender);
4149+
if (replacement is not null && !ReferenceEquals(cc.Content, replacement))
4150+
cc.Content = replacement;
4151+
}
4152+
else
4153+
{
4154+
if (cc.Content is UIElement old) UnmountChild(old);
4155+
cc.Content = Mount(newView, requestRerender);
4156+
}
4157+
SetElementTag(cc, newView);
4158+
}
4159+
}
4160+
41244161
private static readonly object[] s_emptyTreeItems = [];
41254162

41264163
/// <summary>
@@ -4134,10 +4171,8 @@ private void ReconcileTreeNodeContent(
41344171
/// </summary>
41354172
private void DiffTemplatedTreeNodes(
41364173
IList<WinUI.TreeViewNode> liveNodes,
4137-
WinUI.ListView? list,
41384174
TemplatedTreeViewElementBase oldEl, IReadOnlyList<object> oldItems,
4139-
TemplatedTreeViewElementBase newEl, IReadOnlyList<object> newItems,
4140-
Action requestRerender)
4175+
TemplatedTreeViewElementBase newEl, IReadOnlyList<object> newItems)
41414176
{
41424177
// Snapshot: map old key → (live node, old item). Live nodes correspond
41434178
// 1:1 to oldItems in order.
@@ -4161,13 +4196,10 @@ private void DiffTemplatedTreeNodes(
41614196
bool expanded = newEl.GetIsExpanded(newItem);
41624197
if (node.IsExpanded != expanded) node.IsExpanded = expanded;
41634198

4164-
ReconcileRealizedTreeContainer(list, node, newEl, newItem, requestRerender);
4165-
41664199
DiffTemplatedTreeNodes(
4167-
node.Children, list,
4200+
node.Children,
41684201
oldEl, oldEl.GetChildren(match.OldItem) ?? s_emptyTreeItems,
4169-
newEl, newEl.GetChildren(newItem) ?? s_emptyTreeItems,
4170-
requestRerender);
4202+
newEl, newEl.GetChildren(newItem) ?? s_emptyTreeItems);
41714203

41724204
target.Add(node);
41734205
}
@@ -4206,33 +4238,6 @@ private static int IndexOfNode(IList<WinUI.TreeViewNode> nodes, WinUI.TreeViewNo
42064238
return -1;
42074239
}
42084240

4209-
/// <summary>
4210-
/// Reconciles the view hosted in a matched node's realized container (if it
4211-
/// is currently realized). Updates in place when the old/new views are
4212-
/// compatible, otherwise unmounts and re-mounts. Unrealized nodes are
4213-
/// skipped — they rebuild fresh from <c>node.Content</c> on next realization.
4214-
/// </summary>
4215-
private void ReconcileRealizedTreeContainer(
4216-
WinUI.ListView? list, WinUI.TreeViewNode node, TemplatedTreeViewElementBase newEl, object newItem, Action requestRerender)
4217-
{
4218-
if (list?.ContainerFromItem(node) is not WinUI.TreeViewItem container) return;
4219-
if (container.ContentTemplateRoot is not ContentControl cc) return;
4220-
4221-
var newView = newEl.BuildView(newItem);
4222-
if (cc.Content is UIElement existing && GetElementTag(cc) is Element oldView && CanUpdate(oldView, newView))
4223-
{
4224-
var replacement = Update(oldView, newView, existing, requestRerender);
4225-
if (replacement is not null && !ReferenceEquals(cc.Content, replacement))
4226-
cc.Content = replacement;
4227-
}
4228-
else
4229-
{
4230-
if (cc.Content is UIElement old) UnmountChild(old);
4231-
cc.Content = Mount(newView, requestRerender);
4232-
}
4233-
SetElementTag(cc, newView);
4234-
}
4235-
42364241
private UIElement? UpdateRectangle(RectangleElement n, WinShapes.Rectangle r)
42374242
{
42384243
if (n.Fill is not null) r.Fill = n.Fill;

0 commit comments

Comments
 (0)