Skip to content

Commit 7a370d6

Browse files
spec(047): close V1-protocol descriptor regressions (default stays V1 OFF) (#443)
* spec(047): Engine A1 — post-children mount hook for V1 handlers Add an optional AfterChildrenMount hook to the V1 handler surface, invoked by V1HandlerAdapter.Mount after the children strategy (including inline items-binder strategies) has mounted every child. DescriptorHandler forwards to a new ControlDescriptor.AfterChildrenMount delegate. Lets handlers subscribe events that must wire strictly after children-add (e.g. TabView.SelectionChanged, which WinUI fires spuriously during the first tab add when subscribed during the prop-apply phase). Additive and inert until a descriptor populates the callback (Batch T). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * TabItemsHost: in-place reconcile instead of full rebuild on update The V1 TabItemsHost children strategy (backing PivotDescriptor, which is registered/live under V1, and the carved TabViewDescriptor) rebuilt every container on each Update: it unmounted+remounted all TabViewItem/PivotItem containers and their content subtrees. That re-triggers the tab-strip entrance animation and steals focus from descendant controls on any render that touches the host element. Switch the Update path to an index-keyed in-place positional reconcile, mirroring the legacy UpdateTabView/UpdatePivot arms: - keep existing containers for shared indices; reconcile each container's Content via ReconcileV1Child and only reassign when the realized control reference actually changes (avoids WinUI detach/reattach dropping queued setState); - refresh container metadata (header/icon/closable) through a new optional UpdateContainer callback supplied per descriptor; - unmount+remove excess tail; mount+append surplus new items; - fall back to full rebuild only on engine-invariant break / count drift. Extend the Desc_Pivot and Desc_TabView selftests with same-shape update assertions proving container AND content-subtree instance identity is preserved (would fail on the old rebuild path). Spec 047 §14 Phase 3 prelude. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Spec 047 §14: route NavigationHost through V1 dispatch Close the NavigationHost dispatch carve (Path B delegate handler). Expose MountNavigationHost/UpdateNavigationHost as internal and add NavigationHostHandler delegating to them; register it in RegisterV1BuiltInHandlers. Unmount stays on the flag-independent UnmountRecursive intercept (fires before the V1 unmount arm), so teardown is byte-identical V1 ON =/= V1 OFF. Validated: core build green; Navigation_NavHost* selftests (16 asserts) pass under both V1 ON and V1 OFF. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Spec 047 §14: route GridView through V1 dispatch Close the GridView dispatch carve with a hand-coded GridViewHandler (Path B delegate) mirroring ListViewHandler. Expose MountGridView/UpdateGridView as internal and delegate to them; register the handler in RegisterV1BuiltInHandlers. The GridViewDescriptor stays unregistered (its ItemsHost<> strategy pre-mounts every item with no virtualization, which would regress the recycle contract). Validated: core build green; all GridView selftests (RareControl, KLR, SelectionEvt, TemplatedListHL, CoreCov, Desc) pass under both V1 ON and V1 OFF (0 failures). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Close V1 overlay dispatch carves (spec 047 §14 prelude) Route the seven overlay elements (ContentDialog, Flyout, MenuBar, CommandBar, MenuFlyout, Popup, CommandBarFlyout) through V1 handler dispatch via decorator-style Path B handlers that delegate to the existing internal MountXxx/UpdateXxx engine bodies and return ContinueDefaultTraversal on unmount. Because ContinueDefaultTraversal makes the engine fall through to the same UnmountRecursive type-based recursion that runs when the V1 flag is OFF, mount/update/unmount are byte-identical V1 ON ≡ V1 OFF. Made the 14 overlay Mount*/Update* methods internal, added OverlayDecoratorHandlers.cs, registered all 7 in RegisterV1BuiltInHandlers, and updated the carve xmldoc. Validated A|B: overlay selftests (ContentDialog/Flyout/CommandBar/ MenuBar/Popup families) pass with identical counts and 0 failures under both V1 ON and V1 OFF. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Close V1 TabView dispatch carve via Path B delegate (spec 047 §14 prelude) Route TabViewElement through V1 handler dispatch via TabViewHandler, a Path B delegate that calls the COMPLETE legacy MountTabView/UpdateTabView bodies (drag pipeline, pinnable headers, strip header/footer, in-place tab-content reconcile, conditional SelectedIndex). This is distinct from the still-unregistered TabViewDescriptor + TabItemsHost port, which leaves those features on the legacy arm; the delegate runs the full feature set because it IS the legacy code, so it has none of the descriptor's gaps. UpdateTabView returns null (pure in-place reconcile), so the void-Update IElementHandler shape preserves identity. Made MountTabView/UpdateTabView internal. Unmount is byte-identical V1 ON =/= V1 OFF: a WinUI.TabView is an ItemsControl that both unmount paths pool without child recursion (no Panel/Border/ContentControl branch matches), and CollectSelf reproduces the V1-OFF fall-through. Mount/update are unchanged from the previously-carved V1-ON path; registering the handler only makes the parity-safe unmount arm fire. Validated A|B: TabView (5) + Pivot (3) selftests pass identically under V1 ON and V1 OFF. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Close V1 Button ContentElement gap via Path B delegate (spec 047 §14 prelude) ButtonDescriptor only expressed the string-Label fast path and dropped ButtonElement.ContentElement, so element-content buttons (e.g. PropertyGrid expand buttons) rendered blank under V1 ON. Replace the registered descriptor with a delegate ButtonHandler that runs the complete legacy MountButton/ UpdateButton bodies (enabled/disabled-focusable, Click trampoline, setters AND ContentElement). Decorator shape returns ContinueDefaultTraversal so unmount recurses into ContentControl content in both paths, matching V1 OFF cleanup of an element child. ButtonDescriptor retained for its isolated selftests + perf bench. A|B validated: PropertyGrid/Button/Categor selftests 0 failures under V1 ON and V1 OFF. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix V1 panel keyed-reconcile gap via Path B delegate handlers The V1 Panel<> children strategy (V1HandlerAdapter) reconciles children by index with no keyed reconcile, losing WinUI control identity on keyed reorder/reverse/swap/remove-middle. Legacy Update{Flex,Stack,Grid,Canvas, RelativePanel,WrapGrid} run ReconcileChildren -> ChildReconciler (spec-042 keyed LIS) and re-apply attached props, so V1 ON diverged from V1 OFF on ~17 identity-preservation selftests. Route all 6 panels through IDecoratorElementHandler delegates that invoke the complete legacy Mount*/Update* bodies (identical to V1 OFF). ContinueDefaultTraversal on unmount so teardown falls through to the same WinUI.Panel child-recursion V1 OFF uses. Update returns the legacy result when non-null (RelativePanel may substitute the control on count change) else keeps the existing control. Descriptor types retained for isolated selftests + perf bench. Made the 6 Mount*/Update* methods internal for delegate access. A|B validated (V1 ON == V1 OFF, 0 failures): FlexColumn, Keyed*, MultiCycle, Reconciler_KeyedList, Stack, Grid, Canvas, RelativePanel, WrapGrid. Also fixes as side effects: AAF_MoveSpring + AAF_FlexColumnMove (animation) and ConditionalRendering_Toggle_HiddenAgain. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix V1 Lazy*Stack ScrollViewer-wrapper gap via derived-type delegate The V1 LazyStackDescriptor port used ItemsRepeater directly as TControl (a descriptor's RentControl returns a single control, with no place for a wrapping ScrollViewer). Legacy MountLazyStack wraps the ItemsRepeater in a ScrollViewer with orientation-appropriate scrollbars + ScrollViewerSetters. Under V1 ON the ScrollViewer was therefore absent, failing every FindControl<ScrollViewer> fixture (LazyVStack/HStack_ScrollViewer, LazyHStack_HScrollEnabled, ScrollPop/Back, HookPaging_Scroll*) and the component-cleanup-on-unmount gate (EFR_LazyStackUnmount) — no ScrollViewer for the engine to recurse through on teardown. Add RegisterDecoratorHandlerForDerivedTypes<TBase> (the derived-type analogue of RegisterDecoratorHandler) and route the Lazy*Stack family through a Path B LazyStackHandler decorator that runs the complete legacy MountLazyStack/ UpdateLazyStack bodies. ContinueDefaultTraversal on unmount so the engine recurses ScrollViewer -> ItemsRepeater -> realized rows (running each row component's UseEffect cleanup), identical to V1 OFF. Descriptor retained for isolated selftests. Made MountLazyStack/UpdateLazyStack internal for delegate access. A|B validated (V1 ON == V1 OFF, 0 failures): Lazy, Scroll, HookPaging, DataGridScroll. EFR_LazyStackUnmount_AtLeastOneCleanup now after=5 (was 0). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix 3 V1-ON regressions via Path B delegate handlers Convert CheckBox, Expander, and templated list (ListView/GridView/FlipView) V1 descriptors to decorator handlers that delegate Mount/Update to the legacy engine bodies, restoring byte-identical parity with V1 OFF. - CheckBoxHandler: legacy UpdateCheckBox only suppresses echo when the target differs from the current value, fixing ConditionalRendering toggle swallow. - ExpanderHandler: restores ContentTransitions + late callback subscription. - TemplatedListHandler: registered on common base TemplatedListElementBase so typed ListView<T>/GridView<T>/FlipView<T> route through legacy MountTemplatedList + control-typed Update*, restoring ApplyMoveAnimations (fixes AAF_MoveSpring/FlexColumnMove implicit-offset attach). Made the corresponding legacy Mount*/Update* methods internal. A|B validated under real V1 ON vs OFF: 0 failures, equal check counts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4fa02f7 commit 7a370d6

22 files changed

Lines changed: 980 additions & 155 deletions

src/Reactor/Core/Reconciler.Mount.cs

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ private WinUI.RichTextBlock MountRichTextBlock(RichTextBlockElement richText)
301301
return rtb;
302302
}
303303

304-
private WinUI.Button MountButton(ButtonElement btn, Action requestRerender)
304+
internal WinUI.Button MountButton(ButtonElement btn, Action requestRerender)
305305
{
306306
var rented = _pool.TryRent(typeof(WinUI.Button));
307307
var button = rented as WinUI.Button ?? new WinUI.Button();
@@ -675,7 +675,7 @@ private WinUI.AutoSuggestBox MountAutoSuggestBox(AutoSuggestBoxElement asb)
675675
return box;
676676
}
677677

678-
private WinUI.CheckBox MountCheckBox(CheckBoxElement cb)
678+
internal WinUI.CheckBox MountCheckBox(CheckBoxElement cb)
679679
{
680680
var checkBox = new WinUI.CheckBox { Content = cb.Label };
681681
if (cb.IsThreeState)
@@ -1116,7 +1116,7 @@ private WinUI.RichEditBox MountRichEditBox(RichEditBoxElement reb)
11161116
return box;
11171117
}
11181118

1119-
private WinUI.VariableSizedWrapGrid MountWrapGrid(WrapGridElement wg, Action requestRerender)
1119+
internal WinUI.VariableSizedWrapGrid MountWrapGrid(WrapGridElement wg, Action requestRerender)
11201120
{
11211121
var grid = new WinUI.VariableSizedWrapGrid { Orientation = wg.Orientation };
11221122
if (wg.MaximumRowsOrColumns >= 0) grid.MaximumRowsOrColumns = wg.MaximumRowsOrColumns;
@@ -1140,7 +1140,7 @@ private WinUI.VariableSizedWrapGrid MountWrapGrid(WrapGridElement wg, Action req
11401140
return grid;
11411141
}
11421142

1143-
private WinUI.StackPanel MountStack(StackElement stack, Action requestRerender)
1143+
internal WinUI.StackPanel MountStack(StackElement stack, Action requestRerender)
11441144
{
11451145
var panel = _pool.TryRent(typeof(WinUI.StackPanel)) as WinUI.StackPanel ?? new WinUI.StackPanel();
11461146
panel.Orientation = stack.Orientation;
@@ -1160,7 +1160,7 @@ private WinUI.StackPanel MountStack(StackElement stack, Action requestRerender)
11601160
return panel;
11611161
}
11621162

1163-
private WinUI.Grid MountGrid(GridElement grid, Action requestRerender)
1163+
internal WinUI.Grid MountGrid(GridElement grid, Action requestRerender)
11641164
{
11651165
var g = _pool.TryRent(typeof(WinUI.Grid)) as WinUI.Grid ?? new WinUI.Grid();
11661166
g.RowSpacing = grid.RowSpacing;
@@ -1268,7 +1268,7 @@ private WinUI.Border MountBorder(BorderElement border, Action requestRerender)
12681268
return bdr;
12691269
}
12701270

1271-
private WinUI.Expander MountExpander(ExpanderElement exp, Action requestRerender)
1271+
internal WinUI.Expander MountExpander(ExpanderElement exp, Action requestRerender)
12721272
{
12731273
var expander = new WinUI.Expander
12741274
{
@@ -1325,7 +1325,7 @@ private WinUI.Viewbox MountViewbox(ViewboxElement vb, Action requestRerender)
13251325
return viewbox;
13261326
}
13271327

1328-
private WinUI.Canvas MountCanvas(CanvasElement cvs, Action requestRerender)
1328+
internal WinUI.Canvas MountCanvas(CanvasElement cvs, Action requestRerender)
13291329
{
13301330
var canvas = _pool.TryRent(typeof(WinUI.Canvas)) as WinUI.Canvas ?? new WinUI.Canvas();
13311331
if (cvs.Width.HasValue) canvas.Width = cvs.Width.Value;
@@ -1345,7 +1345,7 @@ private WinUI.Canvas MountCanvas(CanvasElement cvs, Action requestRerender)
13451345
return canvas;
13461346
}
13471347

1348-
private Layout.FlexPanel MountFlex(FlexElement flex, Action requestRerender)
1348+
internal Layout.FlexPanel MountFlex(FlexElement flex, Action requestRerender)
13491349
{
13501350
var panel = _pool.TryRent(typeof(Layout.FlexPanel)) as Layout.FlexPanel ?? new Layout.FlexPanel();
13511351
panel.Direction = flex.Direction;
@@ -1369,7 +1369,7 @@ private Layout.FlexPanel MountFlex(FlexElement flex, Action requestRerender)
13691369
return panel;
13701370
}
13711371

1372-
private WinUI.Grid MountNavigationHost(NavigationHostElement element, Action requestRerender)
1372+
internal WinUI.Grid MountNavigationHost(NavigationHostElement element, Action requestRerender)
13731373
{
13741374
var grid = new WinUI.Grid();
13751375
var handle = (Navigation.INavigationHandle)element.NavigationHandle;
@@ -1507,7 +1507,7 @@ private WinUI.TitleBar MountTitleBar(TitleBarElement tb, Action requestRerender)
15071507
return titleBar;
15081508
}
15091509

1510-
private WinUI.TabView MountTabView(TabViewElement tab, Action requestRerender)
1510+
internal WinUI.TabView MountTabView(TabViewElement tab, Action requestRerender)
15111511
{
15121512
var tv = new WinUI.TabView
15131513
{
@@ -1803,7 +1803,7 @@ internal WinUI.ListView MountListView(ListViewElement lv, Action requestRerender
18031803
return listView;
18041804
}
18051805

1806-
private WinUI.GridView MountGridView(GridViewElement gv, Action requestRerender)
1806+
internal WinUI.GridView MountGridView(GridViewElement gv, Action requestRerender)
18071807
{
18081808
var gridView = new WinUI.GridView
18091809
{
@@ -1986,7 +1986,7 @@ private WinUI.FlipView MountFlipView(FlipViewElement fv, Action requestRerender)
19861986
return flipView;
19871987
}
19881988

1989-
private UIElement MountTemplatedList(TemplatedListElementBase el, Action requestRerender)
1989+
internal UIElement MountTemplatedList(TemplatedListElementBase el, Action requestRerender)
19901990
{
19911991
return el.ControlKind switch
19921992
{
@@ -2295,7 +2295,7 @@ private WinUI.InfoBadge MountInfoBadge(InfoBadgeElement badge)
22952295
return ib;
22962296
}
22972297

2298-
private UIElement MountContentDialog(ContentDialogElement cdEl, Action requestRerender)
2298+
internal UIElement MountContentDialog(ContentDialogElement cdEl, Action requestRerender)
22992299
{
23002300
var placeholder = new WinUI.StackPanel { Visibility = Visibility.Collapsed };
23012301
SetElementTag(placeholder, cdEl);
@@ -2350,7 +2350,7 @@ private async void ShowContentDialogCore(ContentDialogElement cdEl, XamlRoot? xa
23502350
cdEl.OnClosed?.Invoke(winUiResult);
23512351
}
23522352

2353-
private UIElement? MountFlyout(FlyoutElement flyEl, Action requestRerender)
2353+
internal UIElement? MountFlyout(FlyoutElement flyEl, Action requestRerender)
23542354
{
23552355
var target = Mount(flyEl.Target, requestRerender);
23562356
if (target is FrameworkElement targetFe)
@@ -2411,7 +2411,7 @@ private WinUI.TeachingTip MountTeachingTip(TeachingTipElement ttEl, Action reque
24112411
return tip;
24122412
}
24132413

2414-
private WinUI.MenuBar MountMenuBar(MenuBarElement mbEl)
2414+
internal WinUI.MenuBar MountMenuBar(MenuBarElement mbEl)
24152415
{
24162416
var menuBar = new WinUI.MenuBar();
24172417
foreach (var menuItem in mbEl.Items)
@@ -2488,7 +2488,7 @@ private static bool IsDescendantOf(DependencyObject element, DependencyObject an
24882488
return false;
24892489
}
24902490

2491-
private WinUI.CommandBar MountCommandBar(CommandBarElement cmdEl, Action requestRerender)
2491+
internal WinUI.CommandBar MountCommandBar(CommandBarElement cmdEl, Action requestRerender)
24922492
{
24932493
var commandBar = new WinUI.CommandBar
24942494
{
@@ -2543,7 +2543,7 @@ private static WinUI.ICommandBarElement CreateAppBarItem(AppBarItemBase item)
25432543
}
25442544
}
25452545

2546-
private UIElement? MountMenuFlyout(MenuFlyoutElement mfEl, Action requestRerender)
2546+
internal UIElement? MountMenuFlyout(MenuFlyoutElement mfEl, Action requestRerender)
25472547
{
25482548
var target = Mount(mfEl.Target, requestRerender);
25492549
if (target is FrameworkElement targetFe)
@@ -3177,7 +3177,7 @@ private static Internal.ReactorListState BuildListStateForItemsRepeater(ItemsRep
31773177
return state;
31783178
}
31793179

3180-
private UIElement MountLazyStack(LazyStackElementBase lazy, Action requestRerender)
3180+
internal UIElement MountLazyStack(LazyStackElementBase lazy, Action requestRerender)
31813181
{
31823182
var repeater = new WinUI.ItemsRepeater();
31833183

@@ -3462,7 +3462,7 @@ private WinShapes.Path MountPath(PathElement pa)
34623462

34633463
// ── RelativePanel ───────────────────────────────────────────────────
34643464

3465-
private WinUI.RelativePanel MountRelativePanel(RelativePanelElement rp, Action requestRerender)
3465+
internal WinUI.RelativePanel MountRelativePanel(RelativePanelElement rp, Action requestRerender)
34663466
{
34673467
var panel = new WinUI.RelativePanel();
34683468
var nameMap = new Dictionary<string, UIElement>();
@@ -3680,7 +3680,7 @@ private WinUI.AnnotatedScrollBar MountAnnotatedScrollBar(AnnotatedScrollBarEleme
36803680

36813681
// ── Popup ───────────────────────────────────────────────────────────
36823682

3683-
private UIElement MountPopup(PopupElement popup, Action requestRerender)
3683+
internal UIElement MountPopup(PopupElement popup, Action requestRerender)
36843684
{
36853685
// Popup is not a UIElement child, so we wrap it in a StackPanel
36863686
var wrapper = new WinUI.StackPanel();
@@ -3719,7 +3719,7 @@ private WinUI.RefreshContainer MountRefreshContainer(RefreshContainerElement rc,
37193719

37203720
// ── CommandBarFlyout ────────────────────────────────────────────────
37213721

3722-
private UIElement? MountCommandBarFlyout(CommandBarFlyoutElement cbf, Action requestRerender)
3722+
internal UIElement? MountCommandBarFlyout(CommandBarFlyoutElement cbf, Action requestRerender)
37233723
{
37243724
var target = Mount(cbf.Target, requestRerender);
37253725
if (target is FrameworkElement targetFe)

0 commit comments

Comments
 (0)