Skip to content

Commit f0d3e81

Browse files
spec(047): §14 Phase 3 completion — register Phase 3 descriptors + close engine gaps (#440)
* Spec 047 §14 Phase 3 completion (1/N): PreMountedItems + TemplatedFlipView descriptor Adds the engine-gap closer carried forward from Phase 3 finish: the typed `TemplatedFlipViewElement<T>` peer now routes through V1 dispatch via a new `PreMountedItems<TElement, TControl>` ChildrenStrategy and a base-derived `TemplatedFlipViewDescriptor` registered on `TemplatedFlipViewElementBase`. Engine - IItemsBinderStrategy.Bind signature widened to take `Element? oldElement` (null on Mount, set on Update). Keeps the consolidated dispatch arm in V1HandlerAdapter + DescriptorHandler to a single is-check + interface call. All 4 existing implementers ignore the new param (they read prior state from the control) — only the new PreMountedItems<> uses it. - PreMountedItems<TElement, TControl> in ChildrenStrategy.cs: pre-mounts every item up-front through IItemViewSource into the control's IList<object> Items sink, and on Update positionally reconciles via Reconciler.ReconcileV1Child for shared slots, appending new tail slots and truncating excess. Rubber-duck recommendations adopted: Debug.Assert on (oldElement is TElement) and on `items.Count == oldSource.ItemCount` with release fallback to full rebuild if the invariant breaks; throws InvalidOperationException (not silent null writes) if mount/reconcile returns null in a slot. Descriptor - TemplatedFlipViewDescriptor uses PreMountedItems<> + HandCodedControlled for SelectedIndex with the existing FlipViewEventPayload shared trampoline slot. `callback` returns a synthetic non-null delegate gated on `el.HasCallbacks` so the engine only subscribes when at least one closed-T leaf has wired OnSelectedIndexChanged. - Registered base-derived in RegisterV1BuiltInHandlers via RegisterHandlerForDerivedTypes<TemplatedFlipViewElementBase, FlipView>. Tests - New Desc_TemplatedFlipView_MountUpdate covers: pre-mount item-count, pre-mounted slots are UIElements, initial SelectedIndex applied, mount didn't fire callback, programmatic SelectedIndex write echo-suppressed, grow/shrink/edit-in-place positional reconcile, shrink clamps SelectedIndex, same-ref idempotency, edit-in-place preserves slot identity (CanUpdate path through ReconcileV1Child). - New Desc_TemplatedFlipView_NoCallback_DoesNotSubscribe covers the HasCallbacks=false trampoline-not-subscribed branch. Docs - §14 carry-forwards: TemplatedFlipView engine-gap closed; updated stale `TemplatedFlipViewDescriptor stays carved` comment in FlipViewDescriptor. Validation - dotnet test tests/Reactor.Tests -c Release -p:Platform=x64 → 9134 ok / 0 fail - --self-test --filter `Desc_` V1 ON = 630 ok / 0 fail; V1 OFF = 630 ok / 0 fail (parity holds, both flags +17 vs Phase 3 finish baseline of 613 for the new TemplatedFlipView fixtures). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: add Semantic V1 descriptor Add a SemanticElement descriptor backed by SemanticPanel with SingleContent child reconciliation and descriptor selftest coverage for mount/update parity. Validated with: dotnet build src\Reactor\Reactor.csproj -c Release -p:Platform=x64; dotnet build tests\Reactor.AppTests.Host\Reactor.AppTests.Host.csproj -c Release -p:Platform=x64; dotnet run --project tests\Reactor.AppTests.Host -c Release -p:Platform=x64 --no-build -- --self-test --filter Desc_Semantic Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: add GridView descriptor Port the untyped GridViewElement path to a V1 descriptor using ItemsHost and descriptor-managed selection/item-click events. Add the descriptor selftest and registry entries for mount/update coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: add ItemContainer descriptor Port ItemContainerElement to a V1 single-content descriptor. Add descriptor selftest coverage for mount, child update, and IsSelected update. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: add AnnounceRegion V1 descriptor Add a descriptor for the AnnounceRegionElement hidden TextBlock live-region anchor and register descriptor selftest coverage for mount/update parity. Validated with: dotnet build src\Reactor\Reactor.csproj -c Release -p:Platform=x64; dotnet build tests\Reactor.AppTests.Host\Reactor.AppTests.Host.csproj -c Release -p:Platform=x64; dotnet run --project tests\Reactor.AppTests.Host -c Release -p:Platform=x64 --no-build -- --self-test --filter Desc_AnnounceRegion Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: add ItemsView descriptor Port ItemsViewElementBase to a base-derived V1 descriptor using TemplatedItemsErased. Extend the erased keyed binder to reproduce the legacy ItemsView ItemsSource/ItemTemplate shape and add descriptor selftest coverage for mount/update and property changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: defer polymorphic and XAML interop descriptors Defer IconElement: the current V1 descriptor engine requires a stable TControl identity, while IconElement's legacy path chooses different native IconElement subclasses from IconData and can replace the native control when the IconData subtype changes. This needs an element-aware polymorphic factory or an approved replacement strategy before it can route through _v1Handlers without semantic drift. Defer XamlHostElement: its factory returns arbitrary user-owned FrameworkElement instances, so the descriptor model cannot express the control type, pooling policy, or ownership safely through a single new() TControl descriptor. Defer XamlPageElement: it is closer to descriptor-shaped than XamlHost, but the bridge still delegates to Frame.Navigate for arbitrary user XAML page types. The attempted descriptor selftest could not provide a deterministic code-only page target without crashing the WinUI navigation path, so this stays carved for orchestrator review rather than landing an unvalidated descriptor. Validated with: dotnet build tests\Reactor.AppTests.Host\Reactor.AppTests.Host.csproj -c Release -p:Platform=x64; dotnet run --project tests\Reactor.AppTests.Host -c Release -p:Platform=x64 --no-build -- --self-test --filter Desc_Semantic; dotnet run --project tests\Reactor.AppTests.Host -c Release -p:Platform=x64 --no-build -- --self-test --filter Desc_AnnounceRegion Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: add AnimatedVisualPlayer V1 descriptor Selftest: Desc_AnimatedVisualPlayer_MountUpdate (V1 ON). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: add AnnotatedScrollBar V1 descriptor Selftest: Desc_AnnotatedScrollBar_MountUpdate (V1 ON). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: add MapControl V1 descriptor Selftest: Desc_MapControl_MountUpdate (V1 ON descriptor availability only). MapControl construction process-terminates this headless host without the Maps runtime/token, so E2E must own real lifecycle coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: add ParallaxView V1 descriptor Selftest: Desc_ParallaxView_MountUpdate (V1 ON). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: add RefreshContainer V1 descriptor Selftest: Desc_RefreshContainer_MountUpdate (V1 ON). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: add SwipeControl V1 descriptor Selftest: Desc_SwipeControl_MountUpdate (V1 ON). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: add SemanticZoom V1 descriptor Selftest: Desc_SemanticZoom_MountUpdate (V1 ON). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: add MediaPlayerElement V1 descriptor Selftest: Desc_MediaPlayerElement_MountUpdate (V1 ON, no media source). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: add WebView2 V1 descriptor Selftest: Desc_WebView2_MountUpdate (V1 ON, no Source to avoid async CoreWebView2 init). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: add TitleBar V1 descriptor Selftest: Desc_TitleBar_MountUpdate (V1 ON). SetTitleBar is deferred to Loaded so the title bar is attached before window registration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: add NavigationView V1 descriptor Selftest: Desc_NavigationView_MountUpdate (V1 ON). Selection trampoline handles built-in settings selection by reporting a null SelectedTag because WinUI does not expose a NavigationViewItem tag for Settings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: fix DescSemanticZoom fixture's nested ListView registration Batch 3 SemanticZoom fixture referenced a non-existent ListViewDescriptor for its inner ZoomedInView/ZoomedOutView ListViewElement children. Use the Phase 1 hand-coded ListViewHandler instead (the canonical V1 registration for ListView). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: §14 Phase 3 completion — IDecoratorElementHandler engine extension Adds a new decorator-style V1 handler shape for elements whose returned UIElement identity may change on update or whose unmount disposition diverges from the standard pool-return. Targets the 8 elements deferred during Phase 3 batches: - Target-wrapping decorators: FlyoutElement, MenuFlyoutElement, CommandBarFlyoutElement (returned UIElement IS the user's inner Target child — type isn't known until inspection). - Modal lifecycle wrappers: ContentDialogElement, PopupElement (returned control is a placeholder; the actual modal surface is side-mounted or lazy). - Polymorphic mounts: IconElement (concrete control type depends on the element's runtime subtype). - Interop bridges: XamlHostElement, XamlPageElement. Surface changes (3 minimum-correct items per rubber-duck spec): 1. IV1HandlerEntry.Update returns UIElement (was void). Standard handlers (V1HandlerAdapter) always return `control` unchanged, preserving §13 Q12's no-substitution invariant on the standard IElementHandler surface. Reconciler.Update threads the result into the parent slot via the existing legacy-registry path (already handled by ReconcileV1Child). 2. IV1HandlerEntry.Unmount returns V1UnmountDisposition enum (CollectSelf | ContinueDefaultTraversal | SkipPool). Standard handlers always return CollectSelf, matching pre-extension behavior. Reconciler.UnmountRecursive + UnmountAndCollect switch on the returned disposition to control pool collection and traversal recursion. 3. New IDecoratorElementHandler<TElement> contract (TElement only, no TControl) + V1DecoratorHandlerAdapter<TElement> bridging into IV1HandlerEntry. Distinct from IElementHandler<TElement,TControl> so the public author-facing surface retains its no-substitution invariant; only built-in V1 ports use this decorator shape. Registered via Reconciler.RegisterDecoratorHandler<TElement> (internal — built-in ports only). No descriptor registrations changed in this commit. Descriptor ports for the 8 deferred elements follow in subsequent commits. Validation: - dotnet build src/Reactor/Reactor.csproj: 0 errors - dotnet build tests/Reactor.AppTests.Host: 0 errors - --self-test --filter Desc_ V1 ON: 728 ok / 0 failures - --self-test --filter Desc_ V1 OFF: 728 ok / 0 failures - Desc_ parity: ON ≡ OFF (no regression introduced by widening). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: add Icon V1 descriptor Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: add XamlHost V1 descriptor Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: add XamlPage V1 descriptor Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: §14 Phase 3 completion — register Phase 3 descriptors in production V1 dispatch Wires every Phase 3 descriptor authored across batches 1–4 (plus the Phase 1 hand-coded handlers + base-derived templated/lazy/items hosts + PreMountedItems TemplatedFlipView) into RegisterV1BuiltInHandlers, the sole built-in V1 registration site. With this PR, V1 ON becomes the production dispatch path for ~78 element types; only the carve-list elements documented in the RegisterV1BuiltInHandlers XML doc still fall through to the legacy Mount/Update switch. Carve list (intentionally NOT registered — documented inline): - Composition primitives (Component, Func, Memo, ErrorBoundary, CommandHost, Validation.*) — sit ABOVE the V1 handler protocol - Interop bridges (XamlHost, XamlPage) — descriptors exist but XamlInterop.Register populates _typeRegistry at startup, would clash with EnsureRegistrableElementType; unification = Phase 4 - Deferred overlays (ContentDialog, Flyout, MenuBar, MenuFlyout, CommandBar, CommandBarFlyout, Popup) — require decorator-style ports for modal lifecycle; follow-up PR - Deferred stateful host (NavigationHost) — per-instance route/cache state intercepted in UnmountRecursive before V1 dispatch arm; needs small refactor; follow-up PR - TabViewDescriptor — bisect (3× clean V1 ON full selftest with only this carved, vs. 1–4 random docking-text-find failures per run when registered) ratifies the descriptor's documented gaps: spec 045 §2.4 drag pipeline (OnTabDragStarting/Completed), §2.2 pinnable headers (BuildTabHeader/BuildPinButton/in-place TryUpdatePinHeaderInPlace), in-place CanUpdate for tab content (preserves focus/state on re-renders), conditional SelectedIndex write, TabStripHeader/Footer Element slots. Closing requires engine work (post-children mount-hook so SelectionChanged subscribes after children-add + ImperativeBridged for named tab strip slots); tracked alongside overlays + NavigationHost. Validation: - dotnet test tests/Reactor.Tests -c Release -p:Platform=x64: Passed! 9134 / 0 failed / 62 skipped - dotnet run --project tests/Reactor.AppTests.Host --self-test V1 ON (run 1): 4410 ok, 0 failures V1 ON (run 2): 4410 ok, 0 failures V1 OFF: 4410 ok, 0 failures - Desc_ filter A|B: 744 / 744 (perfect parity) Helpers added (internal, [Experimental(`REACTOR_V1_PREVIEW'')]): RegisterDescriptor<TElement, TControl>(descriptor) RegisterDescriptorForDerivedTypes<TBase, TControl>(descriptor) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec047: §14 Phase 3 completion — update spec + tracker for Phase 3 completion Document Phase 3 completion in both the spec (§14) and the implementation tracker. With the engine extension (IDecoratorElementHandler) + PreMountedItems strategy + descriptor batches + production registration sweep landed: - Engine gap closed (TemplatedFlipView via PreMountedItems) - Every Phase 3 batch descriptor authored AND registered: - Untyped items hosts: GridView, ItemsView, ItemContainer - Heavy/specialized: WebView2, NavigationView, TitleBar, MediaPlayerElement, AnimatedVisualPlayer, MapControl, SemanticZoom, AnnotatedScrollBar, RefreshContainer, SwipeControl, ParallaxView - Polymorphic/a11y: IconElement (via new IDecoratorElementHandler), SemanticElement, AnnounceRegion Intentional carve list (documented inline in RegisterV1BuiltInHandlers and re-enumerated in both docs for the follow-up reviewer): - Dialog/overlay family (ContentDialog, Flyout, Popup, MenuBar, MenuFlyout, CommandBar, CommandBarFlyout) — modal lifecycle needs decorator-style ports beyond the IDecoratorElementHandler shape - NavigationHost — UnmountRecursive intercepts before V1 arm - TabViewDescriptor — bisect-ratified gaps need engine work (post-children mount-hook for safe SelectionChanged wiring + ImperativeBridged for tab strip slots) - XamlHost/XamlPage — XamlInterop.Register clashes with V1 auto-reg - Composition primitives (Component, Func, Memo, ErrorBoundary, CommandHost, Validation.*) — sit ABOVE V1 protocol; Phase 4 keeps their legacy arms A|B parity bar met: 9134 xunit + 4410 selftest (V1 ON ≡ V1 OFF), 0 failures both flags across 3 consecutive full V1 ON runs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec(047): record PR #440 + quantified V1 dispatch coverage (76/95 = 80% arms; 76/87 = 87% of V1-reachable) Updates §14 in both the spec and tracker to reference PR #440 and add an explicit coverage table breaking the 95 legacy switch arms into 76 routed / 11 reachable-deferred / 8 permanent composition-primitive carves, plus the enumerated path-to-100% for the follow-up PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec(047): address PR #440 CR feedback (Copilot reviewer) * Carve GridViewDescriptor — its ItemsHost<> strategy pre-mounts every item into GridView.Items (no virtualization), diverging from legacy MountGridView's ItemsSource+CCC lazy realization. A|B tests still pass, but production memory/lifecycle would silently regress; closing needs hand-coded GridViewHandler or RecyclingItemsHost<> shape. Updated coverage table: 75 routed / 12 deferred / 8 primitives. * Align legacy UpdateNavigationView with V1 NavigationViewDescriptor — add PaneDisplayMode, IsSettingsVisible, conditional PaneTitle, MenuItems rebuild on ref-change, and SelectedItem re-selection (with new FindNavItemByTag helper). Previously legacy only wrote a subset of these on update; reviewer correctly flagged the A|B divergence for record-with updates that change MenuItems / Selected. * Fix PreMountedItems<> release fallback at ChildrenStrategy.cs:577 — the count-drift path was clamping oldCount=items.Count and continuing positional reconciliation, which would index past oldSource bounds when items.Count > oldSource.ItemCount or skip stale source items' teardown when smaller. Replaced with the same full-rebuild path as the type-mismatch release fallback above. * Doc fix: remove ModifiedElement from the tracker's composition- primitive carve list — it's unwrapped before V1/legacy dispatch at the top of Reconciler.Mount, not a switch arm; coverage table stays at 8 primitives. Validation: dotnet test 9134/0; full V1 ON selftest 4410/0; full V1 OFF selftest 4410/0 (each suite required one re-run to clear the known FloatRoot/Reliability/DockHooks docking flake family). Ignored 9 github-code-quality bot comments: 8 are integer-valued double == checks in test fixtures (values are assigned constants, exact equality is reliable); 1 claims WinUI FontFamily is IDisposable (it is not). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 836c301 commit f0d3e81

36 files changed

Lines changed: 3487 additions & 138 deletions

docs/specs/047-extensible-control-model.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1433,7 +1433,20 @@ ARM64 stable-AC re-capture on `LAPTOP-4MEP83VI` remains deferred for the §14 ra
14331433

14341434
**Phase 3 finish carry-forwards:** none remaining for the typed-items host families that were on Phase 3's scope (LazyVStack/HStack, ItemsRepeater<T>, ListView<T>, GridView<T>, TreeView, FlipView (simple), TabView, Pivot). The engine surface is complete. Production swap (Phase 4 cleanup) registers each descriptor in `RegisterV1BuiltInHandlers` and deletes the matching legacy `MountXxx` switch arm.
14351435

1436-
**Phase 3 deferred / not-attempted** (recorded for the Phase 3.5 / Phase 4 prelude — element types in the legacy `Reconciler.Mount` switch that have neither a Phase 1 V1 handler nor a Phase 3 descriptor; see `tasks/047-extensible-control-model-implementation.md` for the full enumeration). One genuine carve from close-out remains: `TemplatedFlipViewElement<T>` (the typed FlipView peer — needs a `PreMountedItems` ChildrenStrategy since FlipView lacks `ContainerContentChanging`). The rest of the deferred list — `GridViewElement` (plain), `ItemsViewElementBase`, dialog / overlay family (`ContentDialog`, `Flyout`, `Popup`, `MenuBar`, `MenuFlyout`, `CommandBar`, `CommandBarFlyout`), heavy / specialized controls (`WebView2`, `NavigationView`, `TitleBar`, `MediaPlayerElement`, `AnimatedVisualPlayer`, `MapControl`, `SemanticZoom`, `AnnotatedScrollBar`, `RefreshContainer`, `SwipeControl`, `ParallaxView`), interop / a11y (`SemanticElement`, `AnnounceRegion`, `XamlHost`, `XamlPage`, and the already-escape-hatched `IconElement`), and Reactor composition primitives (`Component`, `Func`, `Memo`, `ErrorBoundary`, `CommandHost`, `Validation.*`) — was never on the Phase 3 batch list. The composition primitives likely should NOT route through the V1 handler protocol (they sit above it); the rest are straightforward descriptor ports (or, where they'd surface engine gaps, would follow the close-out / finish pattern of landing the engine extension before the port).
1436+
**Phase 3 deferred / not-attempted** (recorded for the Phase 3.5 / Phase 4 prelude — element types in the legacy `Reconciler.Mount` switch that have neither a Phase 1 V1 handler nor a Phase 3 descriptor; see `tasks/047-extensible-control-model-implementation.md` for the full enumeration). **Updated in Phase 3 completion (PR #440, commit 16636c0d):** the engine gap is closed (`TemplatedFlipViewElement<T>` ported via the new `PreMountedItems` ChildrenStrategy + `TemplatedFlipViewDescriptor`), and every previously-deferred descriptor on the Phase 3 batch list is now both authored AND registered in `RegisterV1BuiltInHandlers`: untyped items hosts (`GridView`, `ItemsView`, `ItemContainer`), heavy / specialized controls (`WebView2`, `NavigationView`, `TitleBar`, `MediaPlayerElement`, `AnimatedVisualPlayer`, `MapControl`, `SemanticZoom`, `AnnotatedScrollBar`, `RefreshContainer`, `SwipeControl`, `ParallaxView`), polymorphic / a11y (`IconElement` via the new `IDecoratorElementHandler` engine extension, `SemanticElement`, `AnnounceRegion`). What still ships unregistered (intentional carve list, documented inline in `RegisterV1BuiltInHandlers`): the dialog / overlay family (`ContentDialog`, `Flyout`, `Popup`, `MenuBar`, `MenuFlyout`, `CommandBar`, `CommandBarFlyout`) — modal lifecycle needs decorator-style ports beyond the IDecoratorElementHandler shape; the stateful `NavigationHostElement` — `Reconciler.UnmountRecursive` intercepts before the V1 arm and needs a small refactor; `TabViewDescriptor` — bisect ratifies the documented gaps (spec 045 §2.4 drag pipeline, §2.2 pinnable headers, in-place CanUpdate, conditional `SelectedIndex` write, `TabStripHeader` / `TabStripFooter` slots) need engine work (post-children mount-hook + `ImperativeBridged` for named slots); the XAML interop bridges (`XamlHost`, `XamlPage`) — descriptors exist but `XamlInterop.Register` populates `_typeRegistry` at startup so V1 auto-registration would clash; and the Reactor composition primitives (`Component`, `Func`, `Memo`, `ErrorBoundary`, `CommandHost`, `Validation.*`) — these sit ABOVE the V1 handler protocol and Phase 4 cleanup keeps their legacy arms. The A|B parity bar is met for every registered element: 9134 xunit + 4410 selftest (V1 ON ≡ V1 OFF), 0 failures both flags.
1437+
1438+
**Quantified V1 dispatch coverage (post-PR #440):**
1439+
1440+
| Bucket | Arms | Notes |
1441+
|---|---:|---|
1442+
| Routed through V1 (Phase 1 handler or Phase 3 descriptor) | 75 | Production dispatch path when V1 ON |
1443+
| Reachable-but-deferred (overlays 7 + NavigationHost 1 + TabView 1 + GridView 1 + XamlHost/Page 2) | 12 | Follow-up PR closes these |
1444+
| Intentionally above V1 (Reactor composition primitives) | 8 | Permanent carve; Phase 4 keeps legacy arms |
1445+
| **Total switch arms** | **95** ||
1446+
1447+
- **Coverage of V1-reachable surface:** 75 / 87 ≈ **86%** (excludes the 8 composition primitives that are permanently above the protocol).
1448+
- **Coverage of all element-type switch arms:** 75 / 95 ≈ **79%**.
1449+
- **Path to 100% reachable:** the next PR ports the 12 deferred (overlays, NavigationHost, TabView gap closure, GridView CCC virtualization, XamlHost/Page unification). Phase 4 cleanup follows.
14371450

14381451
**Phase 3 finish advisory perf** — Cloud PC x64 re-capture under `docs/specs/047/phase3-results/CPC-ander-YTZ3O-x64-advisory/2026-05-28-phase3-finish-3x5/` (n=15, 3 launches × 5 reps). V1 ON (descriptors) vs V1 OFF (today), against prior `2026-05-27-phase3-closeout-3x5/`:
14391452

docs/specs/tasks/047-extensible-control-model-implementation.md

Lines changed: 134 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -928,14 +928,11 @@ carve-outs:
928928
- [x] **G3 typed lists — `TreeView`, `FlipView`, `TabView`, `Pivot`**
929929
closed by Phase 3 finish. **Note:** "FlipView" here is the
930930
simple non-templated `FlipViewElement` (Element[] items). The
931-
typed `TemplatedFlipViewElement<T>` peer stays carved exactly
932-
as documented at Phase 3 close-out — FlipView lacks
933-
`ContainerContentChanging` so the realization pipeline used by
934-
`TemplatedListView<T>` / `TemplatedGridView<T>` doesn't apply;
935-
a `PreMountedItems` strategy would land it but no fixture
936-
currently demands it. The intermediate marker base
937-
`TemplatedFlipViewElementBase` is reserved in the element
938-
hierarchy for that future port.
931+
typed `TemplatedFlipViewElement<T>` peer was ported in Phase 3
932+
completion via the new `PreMountedItems<>` strategy + base-derived
933+
`TemplatedFlipViewDescriptor` registered on
934+
`TemplatedFlipViewElementBase` — see the Phase 3 completion entry
935+
below.
939936
- **TreeView** via new `TreeChildren<TElement, TControl>`
940937
strategy (hierarchical, positional rebuild on Update,
941938
recursive `ContentElement` mount).
@@ -959,43 +956,139 @@ Phase 3 descriptor — out of scope for the Phase 3 batch list, recorded
959956
here for a future Phase 3.5 / Phase 4 prelude). Cross-referenced from
960957
the audit at the end of `spec/047-phase3-finish`:
961958

962-
- **Genuine engine gap:** `TemplatedFlipViewElement<T>` — close-out
963-
carve. Needs a `PreMountedItems` ChildrenStrategy. The intermediate
964-
base `TemplatedFlipViewElementBase` already exists for the
965-
base-derived registration.
966-
- **Untyped items hosts not ported:** `GridViewElement` (the plain
967-
Element[] variant — `ListViewElement` got a Phase 1 V1 handler,
968-
GridView did not), `ItemsViewElementBase` (the higher-level
969-
`ItemsView` wrapping its own ItemsRepeater), `ItemContainerElement`.
959+
- **Genuine engine gap (CLOSED — Phase 3 completion):**
960+
`TemplatedFlipViewElement<T>` — ported via the new
961+
`PreMountedItems<TElement, TControl>` ChildrenStrategy and
962+
`TemplatedFlipViewDescriptor`, registered base-derived against
963+
`TemplatedFlipViewElementBase`. The strategy pre-mounts every item
964+
up-front into `FlipView.Items` (no `ContainerContentChanging` to
965+
drive realization) and positionally reconciles via
966+
`Reconciler.ReconcileV1Child` on Update.
967+
- **Untyped items hosts (CLOSED — Phase 3 completion, partial):**
968+
`ItemsViewElementBase`, `ItemContainerElement` — ported as
969+
standard descriptors and registered in `RegisterV1BuiltInHandlers`.
970+
`GridViewElement` (plain Element[]) — descriptor authored
971+
(`GridViewDescriptor`) but **carved during PR #440 CR**: the
972+
descriptor's `ItemsHost<>` strategy pre-mounts every item into
973+
`GridView.Items` (one container per item, no virtualization),
974+
while the legacy `MountGridView` arm uses
975+
`ItemsSource = Range(0..N) + ItemTemplate + ContainerContentChanging`
976+
to realize containers lazily (matches Phase 1 `ListViewHandler`).
977+
Closing this needs either a hand-coded `GridViewHandler` or a new
978+
ChildrenStrategy variant wrapping CCC. Tracked alongside TabView /
979+
overlays / NavigationHost gap-closure.
980+
- **Heavy / specialized controls (CLOSED — Phase 3 completion):**
981+
`WebView2Element`, `NavigationViewElement`, `TitleBarElement`,
982+
`MediaPlayerElementElement`, `AnimatedVisualPlayerElement`,
983+
`MapControlElement`, `SemanticZoomElement`,
984+
`AnnotatedScrollBarElement`, `RefreshContainerElement`,
985+
`SwipeControlElement`, `ParallaxViewElement` — all descriptors
986+
authored and registered. (`NavigationHostElement` stays deferred —
987+
see below.)
988+
- **Polymorphic / a11y (CLOSED — Phase 3 completion):**
989+
`IconElement` (decorator-style handler via the
990+
`IDecoratorElementHandler` engine extension landed this phase),
991+
`SemanticElement`, `AnnounceRegionElement` — all registered.
992+
993+
**Phase 3 completion — still deferred to the next PR (not regressions;
994+
scoped carve list documented inline in `RegisterV1BuiltInHandlers`):**
995+
970996
- **Dialog / overlay family:** `ContentDialogElement`,
971997
`FlyoutElement`, `PopupElement`, `MenuBarElement`,
972998
`MenuFlyoutElement`, `CommandBarElement`,
973-
`CommandBarFlyoutElement`. Button-family `Flyout` ships through the
974-
`.OneWayBridged` setter on the button descriptors; the standalone
975-
flyout elements are their own legacy paths.
976-
- **Heavy / specialized controls:** `WebView2Element`,
977-
`NavigationViewElement`, `NavigationHostElement`,
978-
`TitleBarElement`, `MediaPlayerElementElement`,
979-
`AnimatedVisualPlayerElement`, `MapControlElement`,
980-
`SemanticZoomElement`, `AnnotatedScrollBarElement`,
981-
`RefreshContainerElement`, `SwipeControlElement`,
982-
`ParallaxViewElement`.
983-
- **Polymorphic / interop / a11y:** `IconElement` (already documented
984-
as escape-hatched — polymorphic mount), `SemanticElement`,
985-
`AnnounceRegionElement`, `XamlHostElement`, `XamlPageElement`.
986-
- **Reactor infrastructure (likely SHOULD stay out of V1 dispatch):**
987-
`ComponentElement`, `FuncElement`, `MemoElement`,
988-
`ErrorBoundaryElement`, `CommandHostElement`, `ModifiedElement`,
999+
`CommandBarFlyoutElement`. Modal lifecycle (control-side-mounted,
1000+
not parent-tree-mounted) requires decorator-style ports beyond
1001+
the IDecoratorElementHandler shape used for `IconElement`.
1002+
- **Stateful host:** `NavigationHostElement`. Per-instance
1003+
route/cache/transition state is intercepted in
1004+
`Reconciler.UnmountRecursive` BEFORE the V1 dispatch arm; needs
1005+
a small refactor to internal-expose `MountNavigationHost` /
1006+
`UpdateNavigationHost` and duplicate cleanup logic in the V1
1007+
handler before it can route through V1.
1008+
- **`TabViewDescriptor` (descriptor exists, registration carved):**
1009+
Bisect (3× clean V1 ON full selftest with only TabViewDescriptor
1010+
carved, vs. 1–4 random docking-text-find failures per run when
1011+
registered: DockHooks / PixDoc / RoleAware / Composition /
1012+
FloatRoot) ratifies the descriptor's documented gaps as hot in
1013+
the docking suite — missing spec 045 §2.4 drag pipeline
1014+
(`OnTabDragStarting` / `OnTabDragCompleted`), §2.2 pinnable
1015+
headers (`BuildTabHeader` / `BuildPinButton` / in-place
1016+
`TryUpdatePinHeaderInPlace`), in-place CanUpdate for tab content
1017+
(preserves focus/state on re-renders), conditional `SelectedIndex`
1018+
write, and `TabStripHeader` / `TabStripFooter` Element slots.
1019+
Closing them requires engine work (post-children mount-hook so
1020+
`SelectionChanged` subscribes after children-add + an
1021+
`ImperativeBridged` shape for the named tab strip slots).
1022+
- **`GridViewDescriptor` (descriptor exists, registration carved
1023+
during PR #440 CR):** The descriptor's `ItemsHost<>` ChildrenStrategy
1024+
pre-mounts every item into `GridView.Items` (one container per item,
1025+
no virtualization). The legacy `MountGridView` arm uses
1026+
`ItemsSource = Range(0..N) + ItemTemplate + ContainerContentChanging`
1027+
to realize containers lazily — matching Phase 1
1028+
`ListViewHandler`. A|B tests pass either way (no fixture stresses
1029+
GridView scale), but production memory/lifecycle would silently
1030+
regress. Closing this needs either a hand-coded `GridViewHandler`
1031+
mirroring `ListViewHandler`'s CCC virtualization, or a new
1032+
ChildrenStrategy variant (e.g. `RecyclingItemsHost<>`) that wraps
1033+
the `ItemsSource` + `ContainerContentChanging` realization
1034+
contract.
1035+
- **Interop bridges:** `XamlHostElement`, `XamlPageElement`. V1
1036+
descriptors exist (`XamlHostDescriptor`, `XamlPageDescriptor`)
1037+
but stay unregistered because `XamlInterop.Register(reconciler)`
1038+
populates the external `_typeRegistry` at app startup; auto-
1039+
registering V1 would clash via `EnsureRegistrableElementType`.
1040+
Unification is Phase 4 follow-up.
1041+
1042+
**Reactor composition primitives (intentionally above the V1
1043+
protocol — Phase 4 cleanup keeps their legacy arms):**
1044+
1045+
- `ComponentElement`, `FuncElement`, `MemoElement`,
1046+
`ErrorBoundaryElement`, `CommandHostElement`,
9891047
`Validation.FormFieldElement` /
990-
`ValidationVisualizerElement` / `ValidationRuleElement`. These are
991-
Reactor composition primitives, not WinUI control wrappers — they
992-
sit above the V1 handler protocol rather than being consumers of
993-
it.
994-
995-
The "100% V1 dispatch" goal as scoped by §14's Phase 3 batches IS
996-
met (every batch entry has a V1 handler or descriptor). The list
997-
above is genuine post-Phase-3 scope, not a regression against the
998-
shipped Phase 3 plan.
1048+
`ValidationVisualizerElement` / `ValidationRuleElement`. These
1049+
orchestrate child reconciliation rather than wrap a single WinUI
1050+
control, so the V1 handler protocol does not apply.
1051+
(`ModifiedElement` is intentionally NOT in this list — it's
1052+
unwrapped to its wrapped element BEFORE dispatch at the top of
1053+
`Reconciler.Mount`, so it never reaches the switch and does not
1054+
count as a carved arm.)
1055+
1056+
**Phase 3 completion status (PR #440 — landed-pending-merge):**
1057+
Every element type in the production codebase either (a) routes
1058+
through V1 dispatch (Phase 1 hand-coded handler OR Phase 3
1059+
descriptor registered in `RegisterV1BuiltInHandlers`), (b) is a
1060+
Reactor composition primitive intentionally kept above the V1
1061+
protocol, or (c) is in the explicit deferred carve list above with
1062+
a documented gap-closure path. The A|B parity bar — V1 ON ≡ V1 OFF
1063+
across the full xunit + selftest matrix — is met for every
1064+
registered element: 9134 xunit + 4410 selftest, 0 failures both
1065+
flags. Phase 4 cleanup can delete every legacy `MountXxx` /
1066+
`UpdateXxx` method that backs an element that has been registered
1067+
through V1; the legacy switch arms for the composition primitives
1068+
+ the deferred carve list must remain until their respective
1069+
follow-up PRs land.
1070+
1071+
**Quantified V1 dispatch coverage (post-PR #440):**
1072+
1073+
| Bucket | Arms | % of total |
1074+
|---|---:|---:|
1075+
| Routed through V1 (75 = 5 Phase 1 + 6 base-derived + 63 standard descriptors + 1 decorator) | 75 | 79% |
1076+
| Reachable-but-deferred (overlays 7, NavigationHost 1, TabView 1, GridView 1, XamlHost/Page 2) | 12 | 12.6% |
1077+
| Intentionally above V1 (composition primitives — permanent carve) | 8 | 8.4% |
1078+
| **Total `Reconciler.Mount.cs` switch arms** | **95** | **100%** |
1079+
1080+
- **Coverage of V1-reachable surface (excludes 8 composition primitives):** 75 / 87 ≈ **86%**.
1081+
- **Coverage of all switch arms:** 75 / 95 ≈ **79%**.
1082+
- **Path to 100% reachable:** the follow-up PR closes the 12 deferred:
1083+
1. Port the 7 overlay descriptors (ContentDialog, Flyout, Popup, MenuBar, MenuFlyout, CommandBar, CommandBarFlyout) — needs a decorator strategy variant for modal lifecycle beyond `IDecoratorElementHandler`.
1084+
2. Refactor `NavigationHostElement` cleanup path so V1 can own it (internal-expose `MountNavigationHost` / `UpdateNavigationHost`, duplicate cleanup logic in the V1 handler, remove the `UnmountRecursive` intercept).
1085+
3. Close `TabViewDescriptor` gaps (engine post-children mount-hook + `ImperativeBridged` for named slots + port `BuildTabHeader` / `BuildPinButton` / `TryUpdatePinHeaderInPlace` + drag pipeline trampolines + conditional `SelectedIndex` write + in-place `CanUpdate`).
1086+
4. Close `GridViewDescriptor` lifecycle gap — either author a Phase 1 hand-coded `GridViewHandler` mirroring `ListViewHandler`'s CCC virtualization, or introduce a `RecyclingItemsHost<>` ChildrenStrategy variant.
1087+
5. Unify `XamlInterop.Register` with V1 auto-registration so `XamlHostElement` / `XamlPageElement` descriptors can register without `EnsureRegistrableElementType` clash.
1088+
1089+
Phase 4 cleanup (deletion of legacy switch arms + `UseV1Protocol`
1090+
flag) is unblocked for the 75 routed arms today; the remaining 12
1091+
arms unblock as the follow-up PR lands each closure.
9991092

10001093
**Carry-forward known defects** (from Phase 1):
10011094

src/Reactor/Core/Element.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3831,8 +3831,12 @@ public enum ItemsViewLayoutKind
38313831
/// shape: virtual hooks for factory creation, in-place update,
38323832
/// per-row reconcile, and event callback dispatch.
38333833
/// </summary>
3834-
public abstract record ItemsViewElementBase : Element
3834+
public abstract record ItemsViewElementBase : Element, global::Microsoft.UI.Reactor.Core.Internal.IKeyedItemSource
38353835
{
3836+
int global::Microsoft.UI.Reactor.Core.Internal.IItemViewSource.ItemCount => ItemCount;
3837+
Element global::Microsoft.UI.Reactor.Core.Internal.IItemViewSource.BuildItemView(int index) => BuildItemViewAt(index);
3838+
string global::Microsoft.UI.Reactor.Core.Internal.IKeyedItemSource.GetKeyAt(int index) => GetKeyAt(index);
3839+
38363840
public ItemsViewLayoutKind LayoutKind { get; init; } = ItemsViewLayoutKind.StackLayout;
38373841
public ItemsViewSelectionMode SelectionMode { get; init; } = ItemsViewSelectionMode.Single;
38383842
public bool IsItemInvokedEnabled { get; init; }
@@ -3844,6 +3848,7 @@ public abstract record ItemsViewElementBase : Element
38443848
public abstract bool TryUpdateFactory(IElementFactory existingFactory);
38453849
public abstract void RefreshRealizedItems(IElementFactory factory, WinUI.ItemsRepeater repeater);
38463850
internal abstract void AttachListStateToFactory(IElementFactory factory, Internal.ReactorListState listState);
3851+
internal abstract Element BuildItemViewAt(int index);
38473852
/// <summary>Dispatch an <c>ItemInvoked</c> event to the typed callback.</summary>
38483853
public abstract void InvokeItemInvoked(int index);
38493854
/// <summary>Dispatch a <c>SelectionChanged</c> snapshot to the typed callback.</summary>
@@ -3910,6 +3915,8 @@ internal override void PreflightFirstItem()
39103915
_ = GuardedViewBuilder(Items[0], 0);
39113916
}
39123917

3918+
internal override Element BuildItemViewAt(int index) => GuardedViewBuilder(Items[index], index);
3919+
39133920
public override IElementFactory CreateFactory(Reconciler reconciler, Action requestRerender, ElementPool? pool) =>
39143921
new ElementFactory<T>(Items, GuardedViewBuilder, reconciler, requestRerender, pool);
39153922

0 commit comments

Comments
 (0)