Repository navigation
[leak-fix] Fix BackButtonBehavior.Command memory leak (Fixes #36538) - #36566
github-actions[bot] wants to merge 4 commits into
Conversation
BackButtonBehavior subscribed to ICommand.CanExecuteChanged with a strong delegate; the matching unsubscribe only ran when Command changed, so a command that outlives the behavior kept it (and everything it roots) alive. Route the subscription through the existing WeakCommandSubscription helper (DependentHandle-based) already used by CommandElement for Button, ImageButton, RefreshView, etc. No public API change; IsEnabled/CanExecute behavior preserved. Adds BackButtonBehavior rows to the existing CommandsSubscribedToCanExecuteCollect theory in CommandTests.cs. The strong-event variant fails without the fix and passes with it. Generated by the Memory Leak Fixer agentic workflow (dotnet/maui). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: Successfully started running 1 pipeline(s). There may be pipelines that require an authorized user to comment /azp run to run. |
This comment has been minimized.
This comment has been minimized.
|
🔍 AI-generated review follow-up (GitHub Copilot CLI, on behalf of @kubaflo) @MauiBot Review The low-confidence |
This comment has been minimized.
This comment has been minimized.
|
🔍 AI-generated review follow-up (GitHub Copilot CLI, on behalf of @kubaflo) @MauiBot Review |
This comment has been minimized.
This comment has been minimized.
|
🔍 AI-generated review follow-up (GitHub Copilot CLI, on behalf of @kubaflo) @MauiBot Review The review selected the submitted |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
🔍 AI-generated review follow-up (GitHub Copilot CLI, on behalf of @kubaflo) @MauiBot Review |
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 1 findings
See inline comments for details.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bbcdaa9d-d75a-4e3b-b375-cf827a689644
|
🔍 AI-generated update (GitHub Copilot CLI, on behalf of @kubaflo) @MauiBot The regression-prevention suggestion from review The full |
|
🔍 AI-generated review response (GitHub Copilot CLI, on behalf of @kubaflo) @MauiBot Review No code change or no-op commit is needed. Releasing the already queued improved-reviewer rerun to retry that UI leg. |
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:
fa8ab0f. To request a fresh review after new comments or commits, comment/review rerun.
🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix
Gate Result: ✅ PASSED
Platform: ANDROID · Base: main · Merge base: 0395a53b
| Test | Without Fix (expect FAIL) | With Fix (expect PASS) |
|---|---|---|
🧪 CommandTests CommandTests |
✅ FAIL — 153s | ✅ PASS — 118s |
🔴 Without fix — 🧪 CommandTests: FAIL ✅ · 153s
Error-relevant lines (filtered from the build log):
at Microsoft.Maui.Controls.Core.UnitTests.CommandTests.CommandsSubscribedToCanExecuteCollect(Type controlType, Boolean useWeakEventHandler) in /_/src/Controls/tests/Core.UnitTests/CommandTests.cs:line 399
at Microsoft.Maui.Controls.Core.UnitTests.CommandTests.BackButtonBehaviorAppliesBoundCommandParameterBeforeCanExecute() in /_/src/Controls/tests/Core.UnitTests/CommandTests.cs:line 284
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
🟢 With fix — 🧪 CommandTests: PASS ✅ · 118s
(no coded error found; showing last 1200 chars)
alse) [< 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 BackButtonBehaviorPreservesUserDefinedDisabledStateForExecutableCommand [< 1 ms]
Passed ExecuteRunsIfNullableAndSetToNull [< 1 ms]
Passed ExecuteParameterized [3 ms]
Passed GenericThrowsWithValidExecuteAndCanExecuteNull [< 1 ms]
[xUnit.net 00:00:04.37] Finished: Microsoft.Maui.Controls.Core.UnitTests
Passed ExecuteWithCanExecute [< 1 ms]
Passed CanExecuteReturnsFalseIfParameterIsWrongReferenceType [< 1 ms]
Test Run Successful.
Total tests: 47
Passed: 47
Total time: 5.0707 Seconds
📁 Fix files reverted (1 files)
src/Controls/src/Core/Shell/BackButtonBehavior.cs
📱 UI Tests — Shell
Detected UI test categories: Shell
✅ Deep UI tests — 309 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 |
|---|---|---|
Shell |
309/309 ✓ | — |
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs) |
📋 Pre-Flight — Context & Validation
Issue: #36538 - [leak-scan] BackButtonBehavior.Command — non-weak ICommand.CanExecuteChanged retains the Shell BackButtonBehavior
PR: #36566 - [leak-fix] Fix BackButtonBehavior.Command memory leak (Fixes #36538)
Platforms Affected: all platforms (managed Controls/Shell code); testing platform requested: android
Files Changed: 1 implementation, 1 test
Key Findings
- The issue reports that
BackButtonBehaviorused a strongICommand.CanExecuteChangedsubscription, so long-lived commands could retain transient Shell back-button behavior instances and their page graph. - The PR replaces the BackButton-specific subscription code with the shared internal
ICommandElement/CommandElementweak subscription path used by sibling command controls. - Prior MauiBot findings about command-parameter binding order and user-disabled
IsEnabledpreservation are fixed in the current local diff. - GitHub CLI is unauthenticated in this environment; context was gathered from the checked-out
pr-review-36566branch, local diff againstorigin/main, and public GitHub API calls.
Code Review Summary
Verdict: NEEDS_DISCUSSION
Confidence: low
Errors: 0 | Warnings: 0 | Suggestions: 0
Key code review findings:
- No actionable implementation findings remain in the current PR diff.
- Prior major finding fixed:
BackButtonBehavior.CommandPropertynow depends onCommandParameterProperty, andCanExecuteChangedcallsCommandElement.GetCanExecute(this, CommandProperty). - Prior moderate finding fixed:
BackButtonBehaviorPreservesUserDefinedDisabledStateForExecutableCommandcovers explicitIsEnabled = falsepreservation. - CI confidence is capped because authenticated required-check lookup was unavailable; public fallback comments/check evidence indicated green or infrastructure-only interruptions.
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #36566 | Implement ICommandElement and route BackButtonBehavior command lifecycle through shared CommandElement/WeakCommandSubscription; add command-parameter dependency and regression tests. |
✅ PASSED (Gate) | BackButtonBehavior.cs, CommandTests.cs |
Original PR; gate already completed externally. |
🔬 Code Review — Deep Analysis
Code Review — PR #36566
Independent Assessment
What this changes: BackButtonBehavior now uses the shared ICommandElement/CommandElement weak command-subscription infrastructure instead of directly subscribing to ICommand.CanExecuteChanged. It also wires CommandProperty.DependsOn(CommandParameterProperty) and adds unit coverage for binding order, disabled-state preservation, and weak-subscription collection.
Inferred motivation: Avoid retaining BackButtonBehavior/Shell pages through long-lived commands while matching the command-parameter timing behavior used by other command controls.
Reconciliation with PR Narrative
Author claims: Fixes memory leak #36538 by replacing a strong CanExecuteChanged subscription with WeakCommandSubscription, with regression tests and no public API change.
Agreement/disagreement: Agrees. The current code matches existing ICommandElement patterns and addresses the issue’s retention path.
Prior Review Reconciliation
| Prior ❌ Error Finding | Source | Status | Evidence |
|---|---|---|---|
BackButtonBehavior did not wire CommandProperty.DependsOn(CommandParameterProperty) / GetCanExecute(this, CommandProperty) |
MauiBot inline [major] / review |
✅ Fixed | Current BackButtonBehavior.cs:24-27 registers the dependency and BackButtonBehavior.cs:118-120 calls CommandElement.GetCanExecute(this, CommandProperty). |
Missing regression coverage for user-disabled IsEnabled preservation |
MauiBot inline [moderate] |
✅ Fixed | CommandTests.cs:287-302 adds BackButtonBehaviorPreservesUserDefinedDisabledStateForExecutableCommand. |
Blast Radius Assessment
- Runs for all instances: No — only
BackButtonBehaviorinstances with command usage. - Startup impact: Minimal; static constructor only registers a bindable-property dependency.
- Static/shared state: No mutable shared state; subscription tracker is per instance.
CI Status
- Required-check result:
gh pr checks --requiredunavailable because GitHub CLI is unauthenticated. - Public fallback: PR discussion reports green command/unit coverage and later zero-findings reviews; current authenticated required-check surface remains unavailable here.
- Classification: Required-check surface undetermined in this environment.
- Action taken: Capped confidence low; no
LGTMper skill Rule #6.
Findings
No actionable ❌ Error,
Failure-Mode Probing
- Command outlives behavior/page: weak subscription avoids command event retaining the behavior.
- Command set/replaced/null:
OnCommandChangingdisposes prior tracker; null command recalculates enabled state to true. - Bound
CommandParameterresolves afterCommand: dependency forcing is active viaGetCanExecute(this, CommandProperty). - User explicitly disables back button:
_userDefinedIsEnabled && _commandEnabledpreserves disabled state; covered by test.
Verdict: NEEDS_DISCUSSION
Confidence: low
Summary: Code review found no unresolved implementation issues, and public CI evidence is green. However, authenticated required-check status could not be collected, so the skill rules prohibit LGTM and cap confidence.
🛠️ Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix | BackButtonBehavior-local WeakCommandSubscription, no ICommandElement. |
❌ FAIL | 1 file | Left CommandParameterProperty on CommandElement.OnCommandParameterChanged, causing an invalid cast after removing ICommandElement. |
| 2 | try-fix | Fully local weak-subscription implementation with local command and parameter callbacks. | ✅ PASS | 1 file | Functionally passes, but expert review says it is not better than the PR because it duplicates shared command logic and weakens shared test observability. |
| 3 | try-fix | Nested weak CanExecuteChangedProxy retained by the command instead of WeakCommandSubscription. |
✅ PASS | 1 file | Fixes behavior retention but can leak stale proxy subscriptions until the command raises again; weaker lifecycle than PR. |
| PR | PR #36566 | Shared ICommandElement/CommandElement weak subscription with command-parameter dependency. |
✅ PASSED (Gate) | 2 files | Original PR; selected as most idiomatic and lifecycle-complete. |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| gpt-5.5 expert loop | 1 | Yes | Candidate 2 should keep the local weak-subscription approach but own the CommandParameter callback as well. |
| maui-expert-reviewer | 1 | No better passing idea | Candidate 2 is probably correct but not demonstrably better than the PR's shared ICommandElement implementation. |
| gpt-5.5 expert loop | 2 | Yes | Candidate 3 can avoid WeakCommandSubscription entirely with a nested weak proxy. |
| maui-expert-reviewer | 2 | No | Candidate 3 is weaker than the PR because stale proxy subscriptions can remain rooted by long-lived commands that never raise again. |
Exhausted: Yes
Selected Fix: PR #36566 — it passes the gate, matches established MAUI command-element conventions, centralizes future command lifecycle fixes, preserves command-parameter dependency behavior, and avoids the stale-proxy/subscription observability gaps found in the alternatives.
📝 Recommended PR Title & Description
Assessment: ✏️ Recommend updating — the description is accurate and high quality, but the current title uses a workflow-style [leak-fix] prefix instead of the repository's searchable component-focused title format.
Recommended title
[All] Shell: Fix BackButtonBehavior.Command memory leak (Fixes #36538)
Recommended description
> [!IMPORTANT]
> This pull request was generated automatically by the **Memory Leak Fixer** agentic workflow in dotnet/maui. It contains a regression test empirically proven to fail without the fix and pass with it. Please review carefully — it is AI-generated.
Fixes #36538
Refs: dotnet/maui#36538
Target branch: main
Attempt: 1/3
## The leak
`BackButtonBehavior` subscribed to `ICommand.CanExecuteChanged` with a **strong** delegate:
```csharp
newCommand.CanExecuteChanged += CanExecuteChanged;
```
The matching unsubscribe only ran when the `Command` property changed. Because an `ICommand` is commonly stored on a long-lived view model (or otherwise shared/retained), that strong subscription keeps the `BackButtonBehavior` — and everything it transitively roots — alive for as long as the command lives.
Retention path from issue #36538:
`ICommand (long-lived) -> CanExecuteChanged event -> BackButtonBehavior.CanExecuteChanged delegate -> BackButtonBehavior`
## The fix
Implement the existing internal `ICommandElement` contract and route command lifecycle callbacks through `CommandElement`, matching `Button`, `ImageButton`, `RefreshView`, and other command controls. `CommandElement` owns the `WeakCommandSubscription`, disposes it before command replacement, and uses a `DependentHandle` so the command references the behavior only weakly.
`BackButtonBehavior.CommandProperty` now declares its dependency on `CommandParameterProperty`, and `CanExecuteChanged` passes `CommandProperty` to `CommandElement.GetCanExecute`. If both properties are bound and the command resolves first during reparenting, the pending parameter binding is applied before the first `CanExecute` evaluation instead of evaluating with a null or stale parameter.
```csharp
public class BackButtonBehavior : BindableObject, ICommandElement
{
public static readonly BindableProperty CommandProperty =
BindableProperty.Create(
nameof(Command),
typeof(ICommand),
typeof(BackButtonBehavior),
null,
BindingMode.OneTime,
propertyChanging: CommandElement.OnCommandChanging,
propertyChanged: CommandElement.OnCommandChanged);
static BackButtonBehavior()
{
CommandProperty.DependsOn(CommandParameterProperty);
}
void ICommandElement.CanExecuteChanged(object sender, EventArgs e)
{
IsEnabledCore = CommandElement.GetCanExecute(this, CommandProperty);
}
WeakCommandSubscription ICommandElement.CleanupTracker { get; set; }
}
```
No public API change (`ICommandElement` is internal and its members are implemented explicitly). User-defined `IsEnabled` state remains combined with command-derived enabled state.
## Regression tests
The existing `CommandsSubscribedToCanExecuteCollect` theory includes `BackButtonBehavior` rows for both weak-event and strong-event command variants. The strong-event variant (`useWeakEventHandler: false`) reproduces the leak. Because `BackButtonBehavior` now implements `ICommandElement`, the shared test also verifies collection of its `WeakCommandSubscription` cleanup tracker and proxy.
`BackButtonBehaviorAppliesBoundCommandParameterBeforeCanExecute` binds both `Command` and `CommandParameter`, applies the binding context with the command binding registered first, and verifies every `CanExecute` invocation receives the resolved parameter.
`BackButtonBehaviorPreservesUserDefinedDisabledStateForExecutableCommand` verifies an explicit `IsEnabled = false` survives both assignment of an always-executable command and a later `CanExecuteChanged` notification.
🏁 Report — Final Recommendation
Comparative Fix Report — PR #36566
Candidates compared
| Rank | Candidate | Regression result | Assessment |
|---|---|---|---|
| 1 | pr |
✅ Passed | Best candidate. Uses the existing shared ICommandElement / CommandElement / WeakCommandSubscription pattern already used by command controls, fixes the leak, preserves command-parameter ordering, preserves explicit disabled state, and keeps shared leak-test observability. |
| 1 | pr-plus-reviewer |
✅ Passed | Equivalent to pr. The expert reviewer found no actionable issues, so no reviewer-driven changes were applied. It ranks alongside the PR but is not a distinct improvement. |
| 3 | try-fix-2 |
✅ Passed | Functionally plausible local weak-subscription implementation, but weaker than the PR because it duplicates shared command lifecycle logic and loses the generic ICommandElement.CleanupTracker / proxy observability in the shared leak test path. |
| 4 | try-fix-3 |
✅ Passed | Fixes retention of BackButtonBehavior, but can leave stale proxy subscriptions rooted by a long-lived command until a later CanExecuteChanged event occurs. This turns the original leak into a smaller lifecycle leak and is weaker than the PR. |
| 5 | try-fix-1 |
❌ Failed | Must rank below all passing candidates. It removed ICommandElement while leaving CommandParameterProperty on CommandElement.OnCommandParameterChanged, causing an InvalidCastException. |
Winning candidate
Winner: pr
The raw PR fix is the strongest solution because it reuses the established shared command infrastructure instead of creating BackButton-specific lifecycle code. It addresses the root leak, handles command replacement deterministically, preserves binding and enabled-state semantics, and is covered by targeted regression tests that already passed the gate. Since the expert reviewer recommended no code changes, pr-plus-reviewer is equivalent and does not supersede the submitted PR.
Candidate details
pr
BackButtonBehavior now implements the internal ICommandElement contract. CommandProperty uses CommandElement.OnCommandChanging and CommandElement.OnCommandChanged, CommandParameterProperty uses CommandElement.OnCommandParameterChanged, and CanExecuteChanged delegates to CommandElement.GetCanExecute(this, CommandProperty).
This is the most maintainable fix because it aligns BackButtonBehavior with existing command controls such as Button, ImageButton, TextCell, MenuItem, RefreshView, and SearchBar.
pr-plus-reviewer
The expert reviewer produced no actionable findings and wrote an empty inline findings file. No sandbox patch was applied, so this candidate is identical to pr.
try-fix-1
This candidate attempted to keep a local WeakCommandSubscription while removing ICommandElement. It failed because CommandParameterProperty still used CommandElement.OnCommandParameterChanged, which casts the bindable object to ICommandElement.
try-fix-2
This candidate fixed the callback ownership issue from try-fix-1 by implementing all command and command-parameter lifecycle handling locally. It passed targeted tests, but it duplicates shared logic and reduces coverage through the shared ICommandElement cleanup-tracker path. There is no demonstrated benefit over the PR.
try-fix-3
This candidate used a nested weak proxy retained by the command. It passed targeted tests and avoids retaining BackButtonBehavior, but stale proxy subscriptions can remain indefinitely if the long-lived command never raises CanExecuteChanged again. That lifecycle behavior is inferior to the PR's WeakCommandSubscription approach.
Final recommendation
Proceed with the PR fix as submitted. Do not replace it with any STEP 5a try-fix candidate.
🧭 Next Steps — review latest findings
No alternative fix was selected for this run. Review the session findings and CI results before merging.
|
🔍 AI-generated follow-up (GitHub Copilot CLI, on behalf of @kubaflo) @MauiBot Review #4704615522 found no actionable implementation issues on The suggested title rewrite was not applied because the |
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). There may be pipelines that require an authorized user to comment /azp run to run. |
|
Azure Pipelines: Successfully started running 3 pipeline(s). |
There was a problem hiding this comment.
Pull request overview
This PR fixes a memory leak in Shell’s BackButtonBehavior.Command handling by switching from a strong CanExecuteChanged subscription to MAUI’s existing weak-command subscription infrastructure (ICommandElement + CommandElement). It also adds/extends unit tests to prevent regressions, including coverage for binding-order timing and preservation of user-defined disabled state.
Changes:
- Update
BackButtonBehaviorto implement internalICommandElementand route command lifecycle throughCommandElement(weak subscription + cleanup). - Register
CommandPropertydependency onCommandParameterPropertysoCanExecuteevaluation can force-apply a pending parameter binding when needed. - Add regression tests for leak collection, correct command-parameter propagation timing, and preserving user-disabled
IsEnabledbehavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/Controls/src/Core/Shell/BackButtonBehavior.cs | Switches command handling to CommandElement/ICommandElement weak subscription and adds DependsOn for parameter binding timing. |
| src/Controls/tests/Core.UnitTests/CommandTests.cs | Adds new BackButtonBehavior-specific regression tests and extends the shared command GC/leak theory to include BackButtonBehavior. |
|
🔍 AI-generated merge-readiness analysis (GitHub Copilot CLI, on behalf of @kubaflo): good to merge. All three current-head pipelines completed for
No failure correlates with Shell back-button command subscription behavior, so no PR code change is warranted. |
This comment has been minimized.
This comment has been minimized.
Tests Failure Analysis
Test Failure Review: Not ready - click to expandOverall verdict: Not ready. Three legs/tests are deterministic regressions vs
Coverage: 162 checks · 146 passing · 16 failing · 0 pending · 0 inaccessible · 1 unmapped · 17 unexplained build legs · 0 unaccounted failing checks · 2 aborted failing checks · 0 canceled-build checks · 5 device-test unverified · 29 unattributed · 3 regressed-vs-base. Deterministic ceiling: Not ready — regressed-vs-base > 0, unexplained legs, aborted checks, and unverified device tests all present. Builds (this PR): maui-pr 1510832, maui-pr-devicetests 1510834, maui-pr-uitests 1510833. Base sampling (main, 5 recent builds per definition): maui-pr 1503430, maui-pr-devicetests 1503431, maui-pr-uitests 1503618. Recommended actionHave a human inspect the three regressed-vs-base items — especially the two Windows device-test build breaks and |
|
🔍 AI-generated follow-up analysis (GitHub Copilot CLI, on behalf of @kubaflo) The
The PR changes only |
… on RS0016 The gate recompiles the MAUI product (Controls.Core, ...) from source via the unit/XAML test project's P2P references, re-running the PublicAPI analyzer under the repo-wide TreatWarningsAsErrors=true. A leak-fix PR that adds a finalizer (e.g. #36605 ~SwipeView()) surfaces RS0016/RS0017 as a build-breaking ERROR during the revert -> build -> restore -> build cycle, so the with-fix build fails to compile and the gate reports a false FAILED — even though the PR's own maui-pr build (a REQUIRED check that separately enforces PublicAPI bookkeeping) is green. Many in-flight PRs are leak fixes that add finalizers (#36575, #36566, #36547, #36531, #36526, #36521, #36513, ...), so ALL of them hit this false-FAILED. Fix: pass -p:TreatWarningsAsErrors=false to the gate's unit/XAML dotnet test and clean-rebuild-retry invocations, matching the deep stage's existing mitigation in Build-AndDeploy.ps1. The gate verifies TEST BEHAVIOR, not API bookkeeping; genuine CS-level compile ERRORS still fail the build. The UITest/device path already routes through Build-AndDeploy.ps1 and was already covered. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 15d2af20-e4ab-4e88-9011-cfbd83513bc0
|
🔍 AI-generated (GitHub Copilot CLI, on behalf of @kubaflo) Closing as superseded. Issue #36538 is a duplicate of #36345, whose fix — #36370 "[leak-fix] Fix BackButtonBehavior.Command memory leak" — is already merged into The |
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
This pull request was generated automatically by the Memory Leak Fixer agentic workflow in dotnet/maui. It contains a regression test empirically proven to fail without the fix and pass with it. Please review carefully — it is AI-generated.
Fixes #36538
Refs: #36538
Target branch: main
Attempt: 1/3
The leak
BackButtonBehaviorsubscribed toICommand.CanExecuteChangedwith a strong delegate:The matching unsubscribe only ran when the
Commandproperty changed. Because anICommandis commonly stored on a long-lived view model (or otherwise shared/retained), that strong subscription keeps theBackButtonBehavior— and everything it transitively roots — alive for as long as the command lives.Retention path (from issue #36538):
ICommand (long-lived) -> CanExecuteChanged event -> BackButtonBehavior.CanExecuteChanged delegate -> BackButtonBehaviorThe fix
Implement the existing internal
ICommandElementcontract and route command lifecycle callbacks throughCommandElement, matchingButton,ImageButton,RefreshView, and other command controls.CommandElementowns theWeakCommandSubscription, disposes it before command replacement, and uses aDependentHandleso the command references the behavior only weakly.BackButtonBehavior.CommandPropertynow declares its dependency onCommandParameterProperty, andCanExecuteChangedpassesCommandPropertytoCommandElement.GetCanExecute. If both properties are bound and the command resolves first during reparenting, the pending parameter binding is applied before the firstCanExecuteevaluation instead of evaluating with a null or stale parameter.No public API change (
ICommandElementis internal and its members are implemented explicitly). User-definedIsEnabledstate remains combined with command-derived enabled state.Regression tests
The existing
CommandsSubscribedToCanExecuteCollecttheory includesBackButtonBehaviorrows for both weak-event and strong-event command variants. The strong-event variant (useWeakEventHandler: false) reproduces the leak. BecauseBackButtonBehaviornow implementsICommandElement, the shared test also verifies collection of itsWeakCommandSubscriptioncleanup tracker and proxy.BackButtonBehaviorAppliesBoundCommandParameterBeforeCanExecutebinds bothCommandandCommandParameter, applies the binding context with the command binding registered first, and verifies everyCanExecuteinvocation receives the resolved parameter.BackButtonBehaviorPreservesUserDefinedDisabledStateForExecutableCommandverifies an explicitIsEnabled = falsesurvives both assignment of an always-executable command and a laterCanExecuteChangednotification.Verified in this run (net TFM,
Controls.Core.UnitTests):BackButtonBehavior, falseAssert.False() Failure — Expected: False, Actual: True(behavior still alive)Red run (unpatched source):
Total tests: 16, Passed: 15, Failed: 1— only theBackButtonBehavior, falsecase failed.Green run (with fix):
Total tests: 16, Passed: 16.The full
CommandTestsclass passes 47 tests. The completeControls.Core.UnitTestsproject passes 5,669 tests with 30 existing tests skipped.Scope
src/Controls/src/Core/Shell/BackButtonBehavior.csandsrc/Controls/tests/Core.UnitTests/CommandTests.cs.[ActiveIssue]/Skip=/ timeout changes.ICommandElement/CommandElementweak-subscription and command-parameter dependency lifecycle as sibling command controls.🤖 Generated by the Memory Leak Fixer agentic workflow (dotnet/maui). This PR is AI-generated — please review before merging.