Skip to content

[Windows] Shell: let flyout items release the shell on disconnect - #39591

Open
FullDevStudio wants to merge 1 commit into
dotnet:mainfrom
FullDevStudio:fix/shell-flyout-items-release-shell
Open

FullDevStudio wants to merge 1 commit into
dotnet:mainfrom
FullDevStudio:fix/shell-flyout-items-release-shell

Conversation

@FullDevStudio

Copy link
Copy Markdown
Contributor

Our app swaps between two shells, and on Windows every old shell stayed in memory with all its pages, about 90 MB per switch. A gcdump showed the path: WinUI keeps the ShellFlyoutItemView of each flyout entry alive through its CCW, and the view still holds _shell and _content (whose parent is the shell item). It only lets go of them when its DataContext changes, and that does not happen when the shell's handler disconnects.

ShellView.Disconnect now also tells every ShellFlyoutItemView under it to release its content and the shell. The cleanup that used to sit in OnDataContextChanged is moved into ReleaseContent, which both paths use, so nothing changes for a DataContext change.

I added a Windows device test that shows a shell with two flyout items, swaps the window's page, disconnects the shell handler and waits for the shell to be collected. Our workaround sets DataContext = null on each ShellFlyoutItemView after the swap; with it, twelve switches left one old shell instead of twelve.

Description of Change

Issues Fixed

Fixes #

Our app swaps between two shells, and on Windows every old shell stayed
in memory with all its pages, about 90 MB per switch. A gcdump showed
the path: WinUI keeps the ShellFlyoutItemView of each flyout entry alive
through its CCW, and the view still holds _shell and _content (whose
parent is the shell item). It only lets go of them when its DataContext
changes, and that does not happen when the shell's handler disconnects.

ShellView.Disconnect now also tells every ShellFlyoutItemView under it to
release its content and the shell. The cleanup that used to sit in
OnDataContextChanged is moved into ReleaseContent, which both paths use,
so nothing changes for a DataContext change.

I added a Windows device test that shows a shell with two flyout items,
swaps the window's page, disconnects the shell handler and waits for the
shell to be collected. Our workaround sets DataContext = null on each
ShellFlyoutItemView after the swap; with it, twelve switches left one old
shell instead of twelve.
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 39591

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 39591"

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service dotnet-policy-service Bot added the community ✨ Community Contribution label Oct 23, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Hey there @@FullDevStudio! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Hey there @FullDevStudio! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@github-actions github-actions Bot 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.

Expert Review — 2 findings

Reviewed commit dbd94e4.

See inline comments for details.

{
ReleaseContent();
Content = null;
_shell = null;

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.

[critical] Logic and Correctness - Clearing _shell makes subsequent native measurement throw: MeasureOverride evaluates ShellView, whose getter dereferences _shell.Handler without checking _shell. Disconnecting a realized Shell while its native flyout is still attached, then allowing another layout pass, now produces a NullReferenceException instead of taking the existing null fallback. Clearing Content removes the template content, not the ShellFlyoutItemView from its NavigationViewItem, so it does not prevent this callback. Make the getter or measure callback tolerate the disconnected state. The added test removes the Shell before disconnecting and does not exercise measurement afterward.


var shellReference = await CreateAndRemoveShellWithFlyoutItems();

await AssertionExtensions.WaitForGC(shellReference);

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.

[moderate] Regression Prevention and Test Coverage - This GC assertion runs after the native test window has already closed. CreateAndRemoveShellWithFlyoutItems awaits CreateHandlerAndAddToWindow, whose Windows SetupWindowForTests finally block disconnects the window handler and calls newWindow.Close() before returning. The reported leak occurs while swapping Shells inside a window that remains open; closing that window first removes the native lifetime condition this test needs to preserve. Keep the window open through the collection assertion and isolate the old Shell's strong locals in a separate helper, so the test checks release during a Shell swap rather than after window teardown.

@kubaflo kubaflo 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.

Could you check the ai's suggestions?

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.

🟡 Changes recommended

Clearing _shell leaves subsequent flyout measurement callbacks vulnerable to a null-reference exception.

1 open issue
What changed in this PR

This PR addresses Windows Shell retention by releasing flyout-item references when the Shell handler disconnects.

Changes:

  • Reuses content cleanup for both data-context changes and disconnects.
  • Disconnects descendant flyout-item views during Shell teardown.
  • Adds a Windows device test that checks Shell collection after a page swap.
File Description
src/​Controls/​tests/​DeviceTests/​Elements/​Shell/​ShellTests.Windows.cs Adds a garbage-collection regression test.
src/​Controls/​src/​Core/​Handlers/​Shell/​Windows/​ShellView.cs Invokes flyout-item cleanup during disconnect.
src/​Controls/​src/​Core/​Handlers/​Shell/​Windows/​ShellFlyoutItemView.cs Extracts shared cleanup and clears retained references.

Review effort: Balanced

{
ReleaseContent();
Content = null;
_shell = null;
@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Oct 23, 2026
@MauiBot

MauiBot commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

PR Test Gate

@FullDevStudio — existing-test verification for commit dbd94e42.

Result SKIPPED Platform Android Commit dbd94e42


🧪 Gate analysis — click to expand

⚠ Skipped. No runnable PR tests were selected. The fix has not been verified by this gate.

No interpretable test-group summary was produced. No additional test execution is inferred.

PR head: dbd94e42.

Without-fix baseline: 1e01690f.

@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Oct 23, 2026
github-actions[bot]

This comment was marked as resolved.

@kubaflo
kubaflo changed the base branch from main to net11.0 October 23, 2026 22:25
@kubaflo

This comment has been minimized.

@azure-pipelines

This comment has been minimized.

This branch was previously deployed

1 inactive deployment
copilot-pat-pool — dbd94e42 Deployed Oct 23, 2026 by FullDevStudio via conclusion #2484
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community ✨ Community Contribution

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants