Skip to content

[iOS] Fix Unsubscribe replaced Shell flyout footer from MeasureInvalidated - #37254

Merged
kubaflo merged 4 commits into
dotnet:inflight/currentfrom
devanathan-vaithiyanathan:fix-99999
Aug 10, 2026
Merged

kubaflo merged 4 commits into
dotnet:inflight/currentfrom
devanathan-vaithiyanathan:fix-99999

Conversation

@devanathan-vaithiyanathan

Copy link
Copy Markdown
Contributor

Issue Details

On iOS and Mac Catalyst, ShellFlyoutContentRenderer subscribes to the flyout footer’s MeasureInvalidated event when the footer is created. When Shell.FlyoutFooter is replaced, the old footer handler is disconnected, but its MeasureInvalidated subscription remains active.

Root Cause

The renderer adds this subscription when installing a footer. However, the replacement path previously disconnected the footer without removing the subscription.

Description of Change

Unsubscribe the previous flyout footer from MeasureInvalidated before disconnecting its handler in ShellFlyoutContentRenderer.cs:138:

Issues Fixed

Fixes #37187

Tested the behavior in the following platforms.

  • Android
  • Windows
  • iOS
  • Mac
Before After
iOS
Before.mov
iOS
After.mov

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

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

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

Or

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

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service dotnet-policy-service Bot added the partner/syncfusion Issues / PR's with Syncfusion collaboration label Aug 10, 2026
@devanathan-vaithiyanathan devanathan-vaithiyanathan added the community ✨ Community Contribution label Aug 10, 2026
@sheiksyedm
sheiksyedm marked this pull request as ready for review August 10, 2026 10:28
Copilot AI lite review requested due to automatic review settings August 10, 2026 10:28
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes an iOS/MacCatalyst Shell flyout lifecycle issue where replacing Shell.FlyoutFooter could leave the previous footer’s MeasureInvalidated subscription active, causing stale re-measurements and delayed detachment.

Changes:

  • Unsubscribes the previous flyout footer from MeasureInvalidated before disconnecting its handler in ShellFlyoutContentRenderer.
  • Adds a HostApp repro page for Issue #37187 to exercise footer replacement + invalidation.
  • Adds an iOS/MacCatalyst UI test to validate the removed footer no longer triggers re-measure of the current footer.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellFlyoutContentRenderer.cs Removes stale MeasureInvalidated handler when the flyout footer is replaced.
src/Controls/tests/TestCases.HostApp/Issues/Issue37187.cs HostApp page reproducing footer replacement + invalidation scenario.
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue37187.cs UI test ensuring invalidating the removed footer does not re-measure the current footer on iOS/MacCatalyst.

@kubaflo

This comment has been minimized.

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

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Expert Review — 6 findings

See inline comments for details.

int previousIndex = GetPreviousIndex(_footerView);
if (_footer is not null)
{
_footer.MeasureInvalidated -= OnFooterMeasureInvalidated;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Handler Mapper and Property Patterns / Memory Leak Prevention — The unsubscribe itself is correct and correctly placed (it runs before _footer.Handler = null / oldRenderer?.DisconnectHandler(), which can themselves raise invalidation), but it only restores symmetry for the replacement path. ShellFlyoutContentRenderer has no Dispose(bool) override, so the teardown path is still asymmetric: when the renderer is torn down (Shell handler disconnect, window close, ShellRenderer recreating the flyout content — see ShellRenderer.cs:173), the last installed _footer keeps its MeasureInvalidated += OnFooterMeasureInvalidated subscription. Because Shell.FlyoutFooter is owned by the user's Shell and outlives the renderer, that subscription roots the whole ShellFlyoutContentRenderer UIViewController tree — the exact leak class this PR is fixing, just via a different trigger. Concrete scenario: set Shell.FlyoutFooter once, tear down/recreate the Shell window (or reuse the same footer View instance across two Shell instances) — the old renderer is never collected, and a later InvalidateMeasure() on that footer still calls into the dead renderer's ReMeasureFooter(), touching View.Frame on a detached UIViewController. Recommend adding a protected override void Dispose(bool disposing) that does _footer.MeasureInvalidated -= OnFooterMeasureInvalidated; (and, while there, _shellContext.Shell.PropertyChanged -= HandleShellPropertyChanged;, subscribed unconditionally in the ctor at line 41 and never removed).


Assert.That(
App.WaitForElement("FooterMeasureStatus").GetText(),
Is.EqualTo("Current footer measure count: 0"));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[critical] Regression Prevention and Test Coverage — This test does not reproduce the bug: it passes identically with and without the fix (Gate verification, iOS: PASS without fix / PASS with fix). A green test that is green in both states provides zero regression protection — if the -= at ShellFlyoutContentRenderer.cs:138 is reverted tomorrow, nothing here catches it. The assertion measures a signal (MeasureOverride invocation count on the current footer) that is not reliably produced by the stale-subscription path, so it cannot discriminate. See the inline notes on TestCases.HostApp/Issues/Issue37187.cs for the specific mechanisms; the test must be rewritten to fail on the pre-fix code before this PR can be considered covered.

}

_oldFooter.InvalidateMeasure();
_statusLabel.Text = $"Current footer measure count: {_currentFooter.MeasureCount}";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[critical] Logic and Correctness / Test Coverage — The probe cannot observe the stale callback, which is why the test passes without the fix. Three independent reasons, any one of which makes it a false green:

  1. Measure caching. The stale path is old footer MeasureInvalidated → OnFooterMeasureInvalidated → ReMeasureFooter → _footerView.SizeThatFits → (_currentFooter as IView).Measure(...). VisualElement/IView.Measure returns the cached desired size when the element's measure is still valid — and _currentFooter (Footer B) was never invalidated — so MeasureProbeView.MeasureOverride is never re-entered even when the stale handler does fire. MeasureCount stays 0 in both states.
  2. Synchronous read. _currentFooter.MeasureCount is read on the same line/turn as _oldFooter.InvalidateMeasure(). Even if a re-measure were triggered, the resulting UpdateFooterPosition / LayoutParallax work is not guaranteed to have completed before the label is formatted.
  3. The removed footer may not raise at all. By the time this runs, Footer A has had Handler = null + DisconnectHandler() applied and is detached from the visual tree; InvalidateMeasure() on such an element is not guaranteed to reach InvokeMeasureInvalidated, so the pre-fix subscription may never be exercised.

Recommend replacing the measure-count probe with a signal that is directly tied to the subscription. Two options that are deterministic:

  • Preferred (cheap + deterministic): drop the UI test and add an iOS/MacCatalyst device test that replaces Shell.FlyoutFooter and asserts the previous footer View becomes GC-eligible (WeakReference + AssertionExtensions.WaitForGC). The stale MeasureInvalidated subscription roots the old footer through the renderer, so this fails pre-fix and passes post-fix, with no timing or measure-cache dependency.
  • If the UI test is kept: make the current footer's measure genuinely dirty-able and observe a user-visible consequence (e.g. old footer A's invalidation causing footer B's frame/height to change), and assert via App.WaitForTextToBePresentInElement rather than a synchronous read.

FlyoutIsPresented = false;
await Task.Delay(250);

_currentFooter.ResetMeasureCount();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Test Coverage — ResetMeasureCount() is called after the flyout has been dismissed (FlyoutIsPresented = false on line 72), so the probe window is a period in which the flyout content is not on screen and no layout is being driven. This both suppresses the very re-measure the test wants to detect and makes the "0" result unfalsifiable. The reset should happen while the flyout is still presented and laid out, and the assertion should be taken against a state where a genuine re-measure would be observable.


_currentFooter = new MeasureProbeView("Footer B");
FlyoutFooter = _currentFooter;
await Task.Delay(250);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[moderate] Async and Threading Safety / Test Coverage — Four chained await Task.Delay(250) calls sequence the whole repro on wall-clock guesses. Per the repo's testing guidance, Task.Delay in tests needs justification; arbitrary waits are the classic source of UI-test flakiness on slower CI simulators (note the 488s vs 90s spread between the two Gate runs). Prefer deterministic synchronization — drive the state from the app side (await a TaskCompletionSource signalled from a layout/SizeChanged/Loaded callback, or WaitForMainThread()), and let the Appium side use App.WaitForTextToBePresentInElement for each stage transition rather than relying on a fixed 1s of accumulated sleeps.

@@ -0,0 +1,108 @@
namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 37187, "Shell flyout footer unsubscribes MeasureInvalidated when replaced", PlatformAffected.iOS | PlatformAffected.macOS)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[minor] Build & Formatting — Two mechanical issues that will trip the repo's formatting/whitespace checks: (a) this file (and TestCases.Shared.Tests/Tests/Issues/Issue37187.cs) has no trailing newline at EOF; (b) this file is indented with 4 spaces while the repo .editorconfig mandates tabs for C# (the companion shared-tests file correctly uses tabs). Also PlatformAffected.iOS | PlatformAffected.macOS on the [Issue] attribute should be PlatformAffected.iOS (the affected target is Mac Catalyst, not macOS/Cocoa) to match the #if IOS || MACCATALYST guard on the test.

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

AI Review Summary

@devanathan-vaithiyanathan — new AI review results are available based on commit e145e45.

Gate Partial Confidence High Platform iOS


🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix

Gate Result: ❌ FAILED

Platform: IOS · Base: main · Merge base: f27ca83a

🩺 Test does not reproduce the bug — ran the same in both states (PASS without fix, PASS with fix). The repro test is not exercising the issue. Strengthen the test before reviewing the fix.

Test Without Fix (expect FAIL) With Fix (expect PASS)
🖥️ Issue37187 Issue37187 ❌ PASS — 488s ✅ PASS — 90s
🔴 Without fix — 🖥️ Issue37187: PASS ❌ · 488s

(no coded error found; showing last 1200 chars)

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.05]   Discovering: Controls.TestCases.iOS.Tests
[xUnit.net 00:00:00.15]   Discovered:  Controls.TestCases.iOS.Tests
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll
   NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 8/10/2026 5:53:00 AM FixtureSetup for Issue37187(iOS)
>>>>> 8/10/2026 5:53:06 AM ReplacedFooterDoesNotInvalidateCurrentFooterMeasure Start
>>>>> 8/10/2026 5:53:09 AM ReplacedFooterDoesNotInvalidateCurrentFooterMeasure Stop
  Passed ReplacedFooterDoesNotInvalidateCurrentFooterMeasure [2 s]
NUnit Adapter 4.5.0.0: Test execution complete
Results File: /Users/cloudtest/vss/_work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue37187.trx
Test Run Successful.
Total tests: 1
     Passed: 1
 Total time: 1.3920 Minutes
>>> TRX_RESULT_FILE: /Users/cloudtest/vss/_work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue37187.trx
🟢 With fix — 🖥️ Issue37187: PASS ✅ · 90s

(no coded error found; showing last 1200 chars)

otal 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.04]   Discovering: Controls.TestCases.iOS.Tests
[xUnit.net 00:00:00.15]   Discovered:  Controls.TestCases.iOS.Tests
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll
   NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 8/10/2026 5:57:58 AM FixtureSetup for Issue37187(iOS)
>>>>> 8/10/2026 5:58:02 AM ReplacedFooterDoesNotInvalidateCurrentFooterMeasure Start
>>>>> 8/10/2026 5:58:04 AM ReplacedFooterDoesNotInvalidateCurrentFooterMeasure Stop
  Passed ReplacedFooterDoesNotInvalidateCurrentFooterMeasure [2 s]
NUnit Adapter 4.5.0.0: Test execution complete
Results File: /Users/cloudtest/vss/_work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue37187.trx
Test Run Successful.
Total tests: 1
     Passed: 1
 Total time: 20.0337 Seconds
>>> TRX_RESULT_FILE: /Users/cloudtest/vss/_work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue37187.trx

⚠️ Failure Details

  • ❌ Issue37187 PASSED without fix (should fail) — tests don't catch the bug
📁 Fix files reverted (1 files)
  • src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellFlyoutContentRenderer.cs

📋 Pre-Flight — Context & Validation

Issue: #37187 - [iOS] Replacing Shell.FlyoutFooter leaves the previous footer active
PR: #37254 - [iOS] Fix Unsubscribe replaced Shell flyout footer from MeasureInvalidated
Platforms Affected: iOS and Mac Catalyst; STEP 5a testing is restricted to iOS
Files Changed: 1 implementation, 2 UI-test files

Key Findings

  • ShellFlyoutContentRenderer.UpdateFlyoutFooter subscribes each installed footer to MeasureInvalidated; the pre-PR replacement path disconnects the old handler and native container without removing that renderer-owned subscription.
  • The PR's production fix is a direct -= on the old footer immediately before its handler is disconnected.
  • The added Issue37187 UI test is the detected primary test (Shell category). Gate verification passed both without and with the PR fix, so it does not currently prove the stale callback.
  • The test invalidates the removed footer and immediately reads the current footer's measure count. It does not establish that the removed footer can emit a fresh invalidation after replacement or wait for any resulting layout work.
  • Public API is unchanged. Public discussion contains no technical review comments or edge cases beyond iOS/Mac Catalyst applicability.
  • Regression cross-reference is clean: no recent labeled bug-fix overlap was detected.
  • No mandatory regression tests were supplied in the STEP 5a request.

Test Scope

Primary test only:

pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform ios -TestFilter "Issue37187"

Gate result is preserved at ../gate/content.md and must not be rerun or overwritten.

Code Review Summary

Verdict: SKIPPED
Confidence: N/A
Errors: 0 | Warnings: 0 | Suggestions: 0

The separate expert-review pass is intentionally deferred to STEP 5b by the bounded execution contract. No pre-flight reviewer agent was launched.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #37254 Directly unsubscribe the previous footer before disconnecting its handler Gate failed: test passed in both states ShellFlyoutContentRenderer.cs Original PR approach; alternatives must not repeat this direct replacement-path unsubscribe

🔬 Code Review — Deep Analysis

Expert PR Evaluation — PR #37254

Verdict: REQUEST CHANGES
Confidence: High in the findings; merge confidence remains low because the trusted Gate failed.

Production fix

The submitted production change is correct, minimal, and ordered safely. Removing the old footer's MeasureInvalidated handler before clearing its native container and handler restores replacement-path subscription symmetry and prevents a detached footer from remeasuring the newly installed footer.

The fix is incomplete for renderer teardown. ShellFlyoutContentRenderer subscribes to both the active footer's MeasureInvalidated event and Shell.PropertyChanged, but has no Dispose(bool) override that removes either subscription. The final footer can therefore retain a disposed renderer after Shell teardown or recreation. A consolidated reviewer candidate should retain the submitted replacement fix and add idempotent disposal cleanup.

Regression coverage

The trusted Gate proved the added Issue37187 UI test is non-discriminating: it passed both without and with the submitted fix. Its synchronous measure-count probe can remain zero because the current footer is measure-cached, the removed footer may no longer emit after handler disconnection, and layout work is not deterministically awaited. The fixed delays also make the scenario timing-sensitive.

The candidate should replace that probe with deterministic coverage that fails on the broken subscription lifecycle. If the existing UI-test surface cannot directly observe the subscription, a focused iOS/Mac Catalyst device test using a weak reference to the replaced footer is the preferred mechanism.

Inline findings

The expert reviewer produced six actionable findings in ../inline-findings.json: incomplete teardown cleanup, non-discriminating regression coverage, three specific weaknesses in the HostApp probe, and formatting/platform metadata issues.


🛠️ Try-Fix — Analysis & Comparison

Try-Fix Aggregate — PR #37254

Candidate Approach Result Findings Artifacts
1 Container-owned MeasureInvalidated subscription (UIContainerView owns subscribe/unsubscribe via Disconnect()) ✅ Pass (non-discriminating test) 3 (0 crit / 1 major / 1 mod / 1 minor) try-fix/attempt-1/, try-fix-1/content.md
2 Handler-lifecycle-coupled teardown (HandlerChanging removes subscriptions when the footer handler transitions to null) ✅ Pass (non-discriminating test) 1 (0 crit / 1 major / 0 mod / 0 minor) try-fix/attempt-2/, try-fix-2/content.md

Candidate 1

Try-Fix Candidate 1 — PR #37254 (iOS Shell FlyoutFooter MeasureInvalidated)

Result: ✅ Pass (non-discriminating — see evidence caveat)
Attempt artifacts: ../try-fix/attempt-1/
Self-review findings: 3 (0 critical, 1 major, 1 moderate, 1 minor)

Approach: Container-Owned MeasureInvalidated Subscription

Move ownership of the MeasureInvalidated subscription off the long-lived
ShellFlyoutContentRenderer and onto the short-lived UIContainerView that wraps the footer,
mirroring the pattern already used by UIContainerCell in the same folder.

  • UIContainerView subscribes to its own View.MeasureInvalidated in the constructor and
    re-subscribes idempotently in UpdatePlatformView(), re-raising through a new
    internal Action<UIContainerView> ViewMeasureInvalidated callback.
  • UIContainerView.Disconnect() — previously an empty method, already invoked by the footer
    replacement path (_footerView?.Disconnect()), by WillRemoveSubview, and by Dispose — now
    clears the callback and unsubscribes from View.MeasureInvalidated.
  • ShellFlyoutContentRenderer no longer subscribes to _footer.MeasureInvalidated at all; it
    assigns _footerView.ViewMeasureInvalidated = OnFooterMeasureInvalidated.

Why this is materially different from the PR

PR #37254 Candidate 1
Root-cause hypothesis "One -= is missing in the replacement path" "The subscription is owned by the wrong object — a per-Shell long-lived renderer subscribed to a replaceable per-instance footer with no lifecycle owner"
Mechanism Adds _footer.MeasureInvalidated -= OnFooterMeasureInvalidated; in UpdateFlyoutFooter Renderer never subscribes to the footer element; container's existing Disconnect() performs teardown
Files ShellFlyoutContentRenderer.cs ShellFlyoutContentRenderer.cs + UIContainerView.cs
Coverage Only the one patched replacement path Every path that already disconnects/disposes the container

There is no _footer.MeasureInvalidated -= ... anywhere in this candidate — it is not the PR's
-= moved to a nearby line. It also does not rely on ignoring callbacks by sender identity:
an earlier container != _footerView guard was deliberately removed so the fix rests solely on
real unsubscription.

Addresses stale callbacks: after replacement the removed footer has no live delegate, so
InvalidateMeasure() on it cannot reach ReMeasureFooter() against the current footer.

Addresses retention: the removed footer no longer holds a delegate targeting the long-lived
renderer. During its subscribed lifetime the delegate targets only the container that owns the
footer, and that reference is severed on Disconnect()/Dispose().

Files Changed

  • src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellFlyoutContentRenderer.cs (+2/-2)
  • src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/UIContainerView.cs (+22/-0)

No test files were changed (no test correction was required for this candidate).
No public API surface change (internal members only) — no PublicAPI.Unshipped.txt update needed.

Candidate Diff

diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellFlyoutContentRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellFlyoutContentRenderer.cs
index 906f7c1df6..aeb309205a 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellFlyoutContentRenderer.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellFlyoutContentRenderer.cs
@@ -152,11 +152,11 @@ namespace Microsoft.Maui.Controls.Platform.Compatibility
 			if (_footer is not null)
 			{
 				_footerView = new UIContainerView(_footer);
+				_footerView.ViewMeasureInvalidated = OnFooterMeasureInvalidated;
 				_uIViews[FooterIndex] = _footerView;
 				AddViewInCorrectOrder(_footerView, previousIndex);
 
 				_footerView.ClipsToBounds = true;
-				_footer.MeasureInvalidated += OnFooterMeasureInvalidated;
 
 				// Same approach as header — use Shell as context without mutating _footer.FlowDirection,
 				// so runtime Shell.FlowDirection changes continue to resolve correctly.
@@ -206,7 +206,7 @@ namespace Microsoft.Maui.Controls.Platform.Compatibility
 			View.AddSubview(newView);
 		}
 
-		void OnFooterMeasureInvalidated(object sender, System.EventArgs e)
+		void OnFooterMeasureInvalidated(UIContainerView container)
 		{
 			ReMeasureFooter();
 		}
diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/UIContainerView.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/UIContainerView.cs
index 7484b56266..b466b055be 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/UIContainerView.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/UIContainerView.cs
@@ -17,10 +17,18 @@ namespace Microsoft.Maui.Controls.Platform.Compatibility
 
 		internal event EventHandler HeaderSizeChanged;
 
+		// The container owns the MeasureInvalidated subscription for the view it wraps so the
+		// subscription lives and dies with the container, not with whoever created it. Consumers
+		// observe measure invalidation through this callback instead of subscribing to the view.
+		internal Action<UIContainerView> ViewMeasureInvalidated { get; set; }
+
 		public UIContainerView(View view)
 		{
 			_view = view;
 
+			if (_view is not null)
+				_view.MeasureInvalidated += OnViewMeasureInvalidated;
+
 			UpdatePlatformView();
 			ClipsToBounds = true;
 			MeasuredHeight = double.NaN;
@@ -29,6 +37,14 @@ namespace Microsoft.Maui.Controls.Platform.Compatibility
 
 		internal void UpdatePlatformView()
 		{
+			if (!_disposed && _view is not null)
+			{
+				// Idempotent: Disconnect() may have run (for example from WillRemoveSubview) before the
+				// container is reused with a refreshed platform view.
+				_view.MeasureInvalidated -= OnViewMeasureInvalidated;
+				_view.MeasureInvalidated += OnViewMeasureInvalidated;
+			}
+
 			_renderer = _view.ToHandler(_view.FindMauiContext());
 			_platformView = _view.ToPlatform();
 
@@ -135,8 +151,20 @@ namespace Microsoft.Maui.Controls.Platform.Compatibility
 			(_view as IView).Arrange(platformFrame);
 		}
 
+		void OnViewMeasureInvalidated(object sender, EventArgs e)
+		{
+			if (_disposed)
+				return;
+
+			ViewMeasureInvalidated?.Invoke(this);
+		}
+
 		internal void Disconnect()
 		{
+			ViewMeasureInvalidated = null;
+
+			if (_view is not null)
+				_view.MeasureInvalidated -= OnViewMeasureInvalidated;
 		}
 
 		protected override void Dispose(bool disposing)

Test

Exact command (the only test run):

pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform ios -TestFilter "Issue37187"

Result: ✅ PASS — Passed ReplacedFooterDoesNotInvalidateCurrentFooterMeasure, "All tests
passed", Result: SUCCESS. Compiled on the first pass; no correction iteration was needed.

🚨 Evidence caveat — the Pass is NON-DISCRIMINATING. The gate artifact
(../gate/content.md) records a definitive failure: Issue37187 PASSED both WITHOUT the fix
(488s) and WITH the fix (90s). A test that passes in the broken state cannot distinguish a
working fix from a no-op. This Pass therefore only proves the candidate builds, deploys, and
does not regress the exercised scenario. It is not evidence that the stale-subscription bug
is fixed. Correctness of this candidate rests on code reasoning, not on this test run — and the
same caveat applies equally to the PR's own fix.

Root cause of the non-discrimination: the test taps InvalidateOldFooter, which calls
_oldFooter.InvalidateMeasure() and then reads _currentFooter.MeasureCount synchronously in
the same handler
, never awaiting a layout pass. Even with a live stale subscription the
renderer's ReMeasureFooter() work would not have incremented the counter by the time the label
text is composed.

Inline Expert Self-Review

3 findings (0 critical, 1 major, 1 moderate, 1 minor) — full JSON at
../try-fix/attempt-1/reviewer-findings.json.

  1. [major] Regression Prevention (Overarching Principle 1/2) —
    ShellFlyoutContentRenderer.cs:155. The candidate ships without a discriminating regression
    test; the existing Issue37187 test was proven non-discriminating by the gate.
    NOT FIXED — STEP 5a explicitly forbids broadening test coverage and restricts execution to
    the single Issue37187 filter. Strengthening the repro (await a layout pass / assert on the
    removed footer's own re-measure) belongs to STEP 5b / the PR.
  2. [moderate] Regression Prevention / Handler Lifecycle — UIContainerView.cs:164.
    Disconnect() is reachable from WillRemoveSubview, so any subview removal (not just
    teardown) now drops the subscription. Mitigated by the idempotent re-subscribe added to
    UpdatePlatformView(), which the ShellPageRendererTracker title-view reuse path calls.
    The WillRemoveSubview → Disconnect() coupling is pre-existing; this change makes it
    observable.
  3. [minor] Complexity Reduction — ShellFlyoutContentRenderer.cs:209. The container
    callback parameter is unused; kept for signature parity with UIContainerCell.

No critical findings, so no pre-test correction batch was applied. Step 7.5 drift check reported
NO-DRIFT (the reviewed diff equals the final diff).

Failure / Blocker Analysis

No implementation failure or blocker. One environment obstacle was worked around:

EstablishBrokenBaseline.ps1 fails fast on a dirty tracked worktree, and the review harness had
~36 pre-existing modifications under .github/eng that must not be edited, reverted, or
cleaned. They were hidden from the index with git update-index --skip-worktree (file contents
untouched; no git checkout/restore/reset was used anywhere) so the baseline script could
run. The baseline reverted the single fix file to merge-base f27ca83a, confirmed by the
absence of the PR's -= line before the candidate was applied. The skip-worktree flags are
cleared after EstablishBrokenBaseline.ps1 -Restore.

Residual Risk

  • The only automated evidence available is non-discriminating; treat "Pass" as "no regression
    observed", not "bug proven fixed".
  • Candidate touches UIContainerView, which is shared by the flyout header
    (ShellFlyoutHeaderContainer) and the navigation-bar title view. Headers/title views now
    also carry a container-owned subscription with a null callback (no behavioral change), torn
    down on their existing Dispose()/Disconnect() paths. Broader Shell UI-test coverage was
    out of scope for this bounded STEP 5a run.

Candidate 2

Try-Fix Candidate 2 — PR #37254 (iOS Shell FlyoutFooter MeasureInvalidated)

Result: ✅ Pass (non-discriminating — see evidence caveat)
Attempt artifacts: ../try-fix/attempt-2/
Self-review findings: 1 (0 critical, 1 major, 0 moderate, 0 minor)

Approach: Handler-Lifecycle-Coupled Teardown

The renderer subscribes each installed footer to both MeasureInvalidated and the footer's own HandlerChanging event. When that exact footer's handler is about to transition to null, the lifecycle callback removes both event subscriptions before the handler disconnects. Cleanup is driven by the handler lifecycle already executed during replacement, not by an explicit cleanup instruction in UpdateFlyoutFooter.

Why this is materially different

PR #37254 Candidate 1 Candidate 2
Root strategy Replacement method directly unsubscribes the old footer Shared UIContainerView owns and re-raises the subscription, then cleans up in Disconnect() Footer handler teardown self-removes the renderer's narrowly scoped subscriptions
Cleanup trigger Explicit old-footer replacement block Container Disconnect() / disposal / WillRemoveSubview HandlerChanging with NewHandler == null
Shared-container impact None Changes UIContainerView used by footer, header, and title view None; UIContainerView is untouched

This is not the PR's -= moved within the replacement method: that method contains no subscription cleanup. It also does not repeat Candidate 1's container callback/Disconnect() ownership and avoids its shared-container and WillRemoveSubview risks. It performs actual unsubscription rather than merely ignoring stale senders.

Stale callbacks: setting the removed footer's handler to null synchronously detaches MeasureInvalidated, so later invalidations cannot invoke the renderer against the current footer.
Retention: the same lifecycle callback removes both delegates, so the removed footer no longer retains the renderer after handler teardown.

Files Changed

  • src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellFlyoutContentRenderer.cs (+10/-0)

No test or public API files changed.

Candidate Diff

diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellFlyoutContentRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellFlyoutContentRenderer.cs
index 906f7c1df6..0a5aa5a672 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellFlyoutContentRenderer.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellFlyoutContentRenderer.cs
@@ -156,6 +156,7 @@ namespace Microsoft.Maui.Controls.Platform.Compatibility
 				AddViewInCorrectOrder(_footerView, previousIndex);
 
 				_footerView.ClipsToBounds = true;
+				_footer.HandlerChanging += OnFooterHandlerChanging;
 				_footer.MeasureInvalidated += OnFooterMeasureInvalidated;
 
 				// Same approach as header — use Shell as context without mutating _footer.FlowDirection,
@@ -211,6 +212,15 @@ namespace Microsoft.Maui.Controls.Platform.Compatibility
 			ReMeasureFooter();
 		}
 
+		void OnFooterHandlerChanging(object sender, HandlerChangingEventArgs e)
+		{
+			if (e.NewHandler is not null || sender is not View footer)
+				return;
+
+			footer.HandlerChanging -= OnFooterHandlerChanging;
+			footer.MeasureInvalidated -= OnFooterMeasureInvalidated;
+		}
+
 		void ReMeasureFooter()
 		{
 			var size = _footerView?.SizeThatFits(new CGSize(View.Frame.Width, double.PositiveInfinity));

Test

Exact and only command:

pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform ios -TestFilter "Issue37187"

Result: ✅ PASS on the first implementation/test pass. ReplacedFooterDoesNotInvalidateCurrentFooterMeasure passed; the runner reported Total tests: 1, Passed: 1, All tests passed, and Result: SUCCESS. No focused correction or retest was needed.

Evidence caveat: this Pass is non-discriminating. The definitive gate result records that Issue37187 passed both without and with the PR fix. Therefore this result proves only that Candidate 2 builds, deploys, and does not regress the exercised scenario; it does not empirically distinguish the lifecycle teardown from broken behavior.

Inline Expert Self-Review

1 finding (0 critical, 1 major, 0 moderate/minor) — attempt-2/reviewer-findings.json.

  1. [major] Regression Prevention at ShellFlyoutContentRenderer.cs:159: the only permitted test is non-discriminating because it also passed in the broken gate state. A live stale subscription could still pass when the test reads the measure count synchronously before resulting layout work. This was not corrected because broadening/strengthening test coverage is outside this bounded Candidate 2 invocation.

The implementation was also checked for logic, handler lifecycle symmetry, memory retention, iOS/macCatalyst scope, Shell navigation behavior, backward compatibility, null safety, threading, and complexity. No other concrete finding was identified. Step 7.5 reported no diff drift after testing.

Failure / Blocker Analysis

No implementation failure or test blocker occurred. The mandatory baseline script initially required the review harness's pre-existing tracked .github/eng modifications to be hidden from status with skip-worktree flags. Their contents were preserved and were not edited, reverted, cleaned, or included in the candidate. The flags are cleared after mandatory restore.

Residual Risk

  • Automated evidence remains non-discriminating; correctness relies on the synchronous HandlerChanging contract and code-path reasoning.
  • Teardown is intentionally tied to Handler = null. The current replacement path performs that transition before DisconnectHandler; direct non-null handler replacement preserves the listener because the footer remains active.

📝 PR Finalize — Recommended Title & Description

Assessment: ✏️ Recommend updating — the current title is ungrammatical, and the description omits the submitted Issue37187 regression-test files.

Recommended title

[iOS] Shell: Unsubscribe replaced flyout footers from MeasureInvalidated

Recommended description

### Issue Details

On iOS and Mac Catalyst, `ShellFlyoutContentRenderer` subscribes to the flyout footer's `MeasureInvalidated` event when the footer is created. When `Shell.FlyoutFooter` is replaced, the old footer handler is disconnected, but its `MeasureInvalidated` subscription remains active.

### Root Cause

`ShellFlyoutContentRenderer.UpdateFlyoutFooter` adds the subscription when installing a footer. The replacement path previously disconnected the old footer without removing that subscription.

### Description of Change

- Unsubscribe the previous flyout footer from `MeasureInvalidated` before disconnecting its handler in `ShellFlyoutContentRenderer.cs`.
- Add `Issue37187` HostApp and Appium UI-test coverage that replaces footer A with footer B, invalidates the removed footer, and checks that the current footer is not remeasured.

### Issues Fixed

Fixes #37187

**Tested the behavior on the following platforms:**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac Catalyst

| Before | After |
|--------|-------|
| **iOS**<br><video src="https://github.com/user-attachments/assets/54d9f739-480c-4e37-bac7-3de20f9902c0" width="300" height="600"> | **iOS**<br><video src="https://github.com/user-attachments/assets/b1c1543a-ccba-4051-a6da-56b9f68c3072" width="300" height="600"> |

🏁 Report — Final Recommendation

⚠️ Final Recommendation: REQUEST CHANGES

Winner: pr-plus-reviewer

The submitted replacement-path unsubscribe is correct, but the trusted Gate failed because its regression test passed in both broken and fixed states. The expert review also found that renderer teardown leaves the active footer and Shell.PropertyChanged subscriptions attached. pr-plus-reviewer preserves the minimal submitted fix, closes teardown symmetry, and replaces the false-green test with a direct subscription assertion that passed the required iOS validation.

Comparative ranking

Rank Candidate Implementation assessment Regression evidence Decision
1 pr-plus-reviewer Keeps the precise replacement fix; adds disposal cleanup and required iOS/Mac Catalyst API entries. ✅ Revised Issue37187 passed in the required sandbox run. Its negative baseline was not rerun by contract, but the test first proves the callback is installed and then directly requires its removal. Winner. Best lifecycle coverage and strongest available test signal.
2 try-fix-2 Narrow handler-lifecycle cleanup avoids changing the shared container, but couples subscription correctness to HandlerChanging(NewHandler == null) and does not cover the renderer's Shell.PropertyChanged teardown. ✅ Candidate run passed, but the test was already proven non-discriminating. Safer than the shared-container alternative, but weaker and less directly evidenced than the winner.
3 try-fix-1 Moves ownership into UIContainerView, covering container disconnect/disposal but expanding behavior to a container shared by footer, header, and title-view paths. Its own review identified WillRemoveSubview/resubscription risk. ✅ Candidate run passed, but the test was already proven non-discriminating. Correct in principle, but unnecessarily broad for this bug.
4 pr The one-line unsubscribe correctly fixes footer replacement and is ordered before handler disconnection, but teardown remains asymmetric. ❌ Trusted Gate failed: the added test passed both without and with the fix. Production change is sound, but the submitted PR is not merge-ready with false-green coverage and unresolved expert feedback.

Expert findings reconciliation

  • Replacement behavior: the PR's line-level fix is correct and retained.
  • Teardown: addressed by removing both renderer-owned managed subscriptions in Dispose(bool).
  • Test correctness: addressed without the suggested weak-reference approach, because a publisher's event delegate retains the renderer, not the publisher itself. The winning test instead examines the actual renderer callback on the retained old footer.
  • Timing/formatting: fixed delays and the measure-count proxy were removed; tabs and final newlines were restored.
  • Platform metadata: PlatformAffected.macOS was retained because this test infrastructure uses it for Mac Catalyst throughout the HostApp suite.

The raw submitted PR therefore needs the pr-plus-reviewer changes before approval.


📱 UI Tests — Shell

Detected UI test categories: Shell

✅ Deep UI tests — 298 passed, 0 failed, 15 skipped across 1 category on platform-pool agent (replaces in-process counts above).

🧪 UI Test Execution Results (deep, platform pool)

Category Tests Snapshot diffs
Shell 298/313 (15 skipped) ✓ —
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs)

🧭 Next Steps — reviewer patch required (pr-plus-reviewer)

The reviewer-enhanced candidate won, so the submitted PR still needs those changes.

Why: The submitted replacement unsubscribe is correct, but pr-plus-reviewer also closes renderer-teardown subscriptions and replaces the false-green test with a deterministic delegate assertion. Its focused iOS validation passed, while the raw PR's trusted Gate failed.

Apply PRAgent/pr-plus-reviewer/reviewer.patch from the CopilotLogs
artifact (or follow the report's Required submitted-PR change), push the update, and run
the review again.

@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-gate-failed AI could not verify tests catch the bug s/agent-fix-win AI found a better alternative fix than the PR s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) and removed s/agent-review-in-progress AI review is currently running for this PR labels Aug 10, 2026
@kubaflo
kubaflo changed the base branch from main to inflight/current August 10, 2026 15:10
@kubaflo
kubaflo merged commit d241ee3 into dotnet:inflight/current Aug 10, 2026
2 of 11 checks passed
@github-actions github-actions Bot added this to the .NET 10 SR10 milestone Aug 10, 2026
kubaflo added a commit that referenced this pull request Aug 12, 2026
…dated (#37254)

<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->
### Issue Details
On iOS and Mac Catalyst, ShellFlyoutContentRenderer subscribes to the
flyout footer’s MeasureInvalidated event when the footer is created.
When Shell.FlyoutFooter is replaced, the old footer handler is
disconnected, but its MeasureInvalidated subscription remains active.

### Root Cause
The renderer adds this subscription when installing a footer. However,
the replacement path previously disconnected the footer without removing
the subscription.

### Description of Change

<!-- Enter description of the fix in this section -->
Unsubscribe the previous flyout footer from MeasureInvalidated before
disconnecting its handler in ShellFlyoutContentRenderer.cs:138:

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #37187 

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->
**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac


| Before  | After  |
|---------|--------|
| **iOS**<br> <video
src="https://github.com/user-attachments/assets/54d9f739-480c-4e37-bac7-3de20f9902c0"
width="300" height="600"> | **iOS**<br> <video
src="https://github.com/user-attachments/assets/b1c1543a-ccba-4051-a6da-56b9f68c3072"
width="300" height="600"> |

---------

Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
@sheiksyedm sheiksyedm modified the milestones: .NET 10 SR10, .NET 10 SR11 Aug 26, 2026
kubaflo added a commit that referenced this pull request Aug 27, 2026
…dated (#37254)

<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->
### Issue Details
On iOS and Mac Catalyst, ShellFlyoutContentRenderer subscribes to the
flyout footer’s MeasureInvalidated event when the footer is created.
When Shell.FlyoutFooter is replaced, the old footer handler is
disconnected, but its MeasureInvalidated subscription remains active.

### Root Cause
The renderer adds this subscription when installing a footer. However,
the replacement path previously disconnected the footer without removing
the subscription.

### Description of Change

<!-- Enter description of the fix in this section -->
Unsubscribe the previous flyout footer from MeasureInvalidated before
disconnecting its handler in ShellFlyoutContentRenderer.cs:138:

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #37187 

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->
**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac


| Before  | After  |
|---------|--------|
| **iOS**<br> <video
src="https://github.com/user-attachments/assets/54d9f739-480c-4e37-bac7-3de20f9902c0"
width="300" height="600"> | **iOS**<br> <video
src="https://github.com/user-attachments/assets/b1c1543a-ccba-4051-a6da-56b9f68c3072"
width="300" height="600"> |

---------

Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
kubaflo added a commit that referenced this pull request Sep 2, 2026
…dated (#37254)

<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->
### Issue Details
On iOS and Mac Catalyst, ShellFlyoutContentRenderer subscribes to the
flyout footer’s MeasureInvalidated event when the footer is created.
When Shell.FlyoutFooter is replaced, the old footer handler is
disconnected, but its MeasureInvalidated subscription remains active.

### Root Cause
The renderer adds this subscription when installing a footer. However,
the replacement path previously disconnected the footer without removing
the subscription.

### Description of Change

<!-- Enter description of the fix in this section -->
Unsubscribe the previous flyout footer from MeasureInvalidated before
disconnecting its handler in ShellFlyoutContentRenderer.cs:138:

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #37187 

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->
**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac


| Before  | After  |
|---------|--------|
| **iOS**<br> <video
src="https://github.com/user-attachments/assets/54d9f739-480c-4e37-bac7-3de20f9902c0"
width="300" height="600"> | **iOS**<br> <video
src="https://github.com/user-attachments/assets/b1c1543a-ccba-4051-a6da-56b9f68c3072"
width="300" height="600"> |

---------

Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
kubaflo added a commit that referenced this pull request Sep 3, 2026
…dated (#37254)

<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->
### Issue Details
On iOS and Mac Catalyst, ShellFlyoutContentRenderer subscribes to the
flyout footer’s MeasureInvalidated event when the footer is created.
When Shell.FlyoutFooter is replaced, the old footer handler is
disconnected, but its MeasureInvalidated subscription remains active.

### Root Cause
The renderer adds this subscription when installing a footer. However,
the replacement path previously disconnected the footer without removing
the subscription.

### Description of Change

<!-- Enter description of the fix in this section -->
Unsubscribe the previous flyout footer from MeasureInvalidated before
disconnecting its handler in ShellFlyoutContentRenderer.cs:138:

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #37187 

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->
**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac


| Before  | After  |
|---------|--------|
| **iOS**<br> <video
src="https://github.com/user-attachments/assets/54d9f739-480c-4e37-bac7-3de20f9902c0"
width="300" height="600"> | **iOS**<br> <video
src="https://github.com/user-attachments/assets/b1c1543a-ccba-4051-a6da-56b9f68c3072"
width="300" height="600"> |

---------

Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
kubaflo added a commit that referenced this pull request Sep 3, 2026
…dated (#37254)

<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->
### Issue Details
On iOS and Mac Catalyst, ShellFlyoutContentRenderer subscribes to the
flyout footer’s MeasureInvalidated event when the footer is created.
When Shell.FlyoutFooter is replaced, the old footer handler is
disconnected, but its MeasureInvalidated subscription remains active.

### Root Cause
The renderer adds this subscription when installing a footer. However,
the replacement path previously disconnected the footer without removing
the subscription.

### Description of Change

<!-- Enter description of the fix in this section -->
Unsubscribe the previous flyout footer from MeasureInvalidated before
disconnecting its handler in ShellFlyoutContentRenderer.cs:138:

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #37187 

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->
**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac


| Before  | After  |
|---------|--------|
| **iOS**<br> <video
src="https://github.com/user-attachments/assets/54d9f739-480c-4e37-bac7-3de20f9902c0"
width="300" height="600"> | **iOS**<br> <video
src="https://github.com/user-attachments/assets/b1c1543a-ccba-4051-a6da-56b9f68c3072"
width="300" height="600"> |

---------

Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
kubaflo added a commit that referenced this pull request Sep 4, 2026
…dated (#37254)

<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->
### Issue Details
On iOS and Mac Catalyst, ShellFlyoutContentRenderer subscribes to the
flyout footer’s MeasureInvalidated event when the footer is created.
When Shell.FlyoutFooter is replaced, the old footer handler is
disconnected, but its MeasureInvalidated subscription remains active.

### Root Cause
The renderer adds this subscription when installing a footer. However,
the replacement path previously disconnected the footer without removing
the subscription.

### Description of Change

<!-- Enter description of the fix in this section -->
Unsubscribe the previous flyout footer from MeasureInvalidated before
disconnecting its handler in ShellFlyoutContentRenderer.cs:138:

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #37187 

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->
**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac


| Before  | After  |
|---------|--------|
| **iOS**<br> <video
src="https://github.com/user-attachments/assets/54d9f739-480c-4e37-bac7-3de20f9902c0"
width="300" height="600"> | **iOS**<br> <video
src="https://github.com/user-attachments/assets/b1c1543a-ccba-4051-a6da-56b9f68c3072"
width="300" height="600"> |

---------

Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
kubaflo added a commit that referenced this pull request Sep 6, 2026
…dated (#37254)

<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->
### Issue Details
On iOS and Mac Catalyst, ShellFlyoutContentRenderer subscribes to the
flyout footer’s MeasureInvalidated event when the footer is created.
When Shell.FlyoutFooter is replaced, the old footer handler is
disconnected, but its MeasureInvalidated subscription remains active.

### Root Cause
The renderer adds this subscription when installing a footer. However,
the replacement path previously disconnected the footer without removing
the subscription.

### Description of Change

<!-- Enter description of the fix in this section -->
Unsubscribe the previous flyout footer from MeasureInvalidated before
disconnecting its handler in ShellFlyoutContentRenderer.cs:138:

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #37187 

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->
**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac


| Before  | After  |
|---------|--------|
| **iOS**<br> <video
src="https://github.com/user-attachments/assets/54d9f739-480c-4e37-bac7-3de20f9902c0"
width="300" height="600"> | **iOS**<br> <video
src="https://github.com/user-attachments/assets/b1c1543a-ccba-4051-a6da-56b9f68c3072"
width="300" height="600"> |

---------

Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
kubaflo added a commit that referenced this pull request Sep 7, 2026
…dated (#37254)

<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->
### Issue Details
On iOS and Mac Catalyst, ShellFlyoutContentRenderer subscribes to the
flyout footer’s MeasureInvalidated event when the footer is created.
When Shell.FlyoutFooter is replaced, the old footer handler is
disconnected, but its MeasureInvalidated subscription remains active.

### Root Cause
The renderer adds this subscription when installing a footer. However,
the replacement path previously disconnected the footer without removing
the subscription.

### Description of Change

<!-- Enter description of the fix in this section -->
Unsubscribe the previous flyout footer from MeasureInvalidated before
disconnecting its handler in ShellFlyoutContentRenderer.cs:138:

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #37187 

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->
**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac


| Before  | After  |
|---------|--------|
| **iOS**<br> <video
src="https://github.com/user-attachments/assets/54d9f739-480c-4e37-bac7-3de20f9902c0"
width="300" height="600"> | **iOS**<br> <video
src="https://github.com/user-attachments/assets/b1c1543a-ccba-4051-a6da-56b9f68c3072"
width="300" height="600"> |

---------

Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
kubaflo added a commit that referenced this pull request Sep 7, 2026
…dated (#37254)

<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->
### Issue Details
On iOS and Mac Catalyst, ShellFlyoutContentRenderer subscribes to the
flyout footer’s MeasureInvalidated event when the footer is created.
When Shell.FlyoutFooter is replaced, the old footer handler is
disconnected, but its MeasureInvalidated subscription remains active.

### Root Cause
The renderer adds this subscription when installing a footer. However,
the replacement path previously disconnected the footer without removing
the subscription.

### Description of Change

<!-- Enter description of the fix in this section -->
Unsubscribe the previous flyout footer from MeasureInvalidated before
disconnecting its handler in ShellFlyoutContentRenderer.cs:138:

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #37187 

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->
**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac


| Before  | After  |
|---------|--------|
| **iOS**<br> <video
src="https://github.com/user-attachments/assets/54d9f739-480c-4e37-bac7-3de20f9902c0"
width="300" height="600"> | **iOS**<br> <video
src="https://github.com/user-attachments/assets/b1c1543a-ccba-4051-a6da-56b9f68c3072"
width="300" height="600"> |

---------

Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
kubaflo added a commit that referenced this pull request Sep 8, 2026
…dated (#37254)

<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->
### Issue Details
On iOS and Mac Catalyst, ShellFlyoutContentRenderer subscribes to the
flyout footer’s MeasureInvalidated event when the footer is created.
When Shell.FlyoutFooter is replaced, the old footer handler is
disconnected, but its MeasureInvalidated subscription remains active.

### Root Cause
The renderer adds this subscription when installing a footer. However,
the replacement path previously disconnected the footer without removing
the subscription.

### Description of Change

<!-- Enter description of the fix in this section -->
Unsubscribe the previous flyout footer from MeasureInvalidated before
disconnecting its handler in ShellFlyoutContentRenderer.cs:138:

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #37187 

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->
**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac


| Before  | After  |
|---------|--------|
| **iOS**<br> <video
src="https://github.com/user-attachments/assets/54d9f739-480c-4e37-bac7-3de20f9902c0"
width="300" height="600"> | **iOS**<br> <video
src="https://github.com/user-attachments/assets/b1c1543a-ccba-4051-a6da-56b9f68c3072"
width="300" height="600"> |

---------

Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
kubaflo added a commit that referenced this pull request Sep 8, 2026
…dated (#37254)

<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->
### Issue Details
On iOS and Mac Catalyst, ShellFlyoutContentRenderer subscribes to the
flyout footer’s MeasureInvalidated event when the footer is created.
When Shell.FlyoutFooter is replaced, the old footer handler is
disconnected, but its MeasureInvalidated subscription remains active.

### Root Cause
The renderer adds this subscription when installing a footer. However,
the replacement path previously disconnected the footer without removing
the subscription.

### Description of Change

<!-- Enter description of the fix in this section -->
Unsubscribe the previous flyout footer from MeasureInvalidated before
disconnecting its handler in ShellFlyoutContentRenderer.cs:138:

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #37187 

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->
**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac


| Before  | After  |
|---------|--------|
| **iOS**<br> <video
src="https://github.com/user-attachments/assets/54d9f739-480c-4e37-bac7-3de20f9902c0"
width="300" height="600"> | **iOS**<br> <video
src="https://github.com/user-attachments/assets/b1c1543a-ccba-4051-a6da-56b9f68c3072"
width="300" height="600"> |

---------

Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
kubaflo added a commit that referenced this pull request Sep 9, 2026
…dated (#37254)

<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->
### Issue Details
On iOS and Mac Catalyst, ShellFlyoutContentRenderer subscribes to the
flyout footer’s MeasureInvalidated event when the footer is created.
When Shell.FlyoutFooter is replaced, the old footer handler is
disconnected, but its MeasureInvalidated subscription remains active.

### Root Cause
The renderer adds this subscription when installing a footer. However,
the replacement path previously disconnected the footer without removing
the subscription.

### Description of Change

<!-- Enter description of the fix in this section -->
Unsubscribe the previous flyout footer from MeasureInvalidated before
disconnecting its handler in ShellFlyoutContentRenderer.cs:138:

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #37187 

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->
**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac


| Before  | After  |
|---------|--------|
| **iOS**<br> <video
src="https://github.com/user-attachments/assets/54d9f739-480c-4e37-bac7-3de20f9902c0"
width="300" height="600"> | **iOS**<br> <video
src="https://github.com/user-attachments/assets/b1c1543a-ccba-4051-a6da-56b9f68c3072"
width="300" height="600"> |

---------

Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
kubaflo added a commit that referenced this pull request Sep 9, 2026
…dated (#37254)

<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->
### Issue Details
On iOS and Mac Catalyst, ShellFlyoutContentRenderer subscribes to the
flyout footer’s MeasureInvalidated event when the footer is created.
When Shell.FlyoutFooter is replaced, the old footer handler is
disconnected, but its MeasureInvalidated subscription remains active.

### Root Cause
The renderer adds this subscription when installing a footer. However,
the replacement path previously disconnected the footer without removing
the subscription.

### Description of Change

<!-- Enter description of the fix in this section -->
Unsubscribe the previous flyout footer from MeasureInvalidated before
disconnecting its handler in ShellFlyoutContentRenderer.cs:138:

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes #37187 

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->
**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac


| Before  | After  |
|---------|--------|
| **iOS**<br> <video
src="https://github.com/user-attachments/assets/54d9f739-480c-4e37-bac7-3de20f9902c0"
width="300" height="600"> | **iOS**<br> <video
src="https://github.com/user-attachments/assets/b1c1543a-ccba-4051-a6da-56b9f68c3072"
width="300" height="600"> |

---------

Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 25, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-shell community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration platform/ios s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-win AI found a better alternative fix than the PR s/agent-gate-failed AI could not verify tests catch the bug s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) shell-flyout

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[iOS] Replacing Shell.FlyoutFooter leaves the previous footer active

5 participants