Repository navigation
[leak-fix] Fix SwipeItemView.Command memory leak (Fixes #36343) - #36357
Conversation
Replace the strong ICommand.CanExecuteChanged subscription with a WeakCommandSubscription (disposed on the command-changing path) so a shared/long-lived command no longer roots the SwipeItemView. Adds a Controls.Core.UnitTests regression test (SwipeItemView case in CommandsSubscribedToCanExecuteCollect) that fails without this fix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
AI Review Summary
@github-actions[bot] — new AI review results are available based on this last commit:
27b91ee. To request a fresh review after new comments or commits, comment/review rerun.
🔍 Custom Prompt Analysis — maintainer-requested
● AI-generated — supplementary analysis from a maintainer-supplied custom prompt (GitHub Copilot CLI).
Requested: Is the weak-event/unsubscription approach in this SwipeItemView.Command leak fix correct and consistent with other leak fixes? Any missed unsubscription paths? Be concise.
SwipeItemView.Command leak fix
Verdict: looks correct. The change replaces the strong Command.CanExecuteChanged += OnCommandCanExecuteChanged reference with the existing WeakCommandSubscription helper, matching the pattern already used by CommandElement controls and SearchHandler.
Unsubscription paths
- Command replacement/nulling: covered.
OnCommandChanging()disposes_commandSubscriptionbefore the bindable property changes, so the old command is unsubscribed whether the new value is another command ornull. - View collection while command is retained: covered by the weak subscription design. The command may retain the proxy, but the proxy uses
DependentHandleso it does not keep theSwipeItemViewalive; finalization/event delivery disposes and detaches the proxy. - Repeated Command changes: covered. The previous tracker is disposed and cleared before a new one is assigned, avoiding subscription accumulation.
Consistency with other leak fixes
- Consistent with recent command leak fixes that centralize
CanExecuteChangedhandling throughWeakCommandSubscriptioninstead of direct event subscriptions. SwipeItemViewdoes not implementICommandElement, so using a private tracker mirrors the manualSearchHandlerpattern rather than the sharedCommandElement.CleanupTrackerpath.
Notes
- No missed unsubscription path found in the PR change.
- Minor coverage caveat: the added test verifies
SwipeItemViewcollection with weak and non-weak command implementations, but unlikeICommandElement/SearchHandlercases it cannot directly assert the private subscription/proxy collection. That is acceptable for the production leak being fixed.
🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix
Gate Result: ✅ PASSED
Platform: ANDROID · Base: main · Merge base: dab8abc4
| Test | Without Fix (expect FAIL) | With Fix (expect PASS) |
|---|---|---|
🧪 CommandTests CommandTests |
✅ FAIL — 107s | ✅ PASS — 79s |
🔴 Without fix — 🧪 CommandTests: FAIL ✅ · 107s
Determining projects to restore...
Restored /home/vsts/work/1/s/src/TestUtils/src/TestUtils/TestUtils.csproj (in 4.14 sec).
Restored /home/vsts/work/1/s/src/Graphics/src/Graphics/Graphics.csproj (in 118 ms).
Restored /home/vsts/work/1/s/src/Essentials/src/Essentials.csproj (in 1.81 sec).
Restored /home/vsts/work/1/s/src/Core/src/Core.csproj (in 2.26 sec).
Restored /home/vsts/work/1/s/src/Core/maps/src/Maps.csproj (in 8.55 sec).
Restored /home/vsts/work/1/s/src/Controls/src/Xaml/Controls.Xaml.csproj (in 41 ms).
Restored /home/vsts/work/1/s/src/Controls/src/Core/Controls.Core.csproj (in 29 ms).
Restored /home/vsts/work/1/s/src/Controls/Maps/src/Controls.Maps.csproj (in 32 ms).
Restored /home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj (in 2.52 sec).
1 of 10 projects are up-to-date for restore.
##vso[build.updatebuildnumber]10.0.90-ci+azdo.14587796
Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.90-ci+azdo.14587796
Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.90-ci+azdo.14587796
Core -> /home/vsts/work/1/s/artifacts/bin/Core/Debug/net10.0/Microsoft.Maui.dll
Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.90-ci+azdo.14587796
Maps -> /home/vsts/work/1/s/artifacts/bin/Maps/Debug/net10.0/Microsoft.Maui.Maps.dll
##vso[build.updatebuildnumber]10.0.90-ci+azdo.14587796
Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.Core/Debug/net10.0/Microsoft.Maui.Controls.dll
##vso[build.updatebuildnumber]10.0.90-ci+azdo.14587796
##vso[build.updatebuildnumber]10.0.90-ci+azdo.14587796
Controls.Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.Maps/Debug/net10.0/Microsoft.Maui.Controls.Maps.dll
Controls.Xaml -> /home/vsts/work/1/s/artifacts/bin/Controls.Xaml/Debug/net10.0/Microsoft.Maui.Controls.Xaml.dll
TestUtils -> /home/vsts/work/1/s/artifacts/bin/TestUtils/Debug/netstandard2.0/Microsoft.Maui.TestUtils.dll
Controls.Core.UnitTests -> /home/vsts/work/1/s/artifacts/bin/Controls.Core.UnitTests/Debug/net10.0/Microsoft.Maui.Controls.Core.UnitTests.dll
Test run for /home/vsts/work/1/s/artifacts/bin/Controls.Core.UnitTests/Debug/net10.0/Microsoft.Maui.Controls.Core.UnitTests.dll (.NETCoreApp,Version=v10.0)
VSTest version 18.0.1 (x64)
Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0)
[xUnit.net 00:00:00.15] Discovering: Microsoft.Maui.Controls.Core.UnitTests
[xUnit.net 00:00:01.21] Discovered: Microsoft.Maui.Controls.Core.UnitTests
[xUnit.net 00:00:01.24] Starting: Microsoft.Maui.Controls.Core.UnitTests
Passed CanExecuteReturnsFalseIfParameterIsWrongValueType [5 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.SearchBar), useWeakEventHandler: False) [78 ms]
[xUnit.net 00:00:02.56] CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.SwipeItemView), useWeakEventHandler: False) [FAIL]
[xUnit.net 00:00:02.58] Assert.False() Failure
[xUnit.net 00:00:02.59] Expected: False
[xUnit.net 00:00:02.62] Actual: True
[xUnit.net 00:00:02.62] Stack Trace:
[xUnit.net 00:00:02.65] /_/src/Controls/tests/Core.UnitTests/CommandTests.cs(354,0): at Microsoft.Maui.Controls.Core.UnitTests.CommandTests.CommandsSubscribedToCanExecuteCollect(Type controlType, Boolean useWeakEventHandler)
[xUnit.net 00:00:02.65] --- End of stack trace from previous location ---
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.MenuItem), useWeakEventHandler: True) [95 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.ImageButton), useWeakEventHandler: False) [74 ms]
Failed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.SwipeItemView), useWeakEventHandler: False) [1 s]
Error Message:
Assert.False() Failure
Expected: False
Actual: True
Stack Trace:
at Microsoft.Maui.Controls.Core.UnitTests.CommandTests.CommandsSubscribedToCanExecuteCollect(Type controlType, Boolean useWeakEventHandler) in /_/src/Controls/tests/Core.UnitTests/CommandTests.cs:line 354
--- End of stack trace from previous location ---
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.Button), useWeakEventHandler: False) [66 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.ImageButton), useWeakEventHandler: True) [65 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.RefreshView), useWeakEventHandler: False) [64 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.TextCell), useWeakEventHandler: False) [89 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.SearchHandler), useWeakEventHandler: True) [65 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.SwipeItemView), useWeakEventHandler: True) [66 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.MenuItem), useWeakEventHandler: False) [61 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.RefreshView), useWeakEventHandler: True) [60 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.TextCell), useWeakEventHandler: True) [61 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.Button), useWeakEventHandler: True) [81 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.SearchBar), useWeakEventHandler: True) [119 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.SearchHandler), useWeakEventHandler: False) [75 ms]
Passed ExecuteDoesNotRunIfParameterIsWrongReferenceType [< 1 ms]
Passed ExecuteRunsIfReferenceTypeAndSetToNull [< 1 ms]
Passed CanExecuteIgnoresParameterIfValueTypeAndSetToNull [< 1 ms]
Passed CanExecute(expected: True) [4 ms]
Passed CanExecute(expected: False) [< 1 ms]
Passed GenericExecuteWithCanExecute [1 ms]
Passed GenericThrowsWithNullExecute [< 1 ms]
Passed Execute [< 1 ms]
Passed GenericCanExecute(expected: True) [< 1 ms]
Passed GenericCanExecute(expected: False) [< 1 ms]
Passed ThrowsWithNullConstructor [< 1 ms]
Passed ExecuteDoesNotRunIfParameterIsWrongValueType [< 1 ms]
Passed GenericThrowsWithNullExecuteAndCanExecuteValid [< 1 ms]
Passed ThrowsWithNullExecuteValidCanExecute [< 1 ms]
Passed CanExecuteUsesParameterIfReferenceTypeAndSetToNull [< 1 ms]
Passed ChangeCanExecute [< 1 ms]
Passed GenericExecute [< 1 ms]
Passed ThrowsWithNullCanExecute [< 1 ms]
Passed ThrowsWithNullParameterizedConstructor [< 1 ms]
Passed CanExecuteUsesParameterIfNullableAndSetToNull [< 1 ms]
[xUnit.net 00:00:03.47] Finished: Microsoft.Maui.Controls.Core.UnitTests
Passed ExecuteDoesNotRunIfValueTypeAndSetToNull [< 1 ms]
Passed Constructor [< 1 ms]
Passed ThrowsWithNullParameterizedCanExecute [< 1 ms]
Passed ExecuteRunsIfNullableAndSetToNull [< 1 ms]
Passed ExecuteParameterized [3 ms]
Passed GenericThrowsWithValidExecuteAndCanExecuteNull [< 1 ms]
Passed ExecuteWithCanExecute [< 1 ms]
Passed CanExecuteReturnsFalseIfParameterIsWrongReferenceType [< 1 ms]
Test Run Failed.
Total tests: 45
Passed: 44
Failed: 1
Total time: 3.9548 Seconds
🟢 With fix — 🧪 CommandTests: PASS ✅ · 79s
Determining projects to restore...
All projects are up-to-date for restore.
##vso[build.updatebuildnumber]10.0.90-ci+azdo.14587796
Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.90-ci+azdo.14587796
Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.90-ci+azdo.14587796
Core -> /home/vsts/work/1/s/artifacts/bin/Core/Debug/net10.0/Microsoft.Maui.dll
##vso[build.updatebuildnumber]10.0.90-ci+azdo.14587796
Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
Maps -> /home/vsts/work/1/s/artifacts/bin/Maps/Debug/net10.0/Microsoft.Maui.Maps.dll
##vso[build.updatebuildnumber]10.0.90-ci+azdo.14587796
Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.Core/Debug/net10.0/Microsoft.Maui.Controls.dll
##vso[build.updatebuildnumber]10.0.90-ci+azdo.14587796
##vso[build.updatebuildnumber]10.0.90-ci+azdo.14587796
Controls.Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.Maps/Debug/net10.0/Microsoft.Maui.Controls.Maps.dll
Controls.Xaml -> /home/vsts/work/1/s/artifacts/bin/Controls.Xaml/Debug/net10.0/Microsoft.Maui.Controls.Xaml.dll
TestUtils -> /home/vsts/work/1/s/artifacts/bin/TestUtils/Debug/netstandard2.0/Microsoft.Maui.TestUtils.dll
Controls.Core.UnitTests -> /home/vsts/work/1/s/artifacts/bin/Controls.Core.UnitTests/Debug/net10.0/Microsoft.Maui.Controls.Core.UnitTests.dll
Test run for /home/vsts/work/1/s/artifacts/bin/Controls.Core.UnitTests/Debug/net10.0/Microsoft.Maui.Controls.Core.UnitTests.dll (.NETCoreApp,Version=v10.0)
VSTest version 18.0.1 (x64)
Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0)
[xUnit.net 00:00:00.14] Discovering: Microsoft.Maui.Controls.Core.UnitTests
[xUnit.net 00:00:01.09] Discovered: Microsoft.Maui.Controls.Core.UnitTests
[xUnit.net 00:00:01.11] Starting: Microsoft.Maui.Controls.Core.UnitTests
Passed CanExecuteReturnsFalseIfParameterIsWrongValueType [5 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.SearchBar), useWeakEventHandler: False) [75 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.MenuItem), useWeakEventHandler: True) [59 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.ImageButton), useWeakEventHandler: False) [58 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.SwipeItemView), useWeakEventHandler: False) [60 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.Button), useWeakEventHandler: False) [106 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.ImageButton), useWeakEventHandler: True) [58 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.RefreshView), useWeakEventHandler: False) [60 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.TextCell), useWeakEventHandler: False) [71 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.SearchHandler), useWeakEventHandler: True) [104 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.SwipeItemView), useWeakEventHandler: True) [89 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.MenuItem), useWeakEventHandler: False) [88 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.RefreshView), useWeakEventHandler: True) [91 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.TextCell), useWeakEventHandler: True) [67 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.Button), useWeakEventHandler: True) [64 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.SearchBar), useWeakEventHandler: True) [61 ms]
Passed CommandsSubscribedToCanExecuteCollect(controlType: typeof(Microsoft.Maui.Controls.SearchHandler), useWeakEventHandler: False) [62 ms]
Passed ExecuteDoesNotRunIfParameterIsWrongReferenceType [< 1 ms]
Passed ExecuteRunsIfReferenceTypeAndSetToNull [< 1 ms]
Passed CanExecuteIgnoresParameterIfValueTypeAndSetToNull [< 1 ms]
Passed CanExecute(expected: True) [3 ms]
Passed CanExecute(expected: False) [< 1 ms]
Passed GenericExecuteWithCanExecute [1 ms]
Passed GenericThrowsWithNullExecute [< 1 ms]
Passed Execute [< 1 ms]
Passed GenericCanExecute(expected: True) [< 1 ms]
Passed GenericCanExecute(expected: False) [< 1 ms]
Passed ThrowsWithNullConstructor [< 1 ms]
Passed ExecuteDoesNotRunIfParameterIsWrongValueType [< 1 ms]
Passed GenericThrowsWithNullExecuteAndCanExecuteValid [< 1 ms]
Passed ThrowsWithNullExecuteValidCanExecute [< 1 ms]
Passed CanExecuteUsesParameterIfReferenceTypeAndSetToNull [< 1 ms]
Passed ChangeCanExecute [< 1 ms]
Passed GenericExecute [< 1 ms]
Passed ThrowsWithNullCanExecute [< 1 ms]
Passed ThrowsWithNullParameterizedConstructor [< 1 ms]
Passed CanExecuteUsesParameterIfNullableAndSetToNull [< 1 ms]
Passed ExecuteDoesNotRunIfValueTypeAndSetToNull [< 1 ms]
Passed Constructor [< 1 ms]
Passed ThrowsWithNullParameterizedCanExecute [< 1 ms]
Passed ExecuteRunsIfNullableAndSetToNull [< 1 ms]
[xUnit.net 00:00:02.39] Finished: Microsoft.Maui.Controls.Core.UnitTests
Passed ExecuteParameterized [3 ms]
Passed GenericThrowsWithValidExecuteAndCanExecuteNull [< 1 ms]
Passed ExecuteWithCanExecute [< 1 ms]
Passed CanExecuteReturnsFalseIfParameterIsWrongReferenceType [< 1 ms]
Test Run Successful.
Total tests: 45
Passed: 45
Total time: 2.8877 Seconds
📁 Fix files reverted (1 files)
src/Controls/src/Core/SwipeView/SwipeItemView.cs
📱 UI Tests — SwipeView
Detected UI test categories: SwipeView
✅ Deep UI tests — 75 passed, 0 failed across 1 category on platform-pool agent (replaces in-process counts above).
🧪 UI Test Execution Results (deep, platform pool)
| Category | Tests | Snapshot diffs |
|---|---|---|
SwipeView |
75/75 ✓ | — |
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs) |
📋 Pre-Flight — Context & Validation
Issue: #36343 - [leak-scan] SwipeItemView.Command leaks the control via strong ICommand.CanExecuteChanged (no weak proxy, no unload teardown)
PR: #36357 - [leak-fix] Fix SwipeItemView.Command memory leak (Fixes #36343)
Platforms Affected: all platforms; managed cross-platform SwipeItemView command plumbing. Requested testing platform: android.
Files Changed: 1 implementation, 1 test
Key Findings
SwipeItemView.OnCommandChanged()onmainsubscribes toICommand.CanExecuteChangedwith an instance-method delegate. A long-lived strong-event command can retain the view and its visual subtree if the view is discarded without clearingCommand.- PR #36357 replaces the raw event subscription with
WeakCommandSubscription, matching established MAUI command-bearing controls and preserving existingIsEnabledrecalculation semantics. - Regression coverage adds
SwipeItemViewtoCommandsSubscribedToCanExecuteCollectfor both weak-event MAUICommandand strong-event customICommand. - GitHub CLI authentication is unavailable in this environment, so authenticated PR surfaces and required-check exactness could not be queried with
gh; public API/curl evidence was used where possible. - Prior MauiBot review requested changes because it selected an alternate candidate over the PR fix; no prior ❌ Error findings were found in public review, inline comment, or issue-comment surfaces.
Code Review Summary
Verdict: NEEDS_DISCUSSION
Confidence: low
Errors: 0 | Warnings: 0 | Suggestions: 0
Key code review findings:
- ℹ No code-level errors or warnings found; the implementation is localized and consistent with existing MAUI
WeakCommandSubscriptionusage. - ℹ Confidence is capped because
gh pr checks --requiredcannot run without authentication.
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #36357 | Replace SwipeItemView raw CanExecuteChanged subscription with WeakCommandSubscription; add SwipeItemView to existing command GC theory. |
✅ PASSED (Gate) | src/Controls/src/Core/SwipeView/SwipeItemView.cs, src/Controls/tests/Core.UnitTests/CommandTests.cs |
Original PR; gate result supplied by caller. |
🔬 Code Review — Deep Analysis
Code Review — PR #36357
Independent Assessment
What this changes: SwipeItemView replaces its strong Command.CanExecuteChanged subscription with WeakCommandSubscription, disposing it when the Command property changes. Tests add SwipeItemView to existing command GC coverage.
Inferred motivation: Prevent long-lived ICommand instances with strong events from retaining discarded SwipeItemView controls.
Reconciliation with PR Narrative
Author claims: Fixes SwipeItemView.Command memory leak via existing weak command subscription pattern.
Agreement/disagreement: Agrees. The implementation matches issue #36343's root cause and preserves existing IsEnabled behavior.
Prior Review Reconciliation
| Prior ❌ Error Finding | Source | Status | Evidence |
|---|---|---|---|
| None found | Public GitHub API: reviews, inline comments, issue comments | n/a | MauiBot had CHANGES_REQUESTED, but its body stated "No prior ❌ Error findings found" and listed only suggestions/alternative preference. Inline comments count was 0. |
Blast Radius Assessment
- Runs for all instances: Yes, all
SwipeItemView.Commandassignments use the weak subscription. - Startup impact: No.
- Static/shared state: No; new state is per-instance.
- Platform impact: Managed cross-platform control code only.
CI Status
- Required-check result:
gh pr checks --requiredunavailable becauseghis unauthenticated. - Public API evidence: head SHA
27b91eehas completed successful check-runs, includingmaui-prper public review/status evidence. - Classification: Required-check exactness undetermined due auth limitation.
- Action taken: Confidence capped low; no GitHub comments posted.
Findings
No ❌ Error,
Failure-Mode Probing
- Command set to
null: existing subscription is disposed,IsEnabledbecomestrue. - Command replaced repeatedly:
OnCommandChangingdisposes the old subscription before the new one is created. - View discarded while command lives:
WeakCommandSubscriptionavoids strongly retaining the view. - Command raises after view collection: proxy resolves missing target and disposes itself.
- MAUI
Commandwith weak events: behavior remains compatible; test covers both weak and strong command cases.
Verdict: NEEDS_DISCUSSION
Confidence: low, solely because authenticated required-check/prior-review verification was unavailable.
Summary: Code-only review found the fix sound, localized, and consistent with MAUI command subscription patterns. Public check-runs are green, but required-check status could not be verified exactly without gh auth, so the skill rules prevent LGTM.
🛠️ Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix | Command-owned weak fan-out broker using ConditionalWeakTable<ICommand, Entry> and weak SwipeItemView references. |
✅ PASS | 2 files | More complex than PR; not better despite passing tests. |
| 2 | try-fix | Handler-scoped strong subscription; unsubscribe on handler disconnect and subscribe on handler connect. | ❌ FAIL | 2 files | Leak test passes, but pre-handler CanExecuteChanged no longer updates IsEnabled; also initially hit PublicAPI analyzer with protected overrides. |
| 3 | try-fix | Hybrid ICommandElement adoption for Command only; keep local CommandParameter callback to preserve commandless disabled-state behavior. |
✅ PASS | 2 files | Best alternative: shared MAUI command infrastructure, preserves edge cases, avoids new primitives. |
| PR | PR #36357 | Per-view WeakCommandSubscription in SwipeItemView; add SwipeItemView to existing command GC theory. |
✅ PASSED (Gate) | 2 files | Original PR; gate supplied by caller. |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
maui-expert-reviewer / gpt-5.5 |
1 | Yes | Command-owned weak fan-out broker; tested as try-fix-1. |
maui-expert-reviewer / gpt-5.5 |
2 | Yes | Handler-scoped strong subscription; tested as try-fix-2 and rejected after semantic regression. |
maui-expert-reviewer / gpt-5.5 |
3 | Yes | Hybrid ICommandElement adoption preserving local CommandParameter semantics; tested as try-fix-3 and selected. |
Exhausted: No — stopped because candidate #3 passes all targeted tests and is demonstrably better than the PR's local-field fix for maintainability/consistency.
Selected Fix: Candidate #3 — Reuses shared MAUI CommandElement/ICommandElement infrastructure while preserving the SwipeItemView-specific edge case that full adoption broke.
📝 Recommended PR Title & Description
Assessment: ✏️ Recommend updating — the current description accurately explains the raw PR fix, but the winning candidate is try-fix-3, which uses ICommandElement/CommandElement rather than a private WeakCommandSubscription field.
Recommended title
SwipeItemView: Fix Command memory leak via CommandElement weak subscription
Recommended description
Fixes #36343
Refs: dotnet/maui#36343
Target branch: `main`
## The leak
`SwipeItemView.OnCommandChanged()` subscribed to the assigned command with a strong delegate:
```csharp
Command.CanExecuteChanged += OnCommandCanExecuteChanged;
Because OnCommandCanExecuteChanged is an instance method, the delegate's Target is the SwipeItemView. Any command that raises CanExecuteChanged through a strong event, such as a custom ICommand or CommunityToolkit.Mvvm's RelayCommand, can keep the SwipeItemView and its visual subtree alive for as long as the command lives. The matching unsubscribe only ran in the property-changing path, so a view that is discarded while a longer-lived command outlives it is never unsubscribed and leaks.
Microsoft.Maui.Controls.Command raises CanExecuteChanged through a WeakEventManager and does not leak; the regression test covers that non-leaking case too.
The fix
Make SwipeItemView implement the internal ICommandElement pattern and route CommandProperty changes through CommandElement.OnCommandChanging / CommandElement.OnCommandChanged. This reuses MAUI's existing WeakCommandSubscription infrastructure through the shared CommandElement path already used by command-bearing controls.
The CommandParameterProperty callback intentionally stays local instead of using CommandElement.OnCommandParameterChanged, so changing CommandParameter while Command is null preserves the existing disabled state rather than re-enabling the item.
- No public API change:
ICommandElementandWeakCommandSubscriptionare internal implementation details. - Managed, cross-platform product change only:
src/Controls/src/Core/SwipeView/SwipeItemView.cs. - Existing
IsEnabledbehavior is preserved, including handlerlessCanExecuteChangedupdates.
Regression tests
Added SwipeItemView to the existing CommandsSubscribedToCanExecuteCollect theory in src/Controls/tests/Core.UnitTests/CommandTests.cs, alongside the other command-bearing controls. The test assigns both weak-event and strong-event commands, drops the view in a nested scope, and asserts the view is collected. Because the winning implementation uses ICommandElement, the existing test also verifies the CleanupTracker and proxy collectibility for SwipeItemView.
Additional semantic coverage verifies:
SwipeItemViewCanExecuteChangedUpdatesWithoutHandler:CanExecuteChangedstill updatesIsEnabledbefore handler attachment.SwipeItemViewCommandParameterChangeWithoutCommandPreservesIsEnabled: changingCommandParameterwithout a command does not re-enable a disabledSwipeItemView.
| State | Result |
|---|---|
Without fix (main) |
Fails for SwipeItemView, useWeakEventHandler: False because the view remains alive |
| With winning fix | Passes the leak regression and focused CommandTests / SwipeViewTests coverage |
Scope
- Product:
src/Controls/src/Core/SwipeView/SwipeItemView.cs - Tests:
src/Controls/tests/Core.UnitTests/CommandTests.cs - Platform tested: Android
- No test was muted, skipped, weakened, or deleted; no
[ActiveIssue]/Skip=; no public API change.
</details>
---
<details>
<summary><strong>🏁 Report — Final Recommendation</strong></summary>
<br/>
## Comparative Report — PR #36357
### Candidates compared
| Rank | Candidate | Regression result | Assessment |
|---:|---|---|---|
| 1 | `try-fix-3` | ✅ PASS | Best overall. Reuses MAUI's shared `ICommandElement` / `CommandElement` weak command-subscription infrastructure, preserves handlerless `CanExecuteChanged` updates, and preserves `SwipeItemView`'s commandless `CommandParameter` behavior. Additional semantic probes passed. |
| 2 | `pr` | ✅ PASSED (Gate) | Correct, localized fix. Uses `WeakCommandSubscription` directly in `SwipeItemView` and the expert reviewer found no required changes. Ranked below `try-fix-3` because it keeps a bespoke command-subscription path instead of the shared command infrastructure. |
| 2 | `pr-plus-reviewer` | ✅ PASSED (same as `pr`) | The expert reviewer produced no actionable feedback, so this is identical to `pr`. |
| 4 | `try-fix-1` | ✅ PASS | Functionally passes tests, but introduces a new command-owned broker with `ConditionalWeakTable`, weak-reference fan-out, locking, pruning, and reentrancy surface. It is more complex than both the PR and shared `CommandElement` approach without a compensating benefit. |
| 5 | `try-fix-2` | ❌ FAIL | Must rank below passing candidates. It avoids the leak by subscribing only while a handler is connected, but regresses managed semantics: `CanExecuteChanged` no longer updates `SwipeItemView.IsEnabled` before handler attachment. |
### Winner
**Winner:** `try-fix-3`
`try-fix-3` wins because it fixes the same leak using the existing shared command infrastructure already used by MAUI command-bearing controls, without introducing a new broker and without changing the edge-case behaviors discovered during try-fix. The raw PR is acceptable and expert-reviewed as LGTM, but `try-fix-3` is the stronger maintainability choice.
### Ranking rule compliance
`try-fix-2` failed targeted regression/semantic testing and is ranked below all passing candidates. The remaining passing candidates were ranked by maintainability, consistency with existing MAUI command infrastructure, and semantic coverage.
### Recommended implementation
Use the `try-fix-3` implementation:
- Add `using Microsoft.Maui.Controls.Internals;`
- Make `SwipeItemView` implement `ICommandElement`.
- Route `CommandProperty` through `CommandElement.OnCommandChanging` / `OnCommandChanged`.
- Implement `ICommandElement.CanExecuteChanged` with `CommandElement.GetCanExecute(this, CommandProperty)`.
- Keep a local guarded `OnCommandParameterChanged()` so `CommandParameter` changes do not alter `IsEnabled` when `Command` is null.
- Keep the added leak regression and add the two semantic tests from `try-fix-3`.
</details>
</details>
<!-- SESSION:27b91ee END -->
---
<details>
<summary><strong>🧭 Next Steps</strong> — alternative fix proposed (<code>try-fix-3</code>)</summary>
<br/>
**Automated review — alternative fix proposed**
The expert-reviewer evaluation compared the PR fix against automatically generated candidates and selected <code>try-fix-3</code> as the strongest fix.
**Why:** try-fix-3 won because it fixes the same SwipeItemView.Command strong-event leak using MAUI shared CommandElement/ICommandElement weak-subscription infrastructure while preserving handlerless CanExecuteChanged updates and commandless CommandParameter semantics. The raw PR and pr-plus-reviewer are valid and expert-reviewed as LGTM, but try-fix-3 is the stronger maintainability choice; try-fix-2 failed regression/semantic testing.
Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.
<details><summary>Candidate diff (<code>try-fix-3</code>)</summary>
````diff
diff --git a/src/Controls/src/Core/SwipeView/SwipeItemView.cs b/src/Controls/src/Core/SwipeView/SwipeItemView.cs
index 346ada5523..a2d8717014 100644
--- a/src/Controls/src/Core/SwipeView/SwipeItemView.cs
+++ b/src/Controls/src/Core/SwipeView/SwipeItemView.cs
@@ -2,6 +2,7 @@
using System;
using System.ComponentModel;
using System.Windows.Input;
+using Microsoft.Maui.Controls.Internals;
namespace Microsoft.Maui.Controls
{
@@ -9,12 +10,12 @@ namespace Microsoft.Maui.Controls
/// Represents a swipe item that displays custom content in a <see cref="SwipeView"/>.
/// </summary>
[ContentProperty(nameof(Content))]
- public partial class SwipeItemView : ContentView, Controls.ISwipeItem, Maui.ISwipeItemView
+ public partial class SwipeItemView : ContentView, Controls.ISwipeItem, Maui.ISwipeItemView, ICommandElement
{
/// <summary>Bindable property for <see cref="Command"/>.</summary>
public static readonly BindableProperty CommandProperty = BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(SwipeItemView), null,
- propertyChanging: (bo, o, n) => ((SwipeItemView)bo).OnCommandChanging(),
- propertyChanged: (bo, o, n) => ((SwipeItemView)bo).OnCommandChanged());
+ propertyChanging: CommandElement.OnCommandChanging,
+ propertyChanged: CommandElement.OnCommandChanged);
/// <summary>Bindable property for <see cref="CommandParameter"/>.</summary>
public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create(nameof(CommandParameter), typeof(object), typeof(SwipeItemView), null,
@@ -52,35 +53,17 @@ namespace Microsoft.Maui.Controls
Invoked?.Invoke(this, EventArgs.Empty);
}
- void OnCommandChanged()
+ void ICommandElement.CanExecuteChanged(object sender, EventArgs e)
{
- IsEnabled = Command?.CanExecute(CommandParameter) ?? true;
-
- if (Command == null)
- return;
-
- Command.CanExecuteChanged += OnCommandCanExecuteChanged;
- }
-
- void OnCommandChanging()
- {
- if (Command == null)
- return;
-
- Command.CanExecuteChanged -= OnCommandCanExecuteChanged;
+ IsEnabled = CommandElement.GetCanExecute(this, CommandProperty);
}
void OnCommandParameterChanged()
{
- if (Command == null)
- return;
-
- IsEnabled = Command.CanExecute(CommandParameter);
+ if (Command != null)
+ ((ICommandElement)this).CanExecuteChanged(this, EventArgs.Empty);
}
- void OnCommandCanExecuteChanged(object sender, EventArgs eventArgs)
- {
- IsEnabled = Command.CanExecute(CommandParameter);
- }
+ WeakCommandSubscription ICommandElement.CleanupTracker { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Controls/tests/Core.UnitTests/CommandTests.cs b/src/Controls/tests/Core.UnitTests/CommandTests.cs
index 3f4237785c..0bb4d28c52 100644
--- a/src/Controls/tests/Core.UnitTests/CommandTests.cs
+++ b/src/Controls/tests/Core.UnitTests/CommandTests.cs
@@ -271,6 +271,8 @@ namespace Microsoft.Maui.Controls.Core.UnitTests
[InlineData(typeof(SearchBar), false)]
[InlineData(typeof(SearchHandler), true)]
[InlineData(typeof(SearchHandler), false)]
+ [InlineData(typeof(SwipeItemView), true)]
+ [InlineData(typeof(SwipeItemView), false)]
public async Task CommandsSubscribedToCanExecuteCollect(Type controlType, bool useWeakEventHandler)
{
// Create a view model with a Command
@@ -310,6 +312,9 @@ namespace Microsoft.Maui.Controls.Core.UnitTests
sh.Command = command;
sh.ClearPlaceholderCommand = command;
break;
+ case SwipeItemView sv:
+ sv.Command = command;
+ break;
}
// Create a weak reference to the button
@@ -352,13 +357,56 @@ namespace Microsoft.Maui.Controls.Core.UnitTests
class CommandWithoutWeakEventHandler : ICommand
{
+ readonly Func<bool> _canExecute;
+
+ public CommandWithoutWeakEventHandler()
+ {
+ _canExecute = () => true;
+ }
+
+ public CommandWithoutWeakEventHandler(Func<bool> canExecute)
+ {
+ _canExecute = canExecute;
+ }
+
public event EventHandler CanExecuteChanged;
- public bool CanExecute(object parameter) => true;
+ public bool CanExecute(object parameter) => _canExecute();
public void Execute(object parameter) { }
public void ChangeCanExecute() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
+
+ [Fact]
+ public void SwipeItemViewCanExecuteChangedUpdatesWithoutHandler()
+ {
+ bool canExecute = false;
+ var command = new CommandWithoutWeakEventHandler(() => canExecute);
+ var swipeItemView = new SwipeItemView
+ {
+ Command = command
+ };
+
+ Assert.False(swipeItemView.IsEnabled);
+
+ canExecute = true;
+ command.ChangeCanExecute();
+
+ Assert.True(swipeItemView.IsEnabled);
+ }
+
+ [Fact]
+ public void SwipeItemViewCommandParameterChangeWithoutCommandPreservesIsEnabled()
+ {
+ var swipeItemView = new SwipeItemView
+ {
+ IsEnabled = false
+ };
+
+ swipeItemView.CommandParameter = new object();
+
+ Assert.False(swipeItemView.IsEnabled);
+ }
}
}
\ No newline at end of file
> [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! > [!IMPORTANT] > 🤖 **AI-generated PR.** This pull request was produced automatically by the **Memory Leak Fixer** agentic workflow on dotnet/maui. The regression test below was empirically proven to **fail on `main` without the fix** and **pass with it** during this run. Please review carefully before merging. Fixes #36343 Refs: #36343 Target branch: `main` Attempt: 1/3 ## The leak `SwipeItemView.OnCommandChanged()` subscribed to the assigned command with a **strong** delegate: ```csharp Command.CanExecuteChanged += OnCommandCanExecuteChanged; ``` Because `OnCommandCanExecuteChanged` is an instance method, the delegate's `Target` is the `SwipeItemView`. Any command that raises `CanExecuteChanged` through a **strong** event (e.g. a custom `ICommand`, or `CommunityToolkit.Mvvm`'s `RelayCommand`) therefore keeps the `SwipeItemView` — and its entire visual subtree — alive for as long as the command lives. The matching `-=` only ran in `OnCommandChanging()` (the property-*changing* path), so a view that is simply unloaded/discarded while a longer-lived command outlives it is never unsubscribed, and it leaks. > `Microsoft.Maui.Controls.Command` raises `CanExecuteChanged` through a `WeakEventManager` and does **not** leak — the regression test covers that non-leaking case too, to prove the fix preserves it. ## The fix Replace the raw subscription with the `WeakCommandSubscription` helper already used by `Button`, `ImageButton`, `RefreshView`, `MenuItem`, `SearchBar`, `TextCell`, etc. It links the command to the view through a `DependentHandle`, so the command references the view only **weakly** and the view can be collected once nothing else roots it: ```csharp WeakCommandSubscription _commandSubscription; void OnCommandChanged() { IsEnabled = Command?.CanExecute(CommandParameter) ?? true; if (Command == null) return; _commandSubscription = new WeakCommandSubscription(this, Command, OnCommandCanExecuteChanged); } void OnCommandChanging() { _commandSubscription?.Dispose(); _commandSubscription = null; } ``` - No public API change — the field is private and `WeakCommandSubscription` is internal. - All existing `IsEnabled` behavior is preserved; only the subscription lifetime changes. - Idiomatic: mirrors exactly how the sibling command-bearing controls already harden this same subscription. ## Regression test Added the `SwipeItemView` case to the existing `CommandsSubscribedToCanExecuteCollect` theory in `src/Controls/tests/Core.UnitTests/CommandTests.cs`, right alongside every other command-bearing control. It assigns a strong-event `ICommand`, drops the view in a nested scope, and asserts the view is collected (`WaitForCollect` returns `false`). | State | `SwipeItemView, useWeakEventHandler: False` | Full theory | |-------|---------------------------------------------|-------------| | Without fix (`main`) | ❌ FAIL — `Assert.False` (view still alive) | 15 passed / **1 failed** | | With fix | ✅ PASS | **16 passed** / 0 failed | No regressions with the fix applied: `CommandTests` (45/45) and `SwipeViewTests` (24/24) pass. Validated by building MAUI from source on the net library TFM. ## Scope - Managed, cross-platform product change only: `src/Controls/src/Core/SwipeView/SwipeItemView.cs`. - Test: `src/Controls/tests/Core.UnitTests/CommandTests.cs`. - No test was muted, skipped, weakened, or deleted; no `[ActiveIssue]`/`Skip=`; no public API change. --- _Generated by the Memory Leak Fixer agentic workflow (dotnet/maui). This description is AI-generated — please review before merging._ > Generated by [Memory Leak Fixer](https://github.com/dotnet/maui/actions/runs/28685456694) · 2.6K AIC · ⌖ 212.1 AIC · ⊞ 60.5K · [◷](https://github.com/search?q=repo%3Adotnet%2Fmaui+%22gh-aw-workflow-id%3A+leak-fixer%22&type=pullrequests) <!-- gh-aw-agentic-workflow: Memory Leak Fixer, engine: copilot, version: 1.0.60, model: claude-opus-4.8, id: 28685456694, workflow_id: leak-fixer, run: https://github.com/dotnet/maui/actions/runs/28685456694 --> <!-- gh-aw-workflow-id: leak-fixer --> <!-- gh-aw-workflow-call-id: dotnet/maui/leak-fixer --> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
> [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! > [!IMPORTANT] > 🤖 **AI-generated PR.** This pull request was produced automatically by the **Memory Leak Fixer** agentic workflow on dotnet/maui. The regression test below was empirically proven to **fail on `main` without the fix** and **pass with it** during this run. Please review carefully before merging. Fixes #36343 Refs: #36343 Target branch: `main` Attempt: 1/3 ## The leak `SwipeItemView.OnCommandChanged()` subscribed to the assigned command with a **strong** delegate: ```csharp Command.CanExecuteChanged += OnCommandCanExecuteChanged; ``` Because `OnCommandCanExecuteChanged` is an instance method, the delegate's `Target` is the `SwipeItemView`. Any command that raises `CanExecuteChanged` through a **strong** event (e.g. a custom `ICommand`, or `CommunityToolkit.Mvvm`'s `RelayCommand`) therefore keeps the `SwipeItemView` — and its entire visual subtree — alive for as long as the command lives. The matching `-=` only ran in `OnCommandChanging()` (the property-*changing* path), so a view that is simply unloaded/discarded while a longer-lived command outlives it is never unsubscribed, and it leaks. > `Microsoft.Maui.Controls.Command` raises `CanExecuteChanged` through a `WeakEventManager` and does **not** leak — the regression test covers that non-leaking case too, to prove the fix preserves it. ## The fix Replace the raw subscription with the `WeakCommandSubscription` helper already used by `Button`, `ImageButton`, `RefreshView`, `MenuItem`, `SearchBar`, `TextCell`, etc. It links the command to the view through a `DependentHandle`, so the command references the view only **weakly** and the view can be collected once nothing else roots it: ```csharp WeakCommandSubscription _commandSubscription; void OnCommandChanged() { IsEnabled = Command?.CanExecute(CommandParameter) ?? true; if (Command == null) return; _commandSubscription = new WeakCommandSubscription(this, Command, OnCommandCanExecuteChanged); } void OnCommandChanging() { _commandSubscription?.Dispose(); _commandSubscription = null; } ``` - No public API change — the field is private and `WeakCommandSubscription` is internal. - All existing `IsEnabled` behavior is preserved; only the subscription lifetime changes. - Idiomatic: mirrors exactly how the sibling command-bearing controls already harden this same subscription. ## Regression test Added the `SwipeItemView` case to the existing `CommandsSubscribedToCanExecuteCollect` theory in `src/Controls/tests/Core.UnitTests/CommandTests.cs`, right alongside every other command-bearing control. It assigns a strong-event `ICommand`, drops the view in a nested scope, and asserts the view is collected (`WaitForCollect` returns `false`). | State | `SwipeItemView, useWeakEventHandler: False` | Full theory | |-------|---------------------------------------------|-------------| | Without fix (`main`) | ❌ FAIL — `Assert.False` (view still alive) | 15 passed / **1 failed** | | With fix | ✅ PASS | **16 passed** / 0 failed | No regressions with the fix applied: `CommandTests` (45/45) and `SwipeViewTests` (24/24) pass. Validated by building MAUI from source on the net library TFM. ## Scope - Managed, cross-platform product change only: `src/Controls/src/Core/SwipeView/SwipeItemView.cs`. - Test: `src/Controls/tests/Core.UnitTests/CommandTests.cs`. - No test was muted, skipped, weakened, or deleted; no `[ActiveIssue]`/`Skip=`; no public API change. --- _Generated by the Memory Leak Fixer agentic workflow (dotnet/maui). This description is AI-generated — please review before merging._ > Generated by [Memory Leak Fixer](https://github.com/dotnet/maui/actions/runs/28685456694) · 2.6K AIC · ⌖ 212.1 AIC · ⊞ 60.5K · [◷](https://github.com/search?q=repo%3Adotnet%2Fmaui+%22gh-aw-workflow-id%3A+leak-fixer%22&type=pullrequests) <!-- gh-aw-agentic-workflow: Memory Leak Fixer, engine: copilot, version: 1.0.60, model: claude-opus-4.8, id: 28685456694, workflow_id: leak-fixer, run: https://github.com/dotnet/maui/actions/runs/28685456694 --> <!-- gh-aw-workflow-id: leak-fixer --> <!-- gh-aw-workflow-call-id: dotnet/maui/leak-fixer --> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
> [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! > [!IMPORTANT] > 🤖 **AI-generated PR.** This pull request was produced automatically by the **Memory Leak Fixer** agentic workflow on dotnet/maui. The regression test below was empirically proven to **fail on `main` without the fix** and **pass with it** during this run. Please review carefully before merging. Fixes #36343 Refs: #36343 Target branch: `main` Attempt: 1/3 ## The leak `SwipeItemView.OnCommandChanged()` subscribed to the assigned command with a **strong** delegate: ```csharp Command.CanExecuteChanged += OnCommandCanExecuteChanged; ``` Because `OnCommandCanExecuteChanged` is an instance method, the delegate's `Target` is the `SwipeItemView`. Any command that raises `CanExecuteChanged` through a **strong** event (e.g. a custom `ICommand`, or `CommunityToolkit.Mvvm`'s `RelayCommand`) therefore keeps the `SwipeItemView` — and its entire visual subtree — alive for as long as the command lives. The matching `-=` only ran in `OnCommandChanging()` (the property-*changing* path), so a view that is simply unloaded/discarded while a longer-lived command outlives it is never unsubscribed, and it leaks. > `Microsoft.Maui.Controls.Command` raises `CanExecuteChanged` through a `WeakEventManager` and does **not** leak — the regression test covers that non-leaking case too, to prove the fix preserves it. ## The fix Replace the raw subscription with the `WeakCommandSubscription` helper already used by `Button`, `ImageButton`, `RefreshView`, `MenuItem`, `SearchBar`, `TextCell`, etc. It links the command to the view through a `DependentHandle`, so the command references the view only **weakly** and the view can be collected once nothing else roots it: ```csharp WeakCommandSubscription _commandSubscription; void OnCommandChanged() { IsEnabled = Command?.CanExecute(CommandParameter) ?? true; if (Command == null) return; _commandSubscription = new WeakCommandSubscription(this, Command, OnCommandCanExecuteChanged); } void OnCommandChanging() { _commandSubscription?.Dispose(); _commandSubscription = null; } ``` - No public API change — the field is private and `WeakCommandSubscription` is internal. - All existing `IsEnabled` behavior is preserved; only the subscription lifetime changes. - Idiomatic: mirrors exactly how the sibling command-bearing controls already harden this same subscription. ## Regression test Added the `SwipeItemView` case to the existing `CommandsSubscribedToCanExecuteCollect` theory in `src/Controls/tests/Core.UnitTests/CommandTests.cs`, right alongside every other command-bearing control. It assigns a strong-event `ICommand`, drops the view in a nested scope, and asserts the view is collected (`WaitForCollect` returns `false`). | State | `SwipeItemView, useWeakEventHandler: False` | Full theory | |-------|---------------------------------------------|-------------| | Without fix (`main`) | ❌ FAIL — `Assert.False` (view still alive) | 15 passed / **1 failed** | | With fix | ✅ PASS | **16 passed** / 0 failed | No regressions with the fix applied: `CommandTests` (45/45) and `SwipeViewTests` (24/24) pass. Validated by building MAUI from source on the net library TFM. ## Scope - Managed, cross-platform product change only: `src/Controls/src/Core/SwipeView/SwipeItemView.cs`. - Test: `src/Controls/tests/Core.UnitTests/CommandTests.cs`. - No test was muted, skipped, weakened, or deleted; no `[ActiveIssue]`/`Skip=`; no public API change. --- _Generated by the Memory Leak Fixer agentic workflow (dotnet/maui). This description is AI-generated — please review before merging._ > Generated by [Memory Leak Fixer](https://github.com/dotnet/maui/actions/runs/28685456694) · 2.6K AIC · ⌖ 212.1 AIC · ⊞ 60.5K · [◷](https://github.com/search?q=repo%3Adotnet%2Fmaui+%22gh-aw-workflow-id%3A+leak-fixer%22&type=pullrequests) <!-- gh-aw-agentic-workflow: Memory Leak Fixer, engine: copilot, version: 1.0.60, model: claude-opus-4.8, id: 28685456694, workflow_id: leak-fixer, run: https://github.com/dotnet/maui/actions/runs/28685456694 --> <!-- gh-aw-workflow-id: leak-fixer --> <!-- gh-aw-workflow-call-id: dotnet/maui/leak-fixer --> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
> [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! > [!IMPORTANT] > 🤖 **AI-generated PR.** This pull request was produced automatically by the **Memory Leak Fixer** agentic workflow on dotnet/maui. The regression test below was empirically proven to **fail on `main` without the fix** and **pass with it** during this run. Please review carefully before merging. Fixes #36343 Refs: #36343 Target branch: `main` Attempt: 1/3 ## The leak `SwipeItemView.OnCommandChanged()` subscribed to the assigned command with a **strong** delegate: ```csharp Command.CanExecuteChanged += OnCommandCanExecuteChanged; ``` Because `OnCommandCanExecuteChanged` is an instance method, the delegate's `Target` is the `SwipeItemView`. Any command that raises `CanExecuteChanged` through a **strong** event (e.g. a custom `ICommand`, or `CommunityToolkit.Mvvm`'s `RelayCommand`) therefore keeps the `SwipeItemView` — and its entire visual subtree — alive for as long as the command lives. The matching `-=` only ran in `OnCommandChanging()` (the property-*changing* path), so a view that is simply unloaded/discarded while a longer-lived command outlives it is never unsubscribed, and it leaks. > `Microsoft.Maui.Controls.Command` raises `CanExecuteChanged` through a `WeakEventManager` and does **not** leak — the regression test covers that non-leaking case too, to prove the fix preserves it. ## The fix Replace the raw subscription with the `WeakCommandSubscription` helper already used by `Button`, `ImageButton`, `RefreshView`, `MenuItem`, `SearchBar`, `TextCell`, etc. It links the command to the view through a `DependentHandle`, so the command references the view only **weakly** and the view can be collected once nothing else roots it: ```csharp WeakCommandSubscription _commandSubscription; void OnCommandChanged() { IsEnabled = Command?.CanExecute(CommandParameter) ?? true; if (Command == null) return; _commandSubscription = new WeakCommandSubscription(this, Command, OnCommandCanExecuteChanged); } void OnCommandChanging() { _commandSubscription?.Dispose(); _commandSubscription = null; } ``` - No public API change — the field is private and `WeakCommandSubscription` is internal. - All existing `IsEnabled` behavior is preserved; only the subscription lifetime changes. - Idiomatic: mirrors exactly how the sibling command-bearing controls already harden this same subscription. ## Regression test Added the `SwipeItemView` case to the existing `CommandsSubscribedToCanExecuteCollect` theory in `src/Controls/tests/Core.UnitTests/CommandTests.cs`, right alongside every other command-bearing control. It assigns a strong-event `ICommand`, drops the view in a nested scope, and asserts the view is collected (`WaitForCollect` returns `false`). | State | `SwipeItemView, useWeakEventHandler: False` | Full theory | |-------|---------------------------------------------|-------------| | Without fix (`main`) | ❌ FAIL — `Assert.False` (view still alive) | 15 passed / **1 failed** | | With fix | ✅ PASS | **16 passed** / 0 failed | No regressions with the fix applied: `CommandTests` (45/45) and `SwipeViewTests` (24/24) pass. Validated by building MAUI from source on the net library TFM. ## Scope - Managed, cross-platform product change only: `src/Controls/src/Core/SwipeView/SwipeItemView.cs`. - Test: `src/Controls/tests/Core.UnitTests/CommandTests.cs`. - No test was muted, skipped, weakened, or deleted; no `[ActiveIssue]`/`Skip=`; no public API change. --- _Generated by the Memory Leak Fixer agentic workflow (dotnet/maui). This description is AI-generated — please review before merging._ > Generated by [Memory Leak Fixer](https://github.com/dotnet/maui/actions/runs/28685456694) · 2.6K AIC · ⌖ 212.1 AIC · ⊞ 60.5K · [◷](https://github.com/search?q=repo%3Adotnet%2Fmaui+%22gh-aw-workflow-id%3A+leak-fixer%22&type=pullrequests) <!-- gh-aw-agentic-workflow: Memory Leak Fixer, engine: copilot, version: 1.0.60, model: claude-opus-4.8, id: 28685456694, workflow_id: leak-fixer, run: https://github.com/dotnet/maui/actions/runs/28685456694 --> <!-- gh-aw-workflow-id: leak-fixer --> <!-- gh-aw-workflow-call-id: dotnet/maui/leak-fixer --> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
> [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! > [!IMPORTANT] > 🤖 **AI-generated PR.** This pull request was produced automatically by the **Memory Leak Fixer** agentic workflow on dotnet/maui. The regression test below was empirically proven to **fail on `main` without the fix** and **pass with it** during this run. Please review carefully before merging. Fixes #36343 Refs: #36343 Target branch: `main` Attempt: 1/3 ## The leak `SwipeItemView.OnCommandChanged()` subscribed to the assigned command with a **strong** delegate: ```csharp Command.CanExecuteChanged += OnCommandCanExecuteChanged; ``` Because `OnCommandCanExecuteChanged` is an instance method, the delegate's `Target` is the `SwipeItemView`. Any command that raises `CanExecuteChanged` through a **strong** event (e.g. a custom `ICommand`, or `CommunityToolkit.Mvvm`'s `RelayCommand`) therefore keeps the `SwipeItemView` — and its entire visual subtree — alive for as long as the command lives. The matching `-=` only ran in `OnCommandChanging()` (the property-*changing* path), so a view that is simply unloaded/discarded while a longer-lived command outlives it is never unsubscribed, and it leaks. > `Microsoft.Maui.Controls.Command` raises `CanExecuteChanged` through a `WeakEventManager` and does **not** leak — the regression test covers that non-leaking case too, to prove the fix preserves it. ## The fix Replace the raw subscription with the `WeakCommandSubscription` helper already used by `Button`, `ImageButton`, `RefreshView`, `MenuItem`, `SearchBar`, `TextCell`, etc. It links the command to the view through a `DependentHandle`, so the command references the view only **weakly** and the view can be collected once nothing else roots it: ```csharp WeakCommandSubscription _commandSubscription; void OnCommandChanged() { IsEnabled = Command?.CanExecute(CommandParameter) ?? true; if (Command == null) return; _commandSubscription = new WeakCommandSubscription(this, Command, OnCommandCanExecuteChanged); } void OnCommandChanging() { _commandSubscription?.Dispose(); _commandSubscription = null; } ``` - No public API change — the field is private and `WeakCommandSubscription` is internal. - All existing `IsEnabled` behavior is preserved; only the subscription lifetime changes. - Idiomatic: mirrors exactly how the sibling command-bearing controls already harden this same subscription. ## Regression test Added the `SwipeItemView` case to the existing `CommandsSubscribedToCanExecuteCollect` theory in `src/Controls/tests/Core.UnitTests/CommandTests.cs`, right alongside every other command-bearing control. It assigns a strong-event `ICommand`, drops the view in a nested scope, and asserts the view is collected (`WaitForCollect` returns `false`). | State | `SwipeItemView, useWeakEventHandler: False` | Full theory | |-------|---------------------------------------------|-------------| | Without fix (`main`) | ❌ FAIL — `Assert.False` (view still alive) | 15 passed / **1 failed** | | With fix | ✅ PASS | **16 passed** / 0 failed | No regressions with the fix applied: `CommandTests` (45/45) and `SwipeViewTests` (24/24) pass. Validated by building MAUI from source on the net library TFM. ## Scope - Managed, cross-platform product change only: `src/Controls/src/Core/SwipeView/SwipeItemView.cs`. - Test: `src/Controls/tests/Core.UnitTests/CommandTests.cs`. - No test was muted, skipped, weakened, or deleted; no `[ActiveIssue]`/`Skip=`; no public API change. --- _Generated by the Memory Leak Fixer agentic workflow (dotnet/maui). This description is AI-generated — please review before merging._ > Generated by [Memory Leak Fixer](https://github.com/dotnet/maui/actions/runs/28685456694) · 2.6K AIC · ⌖ 212.1 AIC · ⊞ 60.5K · [◷](https://github.com/search?q=repo%3Adotnet%2Fmaui+%22gh-aw-workflow-id%3A+leak-fixer%22&type=pullrequests) <!-- gh-aw-agentic-workflow: Memory Leak Fixer, engine: copilot, version: 1.0.60, model: claude-opus-4.8, id: 28685456694, workflow_id: leak-fixer, run: https://github.com/dotnet/maui/actions/runs/28685456694 --> <!-- gh-aw-workflow-id: leak-fixer --> <!-- gh-aw-workflow-call-id: dotnet/maui/leak-fixer --> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
> [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! > [!IMPORTANT] > 🤖 **AI-generated PR.** This pull request was produced automatically by the **Memory Leak Fixer** agentic workflow on dotnet/maui. The regression test below was empirically proven to **fail on `main` without the fix** and **pass with it** during this run. Please review carefully before merging. Fixes #36343 Refs: #36343 Target branch: `main` Attempt: 1/3 ## The leak `SwipeItemView.OnCommandChanged()` subscribed to the assigned command with a **strong** delegate: ```csharp Command.CanExecuteChanged += OnCommandCanExecuteChanged; ``` Because `OnCommandCanExecuteChanged` is an instance method, the delegate's `Target` is the `SwipeItemView`. Any command that raises `CanExecuteChanged` through a **strong** event (e.g. a custom `ICommand`, or `CommunityToolkit.Mvvm`'s `RelayCommand`) therefore keeps the `SwipeItemView` — and its entire visual subtree — alive for as long as the command lives. The matching `-=` only ran in `OnCommandChanging()` (the property-*changing* path), so a view that is simply unloaded/discarded while a longer-lived command outlives it is never unsubscribed, and it leaks. > `Microsoft.Maui.Controls.Command` raises `CanExecuteChanged` through a `WeakEventManager` and does **not** leak — the regression test covers that non-leaking case too, to prove the fix preserves it. ## The fix Replace the raw subscription with the `WeakCommandSubscription` helper already used by `Button`, `ImageButton`, `RefreshView`, `MenuItem`, `SearchBar`, `TextCell`, etc. It links the command to the view through a `DependentHandle`, so the command references the view only **weakly** and the view can be collected once nothing else roots it: ```csharp WeakCommandSubscription _commandSubscription; void OnCommandChanged() { IsEnabled = Command?.CanExecute(CommandParameter) ?? true; if (Command == null) return; _commandSubscription = new WeakCommandSubscription(this, Command, OnCommandCanExecuteChanged); } void OnCommandChanging() { _commandSubscription?.Dispose(); _commandSubscription = null; } ``` - No public API change — the field is private and `WeakCommandSubscription` is internal. - All existing `IsEnabled` behavior is preserved; only the subscription lifetime changes. - Idiomatic: mirrors exactly how the sibling command-bearing controls already harden this same subscription. ## Regression test Added the `SwipeItemView` case to the existing `CommandsSubscribedToCanExecuteCollect` theory in `src/Controls/tests/Core.UnitTests/CommandTests.cs`, right alongside every other command-bearing control. It assigns a strong-event `ICommand`, drops the view in a nested scope, and asserts the view is collected (`WaitForCollect` returns `false`). | State | `SwipeItemView, useWeakEventHandler: False` | Full theory | |-------|---------------------------------------------|-------------| | Without fix (`main`) | ❌ FAIL — `Assert.False` (view still alive) | 15 passed / **1 failed** | | With fix | ✅ PASS | **16 passed** / 0 failed | No regressions with the fix applied: `CommandTests` (45/45) and `SwipeViewTests` (24/24) pass. Validated by building MAUI from source on the net library TFM. ## Scope - Managed, cross-platform product change only: `src/Controls/src/Core/SwipeView/SwipeItemView.cs`. - Test: `src/Controls/tests/Core.UnitTests/CommandTests.cs`. - No test was muted, skipped, weakened, or deleted; no `[ActiveIssue]`/`Skip=`; no public API change. --- _Generated by the Memory Leak Fixer agentic workflow (dotnet/maui). This description is AI-generated — please review before merging._ > Generated by [Memory Leak Fixer](https://github.com/dotnet/maui/actions/runs/28685456694) · 2.6K AIC · ⌖ 212.1 AIC · ⊞ 60.5K · [◷](https://github.com/search?q=repo%3Adotnet%2Fmaui+%22gh-aw-workflow-id%3A+leak-fixer%22&type=pullrequests) <!-- gh-aw-agentic-workflow: Memory Leak Fixer, engine: copilot, version: 1.0.60, model: claude-opus-4.8, id: 28685456694, workflow_id: leak-fixer, run: https://github.com/dotnet/maui/actions/runs/28685456694 --> <!-- gh-aw-workflow-id: leak-fixer --> <!-- gh-aw-workflow-call-id: dotnet/maui/leak-fixer --> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Important
🤖 AI-generated PR. This pull request was produced automatically by the Memory Leak Fixer agentic workflow on dotnet/maui. The regression test below was empirically proven to fail on
mainwithout the fix and pass with it during this run. Please review carefully before merging.Fixes #36343
Refs: #36343
Target branch:
mainAttempt: 1/3
The leak
SwipeItemView.OnCommandChanged()subscribed to the assigned command with a strong delegate:Because
OnCommandCanExecuteChangedis an instance method, the delegate'sTargetis theSwipeItemView. Any command that raisesCanExecuteChangedthrough a strong event (e.g. a customICommand, orCommunityToolkit.Mvvm'sRelayCommand) therefore keeps theSwipeItemView— and its entire visual subtree — alive for as long as the command lives. The matching-=only ran inOnCommandChanging()(the property-changing path), so a view that is simply unloaded/discarded while a longer-lived command outlives it is never unsubscribed, and it leaks.The fix
Replace the raw subscription with the
WeakCommandSubscriptionhelper already used byButton,ImageButton,RefreshView,MenuItem,SearchBar,TextCell, etc. It links the command to the view through aDependentHandle, so the command references the view only weakly and the view can be collected once nothing else roots it:WeakCommandSubscriptionis internal.IsEnabledbehavior is preserved; only the subscription lifetime changes.Regression test
Added the
SwipeItemViewcase to the existingCommandsSubscribedToCanExecuteCollecttheory insrc/Controls/tests/Core.UnitTests/CommandTests.cs, right alongside every other command-bearing control. It assigns a strong-eventICommand, drops the view in a nested scope, and asserts the view is collected (WaitForCollectreturnsfalse).SwipeItemView, useWeakEventHandler: Falsemain)Assert.False(view still alive)No regressions with the fix applied:
CommandTests(45/45) andSwipeViewTests(24/24) pass. Validated by building MAUI from source on the net library TFM.Scope
src/Controls/src/Core/SwipeView/SwipeItemView.cs.src/Controls/tests/Core.UnitTests/CommandTests.cs.[ActiveIssue]/Skip=; no public API change.Generated by the Memory Leak Fixer agentic workflow (dotnet/maui). This description is AI-generated — please review before merging.