Skip to content

Fix AutoSuggestBox empty suggestions and preserve native unmount callbacks - #1434

Merged
Alexandre Zollinger Chohfi (azchohfi) merged 16 commits into
microsoft:mainfrom
bkudiess:bkudiess-reactor-empty-suggestions
Oct 30, 2026
Merged

Alexandre Zollinger Chohfi (azchohfi) merged 16 commits into
microsoft:mainfrom
bkudiess:bkudiess-reactor-empty-suggestions

Conversation

@bkudiess

@bkudiess Barbara Kudiess (bkudiess) commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Use ordinary OneWay for AutoSuggestBoxElement.Suggestions: the previous conditional binding skipped empty arrays, leaving stale native choices visible and selectable. Empty suggestions map to null ItemsSource, so clearing works while preserving direct Items.Add(...) setters on initial mount. Suggestions still apply before controlled Text.

Also fix native-identity defects exposed by CI: .OnUnmount(...) callbacks and keyed-list diagnostic ledgers now live on native attached ReactorState, rather than depending on transient WinRT managed wrappers. Teardown consumes the callback before invocation, clears its registration on detach/pool return, and uses an opaque reconciler-owner token so an app-retained control remounted in another host cannot fire the disposed host's callback. Modifier removal clears only its own still-current callback, preserving an inner target's newly registered callback when a shared-control Flyout wrapper drops its own.

Native regressions cover suggestion clearing/repopulation/direct-items compatibility, callback replacement/removal, Flyout shared-target callbacks, dispose-A/remount-B with bare/modifier-only/callback-bearing controls, and exactly-once teardown. Collection regressions assert fresh managed wrappers through weak-table identity before checking callbacks or warning counts; WinRT WeakReference is unsuitable because it can reproject a live native object. The keyed WrapGrid fixture retains original Border references and checks actual reversed native order instead of hashes of collectible wrappers.

No public API changes, consumer changes, or package publication. The hyperlink/accelerator callback-table audit suggested in review remains a separate follow-up; those tables are not changed or claimed fixed here.

Linked issue / spec

Test plan

Current head fc8a1c3a515ca073a0ee3131417e564157c8b835 is CI-green: 39 successful checks, 3 intentional skips, none failing or pending, verified 2026-10-30 UTC. Base 8f17bac9230565281c84972d65f3b5d49e9aaf5d is integrated and the PR is mergeable. CI run 40982339278 passed the Release solution build, unit tests, both native/packaged/E2E/AOT shards, trim proof, integration, docs/analyzer and fuzz gates. Coverage, Package, Bootstrap, Build metrics and Node projects also passed. The intentional skips are Audit ledger gate, Devtools trim mstat, and Docs tier-drift.

All substantive feedback in the latest human review has been fixed and answered; the two latest human threads are left for the reviewer to resolve. A fresh current-head Copilot review request and lifecycle-label updates still need maintainer permissions. CI-green does not imply fresh approval or merge.

Exact command Result on current source
CI: dotnet restore Reactor.slnx, then dotnet build Reactor.slnx --no-restore --configuration Release Both passed on current head. Release build: 4 warnings, 0 errors. Build solution.
dotnet build src\Reactor\Reactor.csproj --no-restore -c Release -p:Platform=x64 -p:SkipSignaturesGen=true -m:1 -nodereuse:false --nologo -v:quiet Passed: 0 warnings, 0 errors.
dotnet build src\Reactor\Reactor.csproj --no-restore -p:Platform=x64 -p:SkipSignaturesGen=true -m:1 -nodereuse:false --nologo -v:quiet Passed: 0 warnings, 0 errors.
dotnet build tests\Reactor.AppTests.Host --no-restore -p:BuildProjectReferences=false -p:Platform=x64 -p:SkipSignaturesGen=true -m:1 -nodereuse:false --nologo -v:quiet Passed after the core builds: 0 warnings, 0 errors. Dependencies were already built; this avoids rebuilding them during focused iterations.
dotnet run --project tests\Reactor.AppTests.Host --no-build -c Debug -p:Platform=x64 -- --self-test --filter 'SlotReplace' Passed: 17 fixtures, 0 failures. Includes both fresh-TextBlock-wrapper preconditions and the dispose/remount and shared-Flyout callback cases.
dotnet run --project tests\Reactor.AppTests.Host --no-build -c Debug -p:Platform=x64 -- --self-test --filter 'KeyedListDiagnostics_RowsKeysAndWarnings' Passed: 0 failures, including the fresh-ItemsView-wrapper precondition and one warning entry per kind with count 2.
dotnet build tests\Reactor.Tests --no-restore -p:BuildProjectReferences=false -p:Platform=x64 -p:SkipSignaturesGen=true -m:1 -nodereuse:false --nologo -v:quiet Passed: 0 errors, 2 existing NU1902/NU1903 audit warnings for System.Security.Cryptography.Pkcs 9.0.6.
dotnet test tests\Reactor.Tests --no-build -p:Platform=x64 --filter 'FullyQualifiedName~ModifierUnsetClearValueTests|FullyQualifiedName~ReactorDiagnosticsTests|FullyQualifiedName~KeyedListAttributionTests' Passed: 48 tests, 0 failures/skips. Callback and owner-token fields are explicitly classified as managed bookkeeping by the reset guard; dependency-property checks remain intact.
dotnet build tests\Reactor.PackagedTests.Host --no-restore -p:BuildProjectReferences=false -p:Platform=x64 -p:SkipSignaturesGen=true -m:1 -nodereuse:false --nologo -v:quiet Passed: 0 warnings, 0 errors.
dotnet build tests\Reactor.PackagedTests --no-restore -p:BuildProjectReferences=false -p:Platform=x64 -p:SkipSignaturesGen=true -m:1 -nodereuse:false --nologo -v:quiet Passed: 0 warnings, 0 errors.
With REACTOR_PACKAGED_FILTER=SlotReplace: dotnet test tests\Reactor.PackagedTests --no-build -p:Platform=x64 Passed on pushed source: 145 tests, 0 failures/skips, including the package identity guard.
git diff --check Passed; worktree clean after the normal push.

Mutation evidence: restoring the unconditional callback clear fails the original Flyout-target teardown checks with calls=0. Restoring wrapper-keyed diagnostic storage leaves the collection precondition passing but produces two entries per warning kind with count 1, failing both count assertions. Removing the owner check makes the bare and Margin-only remounts invoke host A's callback; removing the current-delegate check loses the inner Flyout callback. The latter mutations produce six failed assertions. All mutations were removed before final builds and runs. Earlier callback-update mutations also proved the a → b → unset clear and latest-callback replacement assertions.

Prior-head evidence and local limitations (not current-head CI evidence)

Head 5527d0e6d7e64c3356064c807bd21732f399d941, with base 6f5e15275f194c521f06bdc087b5c0610f62bef5, passed all 42 checks: 39 successes and 3 intentional skips (Audit ledger gate, Devtools trim mstat, Docs tier-drift). CI run 40967438116 passed both native, packaged, E2E and AOT shards, unit tests, trim proof, integration, docs/analyzer and fuzz gates; Coverage, Package, Node projects, Bootstrap and Build metrics also passed. Its exact CI commands dotnet restore Reactor.slnx, then dotnet build Reactor.slnx --no-restore --configuration Release both passed; Release build: 4 warnings, 0 errors (Build solution). The local dotnet build Reactor.slnx --no-restore -c Release -v:quiet attempt did not finish and was stopped; no local full-solution pass is claimed. It was not repeated for the final ownership changes; the current-head full solution gate passed in CI as recorded above.

Earlier heads 2ade3bf4 (CI 40821636243) and 9c08549f (CI 40808730548) also completed with 39 successful checks and the same 3 intentional skips. These are historical results, not evidence for the final head.

Prior deterministic failures established the mechanisms: forced collection lost the text-child unmount callback with wrapper-keyed storage; retained Border references corrected the keyed identity fixture's hash-only oracle; the diagnostic regression produced two count-1 entries per kind; and the DP-reset guard misclassified the plain callback field before its documented bookkeeping exception was added. No failing assertion was weakened to bypass them.

Earlier full local runs had failures not established against an unchanged baseline. At 036bacb6, REACTOR_SELFTEST_SHARD=1/2 with dotnet test tests\Reactor.PackagedTests --no-build -p:Platform=x64 had 1,019 total, 1,007 succeeded, 6 failed and 6 skipped: TemplatedList_ChurnLeavesNoOrphanedRows, four TitleBarHeight_* cases, and WindowHookOwner_MidRenderOpen. The corresponding packaged CI shards passed. An earlier ARM64 full selftest run had 1,815 passed, 13 failed and 42 skipped, including keyed identity, live-dialog publication, window-level/height/hook cases and an AutoSuggestBox timeout; the latter passed independently. These failures were not labeled environmental or flaky without evidence, and OS settings, timing budgets and assertions were not changed to bypass them.

An earlier local split solution build at 036bacb6 restored successfully with audit warnings, then failed with 3 MSB3030 errors copying deeply nested bundled Copilot CLI assets in sample projects (plus 4 warnings); its corresponding CI solution build passed. Original ARM64 unit tests passed 15,122 with 64 skipped, and original x64/ARM64 suggestions mutations failed the empty-suggestions assertion before restoring the fix. No new full local-suite or consumer validation is claimed here.

Risk / breaking changes

Empty Suggestions now actively clears the native list rather than leaving old choices alone; null ItemsSource preserves direct-items customization. Non-empty diffing, application before controlled Text, and control identity remain unchanged. Callback registration now follows native lifetime but is scoped to an opaque reconciler identity; consumption clears callback and owner before invocation for reentrant exactly-once behavior. Callback-free modifier application avoids a native lookup unless removing a prior callback. Native warning ledgers are lazy and keep their per-control lifetime counts, including pool reuse; non-native and contextual callers keep their prior behavior. No public API, remount workaround, package release, or unrelated callback-table audit is included.

Use an unconditional one-way Suggestions binding so an empty array clears the native ItemsSource. Extend the existing native update fixture to cover clearing without remounting and later repopulation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues block the AutoSuggestBox stale-suggestions fix.

No open issues 🎉

What changed in this PR

Fixes Reactor’s AutoSuggestBox retaining stale choices when Suggestions becomes empty, without changing the public API.

Changes:

  • Uses ordinary OneWay binding so empty suggestions clear the native ItemsSource.
  • Extends the native update fixture to cover clearing, repopulation, preserved control identity, and text updates.
  • Documents the behavior fix in the changelog.
File Description
tests/​Reactor.AppTests.Host/​SelfTest/​Fixtures/​ControlUpdateFixtures2.cs Adds regression assertions for suggestion updates and control identity.
src/​Reactor/​Core/​Element.cs Removes the condition that skipped empty suggestion arrays.
CHANGELOG.md Records the stale-suggestions fix under Fixed.

Review effort: Balanced

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix looks right — plain OneWay matches spec 047 §6.1 and the fixture would fail if it were reverted. One non-blocking inline suggestion about the empty-array write on mount. Approving.

Comment thread src/Reactor/Core/Element.cs Resolved
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Map empty suggestions to a null ItemsSource and cover default-mount direct items alongside clear and repopulate updates.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Store imperative teardown in native attached state instead of a weak table keyed by a transient WinRT wrapper. Force collection in the slot replacement regression, which reproduces the CI failure before this fix.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Forced collection reproduces the managed identity-hash mismatch without replacing native controls. Hold original Border references, compare identity directly, and assert reversed native order as well.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AutoSuggestBox follow-up looks good, thanks for taking the null mapping. Moving .OnUnmount into ReactorState is the right fix, and I traced every unmount path: the callback fires before state is cleared and can't fire twice. Inline: a missing test for changing or removing .OnUnmount on an update, a cheaper clear branch, a changelog citation, and a test nit.

Not blocking:

  • The unmount fix is unrelated to AutoSuggestBox and the bug already exists on main. Could you retitle the PR so it shows in history, or split it out?
  • docs/specs/tasks/047-extensible-control-model-implementation.md:573-575 still says empty Suggestions doesn't clear ItemsSource.
  • s_hyperlinkClickActions (Reconciler.Update.cs:1106-1121) has the same lost-callback problem, and _commandAccelerators in CommandBindings.cs might too. Worth a follow-up issue.

Comment thread src/Reactor/Core/Reconciler.cs Resolved
Comment thread CHANGELOG.md Resolved
Test replacing and clearing teardown callbacks without remounting, and gate absent-callback cleanup on the prior modifier. Correct spec and suggestions documentation and drop redundant WrapGrid collection.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@bkudiess Barbara Kudiess (bkudiess) changed the title Fix AutoSuggestBox retaining suggestions after an empty update Fix AutoSuggestBox empty suggestions and preserve native unmount callbacks Oct 29, 2026
@bkudiess

Copy link
Copy Markdown
Contributor Author

The unmount fix is unrelated to AutoSuggestBox ... retitle the PR so it shows in history, or split it out?

Retitled this PR to “Fix AutoSuggestBox empty suggestions and preserve native unmount callbacks” so both fixes are visible in history.

docs/specs/tasks/047-extensible-control-model-implementation.md:573-575 still says empty Suggestions doesn't clear ItemsSource.

Fixed in d7f427a: the task ledger now marks that gap resolved and describes clearing to null while preserving direct Items.Add setters on mount.

s_hyperlinkClickActions ... and _commandAccelerators ... Worth a follow-up issue.

Leaving that audit for a separate follow-up rather than extending this PR to additional callback tables. Neither table is changed here or claimed fixed.

Use attached ReactorState as the diagnostic ledger key for native controls. Force collection between ItemsView mount and update to reproduce and cover the duplicate-ledger failure. Classify the managed teardown field correctly in the dependency-property reset guard.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick turnaround. The update-path fixture and the gated clear look right, and I traced every pool and drop-all-modifiers path. The gate also fixes a case that's broken on main (inline). The diagnostics fix makes sense. Approving with three small inline follow-ups (one missing test, one storage-consistency nit, one test-precondition check). None block merge.

Comment thread src/Reactor/Core/Reconciler.cs Resolved
Cover Flyout target teardown, keep native diagnostic ledgers on ReactorState, and assert wrapper collection before checking warning deduplication.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the Flyout test, the ledger move and the garbage-collection check all look good, and the merge with main is clean. Two follow-ups inline:

  • Moving .OnUnmount onto the native control means it now outlives a disposed reconciler, so a reused control can fire the old host's callback. An owner token would fix that.
  • The SlotReplacementFixtures garbage-collection step needs the same fresh-wrapper check you just added for ItemsView.

Also, docs/specs/tasks/042-keyed-list-reconciliation-implementation.md:735-737 still says the per-control dedup uses a ConditionalWeakTable.

Comment thread src/Reactor/Core/Reconciler.cs Resolved
Preserve a newly registered inner callback when a shared wrapper removes its own, and assert wrapper collection in the slot teardown regressions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@bkudiess

Copy link
Copy Markdown
Contributor Author

docs/specs/tasks/042-keyed-list-reconciliation-implementation.md:735-737 still says the per-control dedup uses a ConditionalWeakTable.

Updated in e10560a: native ledgers live in ReactorState.KeyedDiagnosticLedger across wrapper collection and pool reuse; only non-native object identities use the ConditionalWeakTable. The contextual fallback and the fact that ledgers do not retain their controls are documented too.

Describe host scoping as preserved behavior in the changelog, document the single shared-control unmount slot, and note that host disposal does not guarantee .OnUnmount.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@azchohfi
Alexandre Zollinger Chohfi (azchohfi) merged commit 7479156 into microsoft:main Oct 30, 2026
42 checks passed
Alexandre Zollinger Chohfi (azchohfi) pushed a commit that referenced this pull request Nov 3, 2026
Get-PinnedWindowsAppSdkVersion reads via [IO.File]::ReadAllText, which
resolves relative paths against the process CWD rather than Push-Location's,
so the scaffold step failed to find ../Directory.Build.props.

(The .OnUnmount fix this commit originally carried landed on main
independently in #1434, with owner tracking and a GC regression test, so it
was dropped in favour of main's version during the rebase.)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 17bce8bb-d57a-4f76-a55d-27c9029fc9a3
Alexandre Zollinger Chohfi (azchohfi) pushed a commit that referenced this pull request Nov 4, 2026
Get-PinnedWindowsAppSdkVersion reads via [IO.File]::ReadAllText, which
resolves relative paths against the process CWD rather than Push-Location's,
so the scaffold step failed to find ../Directory.Build.props.

(The .OnUnmount fix this commit originally carried landed on main
independently in #1434, with owner tracking and a GC regression test, so it
was dropped in favour of main's version during the rebase.)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 17bce8bb-d57a-4f76-a55d-27c9029fc9a3
Alexandre Zollinger Chohfi (azchohfi) pushed a commit that referenced this pull request Nov 5, 2026
Get-PinnedWindowsAppSdkVersion reads via [IO.File]::ReadAllText, which
resolves relative paths against the process CWD rather than Push-Location's,
so the scaffold step failed to find ../Directory.Build.props.

(The .OnUnmount fix this commit originally carried landed on main
independently in #1434, with owner tracking and a GC regression test, so it
was dropped in favour of main's version during the rebase.)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 17bce8bb-d57a-4f76-a55d-27c9029fc9a3
Alexandre Zollinger Chohfi (azchohfi) pushed a commit that referenced this pull request Nov 6, 2026
Get-PinnedWindowsAppSdkVersion reads via [IO.File]::ReadAllText, which
resolves relative paths against the process CWD rather than Push-Location's,
so the scaffold step failed to find ../Directory.Build.props.

(The .OnUnmount fix this commit originally carried landed on main
independently in #1434, with owner tracking and a GC regression test, so it
was dropped in favour of main's version during the rebase.)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 17bce8bb-d57a-4f76-a55d-27c9029fc9a3
Alexandre Zollinger Chohfi (azchohfi) pushed a commit that referenced this pull request Nov 7, 2026
Get-PinnedWindowsAppSdkVersion reads via [IO.File]::ReadAllText, which
resolves relative paths against the process CWD rather than Push-Location's,
so the scaffold step failed to find ../Directory.Build.props.

(The .OnUnmount fix this commit originally carried landed on main
independently in #1434, with owner tracking and a GC regression test, so it
was dropped in favour of main's version during the rebase.)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 17bce8bb-d57a-4f76-a55d-27c9029fc9a3
Alexandre Zollinger Chohfi (azchohfi) pushed a commit that referenced this pull request Nov 7, 2026
Get-PinnedWindowsAppSdkVersion reads via [IO.File]::ReadAllText, which
resolves relative paths against the process CWD rather than Push-Location's,
so the scaffold step failed to find ../Directory.Build.props.

(The .OnUnmount fix this commit originally carried landed on main
independently in #1434, with owner tracking and a GC regression test, so it
was dropped in favour of main's version during the rebase.)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 17bce8bb-d57a-4f76-a55d-27c9029fc9a3
Alexandre Zollinger Chohfi (azchohfi) pushed a commit that referenced this pull request Nov 10, 2026
Get-PinnedWindowsAppSdkVersion reads via [IO.File]::ReadAllText, which
resolves relative paths against the process CWD rather than Push-Location's,
so the scaffold step failed to find ../Directory.Build.props.

(The .OnUnmount fix this commit originally carried landed on main
independently in #1434, with owner tracking and a GC regression test, so it
was dropped in favour of main's version during the rebase.)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 17bce8bb-d57a-4f76-a55d-27c9029fc9a3
Alexandre Zollinger Chohfi (azchohfi) pushed a commit that referenced this pull request Nov 11, 2026
Get-PinnedWindowsAppSdkVersion reads via [IO.File]::ReadAllText, which
resolves relative paths against the process CWD rather than Push-Location's,
so the scaffold step failed to find ../Directory.Build.props.

(The .OnUnmount fix this commit originally carried landed on main
independently in #1434, with owner tracking and a GC regression test, so it
was dropped in favour of main's version during the rebase.)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 17bce8bb-d57a-4f76-a55d-27c9029fc9a3
Alexandre Zollinger Chohfi (azchohfi) pushed a commit that referenced this pull request Nov 12, 2026
Get-PinnedWindowsAppSdkVersion reads via [IO.File]::ReadAllText, which
resolves relative paths against the process CWD rather than Push-Location's,
so the scaffold step failed to find ../Directory.Build.props.

(The .OnUnmount fix this commit originally carried landed on main
independently in #1434, with owner tracking and a GC regression test, so it
was dropped in favour of main's version during the rebase.)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 17bce8bb-d57a-4f76-a55d-27c9029fc9a3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants