fix(settings): release Dashboard and Quick Access subscriptions #50553

Closed
niels9001 opened 8:31am on September 13, 2026 wanted to merge 586 Ξ” into microsoft/powertoys main from
niels9001-dashboard-quickaccess-lifetime
Diff Delta:
586
About 79 Diff Delta/hour
Classified as:  General

niels9001's Description of Work

Summary of the Pull Request

Release Quick Access's retained subscriptions when its owner is torn down, and keep Dashboard page reloads functional. This is an independent lifetime fix based on main at 8e832ee72dcffb5df297ceb9e06a04797375092c, not a Dashboard rendering/startup rewrite.

PR Checklist


  • [ ] Closes: N/A β€” no issue is claimed resolved; in particular, this does not close #49965.
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) -->

  • [x] Communication: I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected

  • [x] Tests: Added/updated and all pass β€” targeted suite, 21/21.

  • [x] Localization: All end-user-facing strings can be localized β€” no new strings.

  • [ ] Dev docs: Added/updated β€” N/A, internal ownership fix.

  • [ ] New binaries: Added on the required places β€” N/A, no new binaries or projects.



  • [ ] Documentation updated: If checked, please file a pull request on our docs repo and link it here: N/A.

Detailed Description of the Pull Request / Additional comments

Root cause: QuickAccessViewModel subscribed to the long-lived General and Keyboard Manager repositories and installed a callback on the General settings snapshot, but had no cleanup. Snapshot replacement also left its callback on the old snapshot. Dashboard disposed its own General subscription and base hotkey-conflict subscription, but never its owned Quick Access model.

Changes:



  • src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessViewModel.cs: idempotent disposal detaches both repositories and its enabled-module callback; queued/in-flight notifications cannot refresh or reattach after teardown. Live refreshes read the latest repository snapshot and detach the old snapshot. Internal dependency injection lets tests use actual subscriptions/refresh logic without touching real settings, resources or module tooltip repositories. The public constructor still eagerly initializes the same items/tooltips.


  • src/settings-ui/Settings.UI.Library/EnabledModules.cs and GeneralSettings.cs: add exact-registration removal with compare/exchange. The existing callback is a single Action, not a multicast event. Removal cannot clear a newer owner's callback; it does not restore older callbacks or change assignment semantics.


  • src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs: cascade disposal to the owned Quick Access model; retain existing General/base cleanup. The internal constructor shares production subscription/ownership wiring with isolated tests.


  • src/settings-ui/Settings.UI/ViewModels/DashboardViewModelLifetime.cs and Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs: scope ownership to the page's loaded lifetime, recreate a disposed model when the same page reloads, and refresh both DataContext and compiled bindings with Bindings.Update(). Duplicate load/unload calls do not add listeners.


  • src/settings-ui/QuickAccess.UI/ViewModels/LauncherViewModel.cs and QuickAccess.UI/QuickAccessXAML/MainWindow.xaml.cs: dispose the standalone model at actual window disposal, not when the flyout hides or navigates between its pages.


  • src/settings-ui/Settings.UI.UnitTests/ViewModelTests/QuickAccessLifetimeTests.cs and Settings.UI.Controls/Settings.UI.Controls.csproj: existing test-project coverage and friend access, with no new packages.

Risk: medium. Shared model ownership and page rebinding change. Repository/watchers are not disposed; JSON/schema, IPC, GPO rules, launch commands, sort behavior and item/tooltips policy are unchanged. Dashboard shortcut rows were inspected: they copy key lists and do not add hotkey subscriptions.

Overlap: Open non-draft #49891 (a1ee77a7c8a4c174baf5bcb131fe82afbc6fec80) touches the same QuickAccessViewModel.cs and DashboardViewModel.cs. Its lazy tooltip/detail initialization and ActionModules removal are not included here. There is textual overlap in Quick Access item initialization/tooltip access; merging both requires keeping that PR's lazy behavior while routing tooltip lookup through this PR's injected delegate. Dashboard's lifetime changes are separate from its lazy-detail changes. That PR and its status are untouched. This PR also does not include Home rendering #50530 or deferred shortcut editors #50532.

Non-goals / limitations: Not module-wide live-state synchronization, not a fix for existing Mouse Utilities pages in #49965, and no frame-time/working-set claim. Earlier retained-Dashboard observations on an older installed Debug build are not a current-main baseline. These tests establish cleanup of the specific Quick Access roots and the Dashboard ownership chain; they do not establish that every possible Dashboard/UI retention root is resolved.

Validation Steps Performed

From the respective project folders:

# src\settings-ui\Settings.UI.UnitTests (includes affected Settings production graph)

& ..\..\..\tools\build\build.ps1 -Platform x64 -Configuration Release -ExtraArgs '/m:2'

# src\settings-ui\QuickAccess.UI (standalone consumer)
& ..\..\..\tools\build\build.ps1 -Platform x64 -Configuration Release -ExtraArgs '/m:2'

Both completed with exit code 0. Initial fresh-worktree missing-assets failures were resolved with targeted restores (/t:Restore /p:RestorePackagesConfig=true /p:RestoreRepositoryPath=<worktree>\packages); the separately invoked Settings.UI.XamlIndexBuilder also needed its own targeted restore. No full-solution build.

Using the installed Visual Studio vstest.console.exe, from the repository root:

vstest.console.exe 'Release\x64\tests\SettingsTests\net10.0-windows10.0.26100.0\Settings.UI.UnitTests.dll' '/Platform:x64' '/TestCaseFilter:FullyQualifiedName~QuickAccessLifetimeTests|FullyQualifiedName~ViewModelTests.MeasureTool.NormalizeUnitsOfMeasureIndexReturnsValidComboBoxIndex|FullyQualifiedName~CommonLibTest.MeasureToolSettingsTests' '/Logger:trx;LogFileName=dashboard-quickaccess-lifetime.trx' '/ResultsDirectory:<private-artifacts>\test-results'

21/21 passed, including 10 lifetime cases and 11 representative existing cases. Coverage includes both repository subscriptions, idempotence, queued and captured in-flight callbacks, superseded/latest snapshots, old/new owner disposal ordering, old-snapshot callback ownership, local enabled changes, Keyboard Manager editor changes, collection/item/command identity, and Dashboard unload/reload through the production owner helper. Dashboard-owned General subscribers return 2 β†’ 0 and Keyboard Manager subscribers 1 β†’ 0. Twenty constructed/disposed Dashboard + Quick Access pairs become weak-reference collectible after queued work drains.

Tests use in-memory repositories and injected labels/tooltips/dispatch; Dashboard owner tests exercise its production subscription constructor without constructing module rows or a WinUI page. Generated Dashboard binding code was inspected to confirm Bindings.Update() switches the visible QuickAccessList.ItemsSource and detaches old model listeners. This is not a real-window reload/navigation run.

Release/x64 only; no ARM64 compilation, full test suite, installed-app UI run, Runner launch, settings/profile changes, or user-process interaction. Build logs/binlogs/TRX remain private and are not uploaded. Manual through-Runner validation is pending separately; this PR intentionally remains draft.

10 total changed files
(1 file ignored)
Loading changes...
Loading changes...
Loading changes...
Loading changes...
Loading changes...
Loading changes...
Loading changes...
Loading changes...
Loading changes...
You've made it to the end of the scroll.
LGTM-land is officially in view