Skip to content

Commit ad8c2de

Browse files
spec(047): Phase 3-final Batch G-prep — ItemsHost typing + descriptor-side ordering
Two engine fixes uncovered by the first Batch G1 attempt (which reverted clean rather than ship broken). ItemsHost is now usable for descriptor authors; flat-items ports (ListBox / ComboBox / RadioButtons in G1) and typed templated lists (G2 keyed-reconcile shape, deferred) build on top. Changes: - ChildrenStrategy.ItemsHost.GetCollection signature: System.Collections.IList -> IList<object>. WinUI Microsoft.UI.Xaml.Controls.ItemCollection does not implement the non-generic IList projection under CsWinRT; a descriptor reaching for c.Items hit InvalidCastException at runtime. IList<object> is the projected interface ItemCollection actually exposes. - DescriptorHandler.Mount / Update now dispatch ItemsHost INLINE between RentControl and the prop loop (Mount), and before the prop Update loop (Update). Selection-tracking initial writes (SelectedIndex / SelectedItem) need the collection in its final shape first — WinUI silently clamps selection against an empty collection. The strategy shape is unchanged; descriptors using ItemsHost simply get the items populated first, matching legacy MountListBox ordering. DescriptorHandler.Children returns null when the strategy is ItemsHost so V1HandlerAdapter doesn't double-dispatch. - V1HandlerAdapter ItemsHost branches kept for hand-coded handlers (none today) and updated for the IList<object> typing. Keyed-reconcile path (originally planned as part of G-prep for Batch G2) deferred: typed templated lists need ReactorListState + KeyedListDiff integration plus a Reconciler.BindKeyedItemsSource helper, which is substantial engine work better landed alongside its consumer rather than blind. V1 ON: 511 ok / 0 failures V1 OFF: 511 ok / 0 failures
1 parent 0f17286 commit ad8c2de

3 files changed

Lines changed: 92 additions & 2 deletions

File tree

src/Reactor/Core/V1Protocol/ChildrenStrategy.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ public sealed record NamedSlot<TElement, TControl>(
135135
[Experimental("REACTOR_V1_PREVIEW")]
136136
public sealed record ItemsHost<TElement, TControl>(
137137
Func<TElement, IReadOnlyList<object>> GetItems,
138-
Func<TControl, global::System.Collections.IList> GetCollection) : ChildrenStrategy<TElement, TControl>
138+
Func<TControl, IList<object>> GetCollection) : ChildrenStrategy<TElement, TControl>
139139
where TElement : Element
140140
where TControl : UIElement
141141
{

src/Reactor/Core/V1Protocol/Descriptor/DescriptorHandler.cs

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,29 @@ public DescriptorHandler(ControlDescriptor<TElement, TControl> descriptor)
4747
/// and bench harnesses — not part of the steady-state author API.</summary>
4848
public ControlDescriptor<TElement, TControl> Descriptor => _descriptor;
4949

50-
public ChildrenStrategy<TElement, TControl>? Children => _descriptor.Children;
50+
/// <summary>
51+
/// Children strategy surfaced to <see cref="V1HandlerAdapter{TElement,TControl}"/>.
52+
/// Returns the descriptor's strategy except when it's an
53+
/// <see cref="ItemsHost{TElement,TControl}"/> — that one is dispatched
54+
/// inline by <see cref="Mount"/> / <see cref="Update"/> before the prop
55+
/// loop runs so initial writes like <c>SelectedIndex</c> land against a
56+
/// populated collection (matches legacy mount ordering).
57+
/// </summary>
58+
public ChildrenStrategy<TElement, TControl>? Children =>
59+
_descriptor.Children is ItemsHost<TElement, TControl> ? null : _descriptor.Children;
5160

5261
public TControl Mount(MountContext ctx, TElement el)
5362
{
5463
var ctrl = ctx.RentControl(_descriptor.PoolPolicy, _descriptor.Factory);
5564

65+
// §14 Phase 3-final: when the descriptor declares an ItemsHost,
66+
// populate the items collection BEFORE the prop loop. Initial writes
67+
// for selection-tracking props (SelectedIndex/SelectedItem) need the
68+
// collection populated first — WinUI silently clamps selection
69+
// against an empty collection.
70+
if (_descriptor.Children is ItemsHost<TElement, TControl> ih)
71+
DispatchItemsHostMount(in ctx, el, ctrl, ih);
72+
5673
// Phase 1: all bare initial writes (no echo possible — subscriptions
5774
// not yet live). §14 Phase 3-final: dispatch through the
5875
// context-carrying overload so OneWayBridged entries can reach the
@@ -75,6 +92,12 @@ public TControl Mount(MountContext ctx, TElement el)
7592

7693
public void Update(UpdateContext ctx, TElement oldEl, TElement newEl, TControl ctrl)
7794
{
95+
// §14 Phase 3-final: ItemsHost diff BEFORE prop Update loop, same
96+
// ordering rationale as Mount — selection-tracking writes need the
97+
// collection in its post-diff shape first.
98+
if (_descriptor.Children is ItemsHost<TElement, TControl> ih)
99+
DispatchItemsHostUpdate(in ctx, oldEl, newEl, ctrl, ih);
100+
78101
var props = _descriptor.Properties;
79102
for (int i = 0; i < props.Count; i++)
80103
props[i].Update(in ctx, ctrl, oldEl, newEl);
@@ -90,4 +113,66 @@ public void Update(UpdateContext ctx, TElement oldEl, TElement newEl, TControl c
90113
if (getSetters is not null)
91114
ctx.ApplySetters(getSetters(newEl), ctrl);
92115
}
116+
117+
private static void DispatchItemsHostMount(
118+
in MountContext ctx, TElement el, TControl ctrl,
119+
ItemsHost<TElement, TControl> ih)
120+
{
121+
var newItems = ih.GetItems(el);
122+
var collection = ih.GetCollection(ctrl);
123+
if (collection.Count > 0) collection.Clear();
124+
for (int i = 0; i < newItems.Count; i++)
125+
{
126+
var item = newItems[i];
127+
if (item is Element childEl)
128+
{
129+
var mounted = ctx.MountChild(childEl);
130+
if (mounted is not null) collection.Add(mounted);
131+
}
132+
else if (item is not null)
133+
collection.Add(item);
134+
}
135+
}
136+
137+
private static void DispatchItemsHostUpdate(
138+
in UpdateContext ctx, TElement oldEl, TElement newEl, TControl ctrl,
139+
ItemsHost<TElement, TControl> ih)
140+
{
141+
var oldItems = ih.GetItems(oldEl);
142+
var newItems = ih.GetItems(newEl);
143+
if (ReferenceEquals(oldItems, newItems)) return;
144+
var equals = ih.ItemEquals ?? object.Equals;
145+
if (oldItems.Count == newItems.Count)
146+
{
147+
bool same = true;
148+
for (int i = 0; i < newItems.Count; i++)
149+
{
150+
if (!equals(oldItems[i], newItems[i])) { same = false; break; }
151+
}
152+
if (same) return;
153+
}
154+
// Structural change — unmount Element items via the reconciler so
155+
// any descendant component state is torn down, then rebuild flat.
156+
// (Keyed reconcile lands separately for typed templated lists.)
157+
var reconciler = ctx.Reconciler;
158+
var rerender = ctx.RequestRerender;
159+
for (int i = 0; i < oldItems.Count; i++)
160+
{
161+
if (oldItems[i] is Element oldChild)
162+
reconciler.ReconcileV1Child(oldChild, null, null, rerender);
163+
}
164+
var collection = ih.GetCollection(ctrl);
165+
if (collection.Count > 0) collection.Clear();
166+
for (int i = 0; i < newItems.Count; i++)
167+
{
168+
var item = newItems[i];
169+
if (item is Element childEl)
170+
{
171+
var mounted = ctx.MountChild(childEl);
172+
if (mounted is not null) collection.Add(mounted);
173+
}
174+
else if (item is not null)
175+
collection.Add(item);
176+
}
177+
}
93178
}

src/Reactor/Core/V1Protocol/V1HandlerAdapter.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ private static void DispatchChildrenMount(
129129
// Typed templated lists (ListView<T>, GridView<T>, etc.) keep
130130
// their own delegate-body handlers in Batch G2 with spec-042
131131
// keyed reconciliation; ItemsHost is for the flat case only.
132+
//
133+
// Note: descriptors hit this only via hand-coded handlers —
134+
// DescriptorHandler interleaves ItemsHost dispatch between
135+
// RentControl and the prop loop so initial writes like
136+
// SelectedIndex see a populated collection.
132137
var newItems = ih.GetItems(element);
133138
var collection = ih.GetCollection(control);
134139
if (collection.Count > 0) collection.Clear();

0 commit comments

Comments
 (0)