From 0f882b427e015a7fc955fbf50a9b0b5324b13a5e Mon Sep 17 00:00:00 2001 From: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com> Date: Tue, 6 Jan 2026 15:22:24 +0530 Subject: [PATCH 1/9] Fixed - Shell replace navigation leaks current page --- .../Shell/Android/ShellContentFragment.cs | 19 +++++++++++++++++-- .../Shell/Android/ShellItemRendererBase.cs | 10 ++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs index ad0f11feeada..d079fee8bd69 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs @@ -1,5 +1,6 @@ #nullable disable using System; +using System.Collections.Generic; using Android.OS; using Android.Runtime; using Android.Views; @@ -23,6 +24,8 @@ public class ShellContentFragment : Fragment, AndroidAnimation.IAnimationListene // of creating a fragment bool _isAnimating = false; + static readonly Dictionary _pageFragmentMap = new Dictionary(); + #region IAnimationListener void AndroidAnimation.IAnimationListener.OnAnimationEnd(AndroidAnimation animation) @@ -91,6 +94,11 @@ public ShellContentFragment(IShellContext shellContext, Page page) public Fragment Fragment => this; + internal static bool TryGetFragment(Page page, out ShellContentFragment fragment) + { + return _pageFragmentMap.TryGetValue(page, out fragment); + } + public override AndroidAnimation OnCreateAnimation(int transit, bool enter, int nextAnim) { var result = base.OnCreateAnimation(transit, enter, nextAnim); @@ -163,6 +171,7 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, _appearanceTracker = _shellContext.CreateToolbarAppearanceTracker(); ((IShellController)_shellContext.Shell).AddAppearanceObserver(this, _page); + _pageFragmentMap[_page] = this; if (_shellPageContainer.LayoutParameters is CoordinatorLayout.LayoutParams layoutParams) layoutParams.Behavior = new AppBarLayout.ScrollingViewBehavior(); @@ -170,13 +179,18 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, return _root; } - void Destroy() + internal void Destroy() { if (_destroyed) return; _destroyed = true; + if (_page is not null) + { + _pageFragmentMap.Remove(_page); + } + // If the user taps very quickly on back button multiple times to pop a page, // the app enters background state in the middle of the animation causing the fragment to be destroyed without completing the animation. // That'll cause `IAnimationListener.onAnimationEnd` to not be called, so we need to call it manually if something is still subscribed to the event @@ -194,7 +208,6 @@ void Destroy() if (_shellContent != null) { ((IShellContentController)_shellContent).RecyclePage(_page); - _page.Handler = null; } if (_shellPageContainer != null) @@ -220,6 +233,8 @@ void Destroy() _viewhandler = null; _shellContent = null; _shellPageContainer = null; + _page?.DisconnectHandlers(); + _page = null; } protected override void Dispose(bool disposing) diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs index 3efd98363373..0b90c5d37a38 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs @@ -171,6 +171,8 @@ protected virtual Task HandleFragmentUpdate(ShellNavigationSource navSourc if (!isForCurrentTab && removeFragment != _currentFragment) return Task.FromResult(true); + + DisposePage(page); break; case ShellNavigationSource.PopToRoot: @@ -428,5 +430,13 @@ void RemoveFragment(Fragment fragment) t.RemoveEx(fragment); t.CommitAllowingStateLossEx(); } + + void DisposePage(Page page) + { + if (ShellContentFragment.TryGetFragment(page, out var shellFragment)) + { + shellFragment.Destroy(); + } + } } } \ No newline at end of file From 5b11b5ece58fb89efc03f19f00ea12ed7bf72cde Mon Sep 17 00:00:00 2001 From: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com> Date: Tue, 6 Jan 2026 17:22:14 +0530 Subject: [PATCH 2/9] Update ShellContentFragment.cs --- .../Handlers/Shell/Android/ShellContentFragment.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs index d079fee8bd69..5803da9933c6 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs @@ -189,6 +189,8 @@ internal void Destroy() if (_page is not null) { _pageFragmentMap.Remove(_page); + _page.DisconnectHandlers(); + _page = null; } // If the user taps very quickly on back button multiple times to pop a page, @@ -208,6 +210,7 @@ internal void Destroy() if (_shellContent != null) { ((IShellContentController)_shellContent).RecyclePage(_page); + _page.Handler = null; } if (_shellPageContainer != null) @@ -233,8 +236,6 @@ internal void Destroy() _viewhandler = null; _shellContent = null; _shellPageContainer = null; - _page?.DisconnectHandlers(); - _page = null; } protected override void Dispose(bool disposing) From c97c0975d962c51abfc22322a6dbc6ef0d2a0a0e Mon Sep 17 00:00:00 2001 From: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:04:55 +0530 Subject: [PATCH 3/9] Update ShellContentFragment.cs --- .../Handlers/Shell/Android/ShellContentFragment.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs index 5803da9933c6..a7a658f9b07e 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs @@ -186,13 +186,6 @@ internal void Destroy() _destroyed = true; - if (_page is not null) - { - _pageFragmentMap.Remove(_page); - _page.DisconnectHandlers(); - _page = null; - } - // If the user taps very quickly on back button multiple times to pop a page, // the app enters background state in the middle of the animation causing the fragment to be destroyed without completing the animation. // That'll cause `IAnimationListener.onAnimationEnd` to not be called, so we need to call it manually if something is still subscribed to the event @@ -228,6 +221,12 @@ internal void Destroy() _toolbarTracker?.Dispose(); _appearanceTracker?.Dispose(); + if (_page is not null) + { + _pageFragmentMap.Remove(_page); + _page.DisconnectHandlers(); + _page = null; + } _appearanceTracker = null; _toolbarTracker = null; From 8c4f58acd101c5a3f6ffe20b2670d17d285c83c8 Mon Sep 17 00:00:00 2001 From: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com> Date: Wed, 7 Jan 2026 10:28:38 +0530 Subject: [PATCH 4/9] Update ShellItemRendererBase.cs --- .../Handlers/Shell/Android/ShellItemRendererBase.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs index 0b90c5d37a38..31a82c4a7959 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs @@ -167,12 +167,13 @@ protected virtual Task HandleFragmentUpdate(ShellNavigationSource navSourc if (ChildFragmentManager.Contains(removeFragment.Fragment) && !isForCurrentTab && removeFragment != _currentFragment) RemoveFragment(removeFragment.Fragment); _fragmentMap.Remove(page); + + DisposePage(page); } if (!isForCurrentTab && removeFragment != _currentFragment) return Task.FromResult(true); - DisposePage(page); break; case ShellNavigationSource.PopToRoot: From f99c84a24528852cba1f033c7c11d522dc2d6da9 Mon Sep 17 00:00:00 2001 From: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:10:14 +0530 Subject: [PATCH 5/9] changes updated. --- .../Shell/Android/ShellContentFragment.cs | 28 ++++++++----------- .../Shell/Android/ShellItemRendererBase.cs | 13 +++------ 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs index a7a658f9b07e..406fae03f66e 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs @@ -24,8 +24,6 @@ public class ShellContentFragment : Fragment, AndroidAnimation.IAnimationListene // of creating a fragment bool _isAnimating = false; - static readonly Dictionary _pageFragmentMap = new Dictionary(); - #region IAnimationListener void AndroidAnimation.IAnimationListener.OnAnimationEnd(AndroidAnimation animation) @@ -94,11 +92,6 @@ public ShellContentFragment(IShellContext shellContext, Page page) public Fragment Fragment => this; - internal static bool TryGetFragment(Page page, out ShellContentFragment fragment) - { - return _pageFragmentMap.TryGetValue(page, out fragment); - } - public override AndroidAnimation OnCreateAnimation(int transit, bool enter, int nextAnim) { var result = base.OnCreateAnimation(transit, enter, nextAnim); @@ -171,7 +164,6 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, _appearanceTracker = _shellContext.CreateToolbarAppearanceTracker(); ((IShellController)_shellContext.Shell).AddAppearanceObserver(this, _page); - _pageFragmentMap[_page] = this; if (_shellPageContainer.LayoutParameters is CoordinatorLayout.LayoutParams layoutParams) layoutParams.Behavior = new AppBarLayout.ScrollingViewBehavior(); @@ -179,7 +171,7 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, return _root; } - internal void Destroy() + void Destroy() { if (_destroyed) return; @@ -221,13 +213,6 @@ internal void Destroy() _toolbarTracker?.Dispose(); _appearanceTracker?.Dispose(); - if (_page is not null) - { - _pageFragmentMap.Remove(_page); - _page.DisconnectHandlers(); - _page = null; - } - _appearanceTracker = null; _toolbarTracker = null; _toolbar = null; @@ -237,6 +222,17 @@ internal void Destroy() _shellPageContainer = null; } + internal void DisposePage() + { + Destroy(); + + if (_page is not null) + { + _page.DisconnectHandlers(); + _page = null; + } + } + protected override void Dispose(bool disposing) { if (_disposed) diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs index 31a82c4a7959..f04125c2a3df 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs @@ -168,7 +168,10 @@ protected virtual Task HandleFragmentUpdate(ShellNavigationSource navSourc RemoveFragment(removeFragment.Fragment); _fragmentMap.Remove(page); - DisposePage(page); + if (removeFragment is ShellContentFragment shellFragment) + { + shellFragment.DisposePage(); + } } if (!isForCurrentTab && removeFragment != _currentFragment) @@ -431,13 +434,5 @@ void RemoveFragment(Fragment fragment) t.RemoveEx(fragment); t.CommitAllowingStateLossEx(); } - - void DisposePage(Page page) - { - if (ShellContentFragment.TryGetFragment(page, out var shellFragment)) - { - shellFragment.Destroy(); - } - } } } \ No newline at end of file From 5b89b9d13c9bf985247d8f2273297d81ee432935 Mon Sep 17 00:00:00 2001 From: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:12:27 +0530 Subject: [PATCH 6/9] remove unwanted changes. --- .../Handlers/Shell/Android/ShellContentFragment.cs | 2 +- .../Handlers/Shell/Android/ShellItemRendererBase.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs index 406fae03f66e..c757127c51b7 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs @@ -1,6 +1,5 @@ #nullable disable using System; -using System.Collections.Generic; using Android.OS; using Android.Runtime; using Android.Views; @@ -213,6 +212,7 @@ void Destroy() _toolbarTracker?.Dispose(); _appearanceTracker?.Dispose(); + _appearanceTracker = null; _toolbarTracker = null; _toolbar = null; diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs index f04125c2a3df..63cce3dfdcfa 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs @@ -176,7 +176,6 @@ protected virtual Task HandleFragmentUpdate(ShellNavigationSource navSourc if (!isForCurrentTab && removeFragment != _currentFragment) return Task.FromResult(true); - break; case ShellNavigationSource.PopToRoot: From 03ca7a64f13e6ba977acdcc9e5a20d9772c6dd53 Mon Sep 17 00:00:00 2001 From: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:49:12 +0530 Subject: [PATCH 7/9] added test. --- .../TestCases.HostApp/Issues/Issue25134.cs | 114 ++++++++++++++++++ .../Tests/Issues/Issue25134.cs | 28 +++++ 2 files changed, 142 insertions(+) create mode 100644 src/Controls/tests/TestCases.HostApp/Issues/Issue25134.cs create mode 100644 src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue25134.cs diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue25134.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue25134.cs new file mode 100644 index 000000000000..982fb476fcdf --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue25134.cs @@ -0,0 +1,114 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 25134, "Shell retains page references when replacing a page", PlatformAffected.Android)] +public class Issue25134 : Shell +{ + public Issue25134() + { + var mainContent = new ShellContent + { + ContentTemplate = new DataTemplate(() => new Issue25134InitialPage()), + Title = "Initial", + Route = "initial" + }; + + Items.Add(mainContent); + Routing.RegisterRoute("Issue25134_child", typeof(Issue25134ChildPage)); + Routing.RegisterRoute("Issue25134_replace", typeof(Issue25134ReplacePage)); + } +} + +public class Issue25134InitialPage : ContentPage +{ + public WeakReference ChildPageReference { get; set; } + + public Issue25134InitialPage() + { + Title = "Initial Page"; + + var goToChildButton = new Button + { + Text = "Go to child page", + AutomationId = "goToChildPage", + VerticalOptions = LayoutOptions.Start, + Command = new Command(() => Shell.Current.GoToAsync("Issue25134_child")) + }; + + Content = new VerticalStackLayout + { + goToChildButton + }; + } +} + +public class Issue25134ChildPage : ContentPage +{ + public Issue25134ChildPage() + { + Title = "Child Page"; + + var button = new Button + { + Text = "Replace", + AutomationId = "replace", + VerticalOptions = LayoutOptions.Start, + Command = new Command(() => Shell.Current.GoToAsync("../Issue25134_replace")) + }; + + Content = new VerticalStackLayout + { + button + }; + } + + protected override void OnParentSet() + { + base.OnParentSet(); + + if (Parent is ShellSection section) + { + var initialPage = (section.CurrentItem as IShellContentController).Page as Issue25134InitialPage; + initialPage!.ChildPageReference = new WeakReference(this); + } + } +} + +public class Issue25134ReplacePage : ContentPage +{ + public Issue25134ReplacePage() + { + Title = "Replace Page"; + + Button checkRefButton = null; + checkRefButton = new Button + { + Text = "Check reference", + AutomationId = "checkReference", + VerticalOptions = LayoutOptions.Start, + Command = new Command(async () => + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + await Task.Yield(); + + var section = (ShellSection)Parent; + var initialPage = (Issue25134InitialPage)(section.CurrentItem as IShellContentController).Page; + checkRefButton.Text = initialPage.ChildPageReference.TryGetTarget(out var page) ? "alive" : "gone"; + }) + }; + + var backButton = new Button + { + Text = "Go back", + AutomationId = "goBack", + VerticalOptions = LayoutOptions.Start, + Command = new Command(() => Shell.Current.GoToAsync("..")) + }; + + Content = new VerticalStackLayout + { + checkRefButton, + backButton + }; + } +} diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue25134.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue25134.cs new file mode 100644 index 000000000000..f69291b767f9 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue25134.cs @@ -0,0 +1,28 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue25134 : _IssuesUITest +{ + public Issue25134(TestDevice testDevice) : base(testDevice) + { + } + + public override string Issue => "Shell retains page references when replacing a page"; + + [Test] + [Category(UITestCategories.Shell)] + public void ShellReplaceDisposesPage() + { + App.WaitForElement("goToChildPage"); + App.Tap("goToChildPage"); + App.WaitForElement("replace"); + App.Tap("replace"); + var button = App.WaitForElement("checkReference"); + App.Tap("checkReference"); + Assert.That(button.GetText(), Is.EqualTo("gone")); + } +} + From 5cf0c7f138937bd8fe455e36116d991dea619001 Mon Sep 17 00:00:00 2001 From: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:50:43 +0530 Subject: [PATCH 8/9] test updated --- src/Controls/tests/TestCases.HostApp/Issues/Issue25134.cs | 2 +- .../tests/TestCases.Shared.Tests/Tests/Issues/Issue25134.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue25134.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue25134.cs index 982fb476fcdf..5dfc5191fe47 100644 --- a/src/Controls/tests/TestCases.HostApp/Issues/Issue25134.cs +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue25134.cs @@ -111,4 +111,4 @@ public Issue25134ReplacePage() backButton }; } -} +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue25134.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue25134.cs index f69291b767f9..c472c30a3fe5 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue25134.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue25134.cs @@ -24,5 +24,4 @@ public void ShellReplaceDisposesPage() App.Tap("checkReference"); Assert.That(button.GetText(), Is.EqualTo("gone")); } -} - +} \ No newline at end of file From fb88c5a6d3a543d8370d64f440a0373dcb6e510f Mon Sep 17 00:00:00 2001 From: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:57:05 +0530 Subject: [PATCH 9/9] Update ShellContentFragment.cs --- .../Handlers/Shell/Android/ShellContentFragment.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs index c757127c51b7..c3179651a5ca 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs @@ -224,6 +224,10 @@ void Destroy() internal void DisposePage() { + if (_destroyed) + { + return; + } Destroy(); if (_page is not null)