From 321d5b98051eabdce25b84dc1e4184501ebadb9c Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Thu, 1 Jan 2026 22:07:10 -0300 Subject: [PATCH 01/13] clean up the DetailView --- src/Core/src/Platform/Android/Navigation/ScopedFragment.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Core/src/Platform/Android/Navigation/ScopedFragment.cs b/src/Core/src/Platform/Android/Navigation/ScopedFragment.cs index 163c6c2d92dd..bce0ac610df7 100644 --- a/src/Core/src/Platform/Android/Navigation/ScopedFragment.cs +++ b/src/Core/src/Platform/Android/Navigation/ScopedFragment.cs @@ -36,6 +36,8 @@ public override void OnDestroy() { base.OnDestroy(); IsDestroyed = true; + + DetailView = null!; } } } From 0607dfc9737ff74f3b9e484b01d6fe479c9c698e Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Thu, 1 Jan 2026 22:10:01 -0300 Subject: [PATCH 02/13] calling Disconnect handler when removing the FlyoutPage.Detail --- src/Controls/src/Core/FlyoutPage/FlyoutPage.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Controls/src/Core/FlyoutPage/FlyoutPage.cs b/src/Controls/src/Core/FlyoutPage/FlyoutPage.cs index c2570bdfb132..550b8c906e63 100644 --- a/src/Controls/src/Core/FlyoutPage/FlyoutPage.cs +++ b/src/Controls/src/Core/FlyoutPage/FlyoutPage.cs @@ -76,6 +76,7 @@ public Page Detail { previousDetail.SendNavigatedFrom( new NavigatedFromEventArgs(destinationPage: value, NavigationType.Replace)); + previousDetail.Handler?.DisconnectHandler(); } _detail.SendNavigatedTo(new NavigatedToEventArgs(previousDetail, NavigationType.Replace)); From 03024a9ac2068a2c714476d84d8742df1efbfbfc Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Thu, 1 Jan 2026 23:33:14 -0300 Subject: [PATCH 03/13] fix ScopedFragment leak --- .../Navigation/NavigationViewFragment.cs | 1 + .../Navigation/StackNavigationManager.cs | 44 ++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs b/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs index 496990dee1a7..a5fd1a999391 100644 --- a/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs +++ b/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs @@ -89,6 +89,7 @@ public override void OnDestroy() { _currentView = null; _fragmentContainerView = null; + _navigationManager = null; base.OnDestroy(); } diff --git a/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs b/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs index 546d319a6213..5f2f2ed581d7 100644 --- a/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs +++ b/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Android.Content; using Android.OS; using Android.Views; @@ -310,13 +311,44 @@ public virtual void Disconnect() _fragmentContainerView.ChildViewAdded -= OnNavigationHostViewAdded; } + if (_fragmentManager is not null) + { + CleanUpFragments(_fragmentManager); + } + _fragmentLifecycleCallbacks?.Disconnect(); _fragmentLifecycleCallbacks = null; - + VirtualView = null; NavigationView = null; SetNavHost(null); _fragmentNavigator = null; + _fragmentManager = null; + _fragmentContainerView = null; + _navGraph = null; + _currentPage = null; + NavigationStack = []; + ActiveRequestedArgs = null; + OnResumeRequestedArgs = null; + } + + + static void CleanUpFragments(FragmentManager fragmentManager) + { + fragmentManager.ExecutePendingTransactionsEx(); + ; if (fragmentManager.BackStackEntryCount > 0) + { + fragmentManager.PopBackStackImmediate(); + } + + var transaction = fragmentManager.BeginTransactionEx(); + foreach (var fragment in fragmentManager.Fragments) + { + transaction.RemoveEx(fragment); + } + + transaction.CommitAllowingStateLoss(); + fragmentManager.ExecutePendingTransactionsEx(); } public virtual void Connect(IView navigationView) @@ -344,6 +376,14 @@ public virtual void Connect(IView navigationView) } } + +#pragma warning disable RS0016 + ~StackNavigationManager() +#pragma warning restore RS0016 + { + System.Diagnostics.Debug.WriteLine($"~StackNavigationManager called"); + } + void OnNavigationPlatformViewAttachedToWindow(object? sender, AView.ViewAttachedToWindowEventArgs e) { // If the previous Navigation Host Fragment was destroyed then we need to add a new one @@ -505,7 +545,7 @@ void SetNavHost(NavHostFragment? navHost) (FragmentNavigator)NavController .NavigatorProvider .GetNavigator(Java.Lang.Class.FromType(typeof(FragmentNavigator))); - + foreach (var fragment in _navHost.ChildFragmentManager.Fragments) { if (fragment is NavigationViewFragment nvf) From 240f17c0c05394c2a3d60ff85be9d67603b6d45e Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Thu, 1 Jan 2026 23:42:17 -0300 Subject: [PATCH 04/13] remove and dispose the NavigationViewFragment --- .../src/Platform/Android/Navigation/NavigationViewFragment.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs b/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs index a5fd1a999391..92425541210e 100644 --- a/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs +++ b/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs @@ -92,6 +92,7 @@ public override void OnDestroy() _navigationManager = null; base.OnDestroy(); + this.Dispose(); } public override Animation OnCreateAnimation(int transit, bool enter, int nextAnim) From 5d3fd8f295a18cee5aae763bd2aa98a0c9531b38 Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Fri, 2 Jan 2026 00:32:47 -0300 Subject: [PATCH 05/13] call Dispose in MauiNavHostFragment --- .../src/Platform/Android/Navigation/MauiNavHostFragment.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Core/src/Platform/Android/Navigation/MauiNavHostFragment.cs b/src/Core/src/Platform/Android/Navigation/MauiNavHostFragment.cs index 29ff73d8e85a..425dabc48bee 100644 --- a/src/Core/src/Platform/Android/Navigation/MauiNavHostFragment.cs +++ b/src/Core/src/Platform/Android/Navigation/MauiNavHostFragment.cs @@ -18,5 +18,11 @@ public MauiNavHostFragment() protected MauiNavHostFragment(nint javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) { } + + public override void OnDestroy() + { + base.OnDestroy(); + this.Dispose(); + } } } From bedfb8073f231378c64132e7b1bc81182aa7db5b Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Fri, 2 Jan 2026 00:32:57 -0300 Subject: [PATCH 06/13] code style --- .../Platform/Android/Navigation/StackNavigationManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs b/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs index 5f2f2ed581d7..ea92db105637 100644 --- a/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs +++ b/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs @@ -310,7 +310,7 @@ public virtual void Disconnect() _fragmentContainerView.ViewAttachedToWindow -= OnNavigationPlatformViewAttachedToWindow; _fragmentContainerView.ChildViewAdded -= OnNavigationHostViewAdded; } - + if (_fragmentManager is not null) { CleanUpFragments(_fragmentManager); @@ -336,7 +336,7 @@ public virtual void Disconnect() static void CleanUpFragments(FragmentManager fragmentManager) { fragmentManager.ExecutePendingTransactionsEx(); - ; if (fragmentManager.BackStackEntryCount > 0) + if (fragmentManager.BackStackEntryCount > 0) { fragmentManager.PopBackStackImmediate(); } @@ -347,7 +347,7 @@ static void CleanUpFragments(FragmentManager fragmentManager) transaction.RemoveEx(fragment); } - transaction.CommitAllowingStateLoss(); + transaction.CommitNowAllowingStateLoss(); fragmentManager.ExecutePendingTransactionsEx(); } From 9d70c6477ad306c0a2cd902479b131c1ff7f80c2 Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Fri, 2 Jan 2026 00:39:36 -0300 Subject: [PATCH 07/13] Add unit test --- .../tests/DeviceTests/Memory/MemoryTests.cs | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs index 1601f63be7ec..775ffe8f3258 100644 --- a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs +++ b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs @@ -577,6 +577,95 @@ await CreateHandlerAndAddToWindow(window, async () => await AssertionExtensions.WaitForGC([.. references]); } + [Fact("FlyoutPage Detail Does Not Leak When Replaced")] + public async Task FlyoutPageDetailDoesNotLeak() + { + SetupBuilder(); + + var references = new List(); + var flyoutPage = new FlyoutPage + { + Flyout = new ContentPage { Title = "Flyout" } + }; + + await CreateHandlerAndAddToWindow(new Window(flyoutPage), async () => + { + await OnLoadedAsync(flyoutPage); + + // Create and set first detail page + var detailPage1 = new ContentPage { Title = "Detail 1" }; + var navPage1 = new NavigationPage(detailPage1); + flyoutPage.Detail = navPage1; + + await OnLoadedAsync(detailPage1); + + references.Add(new(navPage1)); + references.Add(new(navPage1.Handler)); + references.Add(new(navPage1.Handler.PlatformView)); + references.Add(new(detailPage1)); + references.Add(new(detailPage1.Handler)); + references.Add(new(detailPage1.Handler.PlatformView)); + + // Replace with second detail page + var detailPage2 = new ContentPage { Title = "Detail 2" }; + var navPage2 = new NavigationPage(detailPage2); + flyoutPage.Detail = navPage2; + + await OnLoadedAsync(detailPage2); + + // The old detail page and navigation page should be collected + navPage1 = null; + detailPage1 = null; + }); + + await AssertionExtensions.WaitForGC([.. references]); + } + + [Fact("FlyoutPage Detail Does Not Leak With Multiple Replacements")] + public async Task FlyoutPageDetailDoesNotLeakWithMultipleReplacements() + { + SetupBuilder(); + + var references = new List(); + var flyoutPage = new FlyoutPage + { + Flyout = new ContentPage { Title = "Flyout" } + }; + + await CreateHandlerAndAddToWindow(new Window(flyoutPage), async () => + { + await OnLoadedAsync(flyoutPage); + + // Simulate multiple replacements similar to the Sandbox scenario + for (int i = 0; i < 5; i++) + { + var detailPage = new ContentPage { Title = $"Detail {i}" }; + var navPage = new NavigationPage(detailPage); + + flyoutPage.Detail = navPage; + await OnLoadedAsync(detailPage); + + // Track references for first 3 iterations (they should be collected) + if (i < 3) + { + references.Add(new(navPage)); + references.Add(new(navPage.Handler)); + references.Add(new(navPage.Handler.PlatformView)); + references.Add(new(detailPage)); + references.Add(new(detailPage.Handler)); + references.Add(new(detailPage.Handler.PlatformView)); + } + + // Small delay to simulate real usage + await Task.Delay(50); + } + + // After loop, the last 2 detail pages are still active, but first 3 should be collected + }); + + await AssertionExtensions.WaitForGC([.. references]); + } + [Fact("VisualDiagnosticsOverlay Does Not Leak" #if IOS || MACCATALYST , Skip = "Fails with 'MauiContext should have been set on parent.'" From 04a8f3079367cda216052e1ba02e07733a17050e Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Fri, 2 Jan 2026 00:40:49 -0300 Subject: [PATCH 08/13] remove comments --- src/Controls/tests/DeviceTests/Memory/MemoryTests.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs index 775ffe8f3258..50b9647e1046 100644 --- a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs +++ b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs @@ -592,7 +592,6 @@ await CreateHandlerAndAddToWindow(new Window(flyoutPage), async () => { await OnLoadedAsync(flyoutPage); - // Create and set first detail page var detailPage1 = new ContentPage { Title = "Detail 1" }; var navPage1 = new NavigationPage(detailPage1); flyoutPage.Detail = navPage1; @@ -606,14 +605,12 @@ await CreateHandlerAndAddToWindow(new Window(flyoutPage), async () => references.Add(new(detailPage1.Handler)); references.Add(new(detailPage1.Handler.PlatformView)); - // Replace with second detail page var detailPage2 = new ContentPage { Title = "Detail 2" }; var navPage2 = new NavigationPage(detailPage2); flyoutPage.Detail = navPage2; await OnLoadedAsync(detailPage2); - // The old detail page and navigation page should be collected navPage1 = null; detailPage1 = null; }); @@ -636,7 +633,6 @@ await CreateHandlerAndAddToWindow(new Window(flyoutPage), async () => { await OnLoadedAsync(flyoutPage); - // Simulate multiple replacements similar to the Sandbox scenario for (int i = 0; i < 5; i++) { var detailPage = new ContentPage { Title = $"Detail {i}" }; @@ -645,7 +641,6 @@ await CreateHandlerAndAddToWindow(new Window(flyoutPage), async () => flyoutPage.Detail = navPage; await OnLoadedAsync(detailPage); - // Track references for first 3 iterations (they should be collected) if (i < 3) { references.Add(new(navPage)); @@ -656,11 +651,9 @@ await CreateHandlerAndAddToWindow(new Window(flyoutPage), async () => references.Add(new(detailPage.Handler.PlatformView)); } - // Small delay to simulate real usage await Task.Delay(50); } - // After loop, the last 2 detail pages are still active, but first 3 should be collected }); await AssertionExtensions.WaitForGC([.. references]); From 6792b088c3d67efd8b10af56fa092bd529317510 Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Fri, 2 Jan 2026 00:56:50 -0300 Subject: [PATCH 09/13] unit test --- src/Controls/tests/DeviceTests/Memory/MemoryTests.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs index 50b9647e1046..85281cb84f41 100644 --- a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs +++ b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs @@ -85,6 +85,7 @@ void SetupBuilder() handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); + handlers.AddHandler(); #if IOS || MACCATALYST handlers.AddHandler(); @@ -585,7 +586,8 @@ public async Task FlyoutPageDetailDoesNotLeak() var references = new List(); var flyoutPage = new FlyoutPage { - Flyout = new ContentPage { Title = "Flyout" } + Flyout = new ContentPage { Title = "Flyout" }, + Detail = new ContentPage { Title = "Detail" } }; await CreateHandlerAndAddToWindow(new Window(flyoutPage), async () => @@ -595,7 +597,7 @@ await CreateHandlerAndAddToWindow(new Window(flyoutPage), async () => var detailPage1 = new ContentPage { Title = "Detail 1" }; var navPage1 = new NavigationPage(detailPage1); flyoutPage.Detail = navPage1; - + await OnLoadedAsync(detailPage1); references.Add(new(navPage1)); @@ -626,7 +628,8 @@ public async Task FlyoutPageDetailDoesNotLeakWithMultipleReplacements() var references = new List(); var flyoutPage = new FlyoutPage { - Flyout = new ContentPage { Title = "Flyout" } + Flyout = new ContentPage { Title = "Flyout" }, + Detail = new ContentPage { Title = "Detail" } }; await CreateHandlerAndAddToWindow(new Window(flyoutPage), async () => From 73d7d4a3ae3a719a36c23422d8a16458296a183b Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Fri, 2 Jan 2026 01:00:41 -0300 Subject: [PATCH 10/13] remove finalizer --- .../Platform/Android/Navigation/StackNavigationManager.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs b/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs index ea92db105637..db783f8b2f34 100644 --- a/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs +++ b/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs @@ -375,14 +375,6 @@ public virtual void Connect(IView navigationView) _fragmentContainerView.ChildViewAdded += OnNavigationHostViewAdded; } } - - -#pragma warning disable RS0016 - ~StackNavigationManager() -#pragma warning restore RS0016 - { - System.Diagnostics.Debug.WriteLine($"~StackNavigationManager called"); - } void OnNavigationPlatformViewAttachedToWindow(object? sender, AView.ViewAttachedToWindowEventArgs e) { From 41ccb0f7f8e425de2e39dd0e7721fd5f0d310d11 Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Fri, 2 Jan 2026 23:21:51 -0300 Subject: [PATCH 11/13] commit suggestions from copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/Platform/Android/Navigation/StackNavigationManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs b/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs index db783f8b2f34..07f4719f77e5 100644 --- a/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs +++ b/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; + using Android.Content; using Android.OS; using Android.Views; @@ -336,7 +336,7 @@ public virtual void Disconnect() static void CleanUpFragments(FragmentManager fragmentManager) { fragmentManager.ExecutePendingTransactionsEx(); - if (fragmentManager.BackStackEntryCount > 0) + while (fragmentManager.BackStackEntryCount > 0) { fragmentManager.PopBackStackImmediate(); } From 6e0ceabd861d5646018e39ebe46234b76cd1b73e Mon Sep 17 00:00:00 2001 From: Shane Date: Wed, 4 Mar 2026 17:16:58 -0600 Subject: [PATCH 12/13] Rewrite FlyoutPage tests to check handler disconnection deterministically The original tests put WaitForGC outside CreateHandlerAndAddToWindow, where window.Destroying() already disconnected all handlers. The new tests check Assert.Null(oldDetail.Handler) inside the callback while the window is still alive, making them deterministic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/DeviceTests/Memory/MemoryTests.cs | 69 ++++++++++--------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs index 85281cb84f41..fc11465d48f3 100644 --- a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs +++ b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs @@ -578,7 +578,7 @@ await CreateHandlerAndAddToWindow(window, async () => await AssertionExtensions.WaitForGC([.. references]); } - [Fact("FlyoutPage Detail Does Not Leak When Replaced")] + [Fact("FlyoutPage Detail Handler Is Disconnected When Replaced")] public async Task FlyoutPageDetailDoesNotLeak() { SetupBuilder(); @@ -587,40 +587,39 @@ public async Task FlyoutPageDetailDoesNotLeak() var flyoutPage = new FlyoutPage { Flyout = new ContentPage { Title = "Flyout" }, - Detail = new ContentPage { Title = "Detail" } + Detail = new NavigationPage(new ContentPage { Title = "Initial Detail" }) }; await CreateHandlerAndAddToWindow(new Window(flyoutPage), async () => { await OnLoadedAsync(flyoutPage); - var detailPage1 = new ContentPage { Title = "Detail 1" }; - var navPage1 = new NavigationPage(detailPage1); - flyoutPage.Detail = navPage1; - - await OnLoadedAsync(detailPage1); + // Capture old detail and its handler before replacement + var oldDetail = flyoutPage.Detail; + var oldHandler = oldDetail.Handler; + Assert.NotNull(oldHandler); - references.Add(new(navPage1)); - references.Add(new(navPage1.Handler)); - references.Add(new(navPage1.Handler.PlatformView)); - references.Add(new(detailPage1)); - references.Add(new(detailPage1.Handler)); - references.Add(new(detailPage1.Handler.PlatformView)); + references.Add(new(oldDetail)); + references.Add(new(oldHandler)); + references.Add(new(oldHandler.PlatformView)); - var detailPage2 = new ContentPage { Title = "Detail 2" }; - var navPage2 = new NavigationPage(detailPage2); - flyoutPage.Detail = navPage2; + // Replace detail + var newDetailPage = new ContentPage { Title = "New Detail" }; + flyoutPage.Detail = new NavigationPage(newDetailPage); + await OnLoadedAsync(newDetailPage); - await OnLoadedAsync(detailPage2); + // The fix calls previousDetail.Handler?.DisconnectHandler() in the Detail setter. + // Without the fix, the old handler stays connected (non-null). + Assert.Null(oldDetail.Handler); - navPage1 = null; - detailPage1 = null; + oldDetail = null; + oldHandler = null; }); await AssertionExtensions.WaitForGC([.. references]); } - [Fact("FlyoutPage Detail Does Not Leak With Multiple Replacements")] + [Fact("FlyoutPage Detail Handler Disconnects With Multiple Replacements")] public async Task FlyoutPageDetailDoesNotLeakWithMultipleReplacements() { SetupBuilder(); @@ -629,7 +628,7 @@ public async Task FlyoutPageDetailDoesNotLeakWithMultipleReplacements() var flyoutPage = new FlyoutPage { Flyout = new ContentPage { Title = "Flyout" }, - Detail = new ContentPage { Title = "Detail" } + Detail = new NavigationPage(new ContentPage { Title = "Initial Detail" }) }; await CreateHandlerAndAddToWindow(new Window(flyoutPage), async () => @@ -638,25 +637,27 @@ await CreateHandlerAndAddToWindow(new Window(flyoutPage), async () => for (int i = 0; i < 5; i++) { - var detailPage = new ContentPage { Title = $"Detail {i}" }; - var navPage = new NavigationPage(detailPage); - - flyoutPage.Detail = navPage; - await OnLoadedAsync(detailPage); + var oldDetail = flyoutPage.Detail; + var oldHandler = oldDetail.Handler; + Assert.NotNull(oldHandler); if (i < 3) { - references.Add(new(navPage)); - references.Add(new(navPage.Handler)); - references.Add(new(navPage.Handler.PlatformView)); - references.Add(new(detailPage)); - references.Add(new(detailPage.Handler)); - references.Add(new(detailPage.Handler.PlatformView)); + references.Add(new(oldDetail)); + references.Add(new(oldHandler)); + references.Add(new(oldHandler.PlatformView)); } - await Task.Delay(50); - } + var newDetailPage = new ContentPage { Title = $"Detail {i}" }; + flyoutPage.Detail = new NavigationPage(newDetailPage); + await OnLoadedAsync(newDetailPage); + + // Each replacement must disconnect the previous detail's handler + Assert.Null(oldDetail.Handler); + oldDetail = null; + oldHandler = null; + } }); await AssertionExtensions.WaitForGC([.. references]); From eea1e4d889ac7bdc6816b478a02f66c6cdd1b0a8 Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Sun, 8 Mar 2026 19:31:11 -0300 Subject: [PATCH 13/13] sandbox test (to be reverted) --- .../Controls.Sample.Sandbox/App.xaml.cs | 12 +-- .../AppFlyoutPage.xaml | 23 ++++ .../AppFlyoutPage.xaml.cs | 42 ++++++++ .../Controls.Sample.Sandbox/Page1.xaml | 19 ++++ .../Controls.Sample.Sandbox/Page1.xaml.cs | 29 +++++ .../Controls.Sample.Sandbox/Page2.xaml | 18 ++++ .../Controls.Sample.Sandbox/Page2.xaml.cs | 19 ++++ .../Services/NavigationService.cs | 102 ++++++++++++++++++ 8 files changed, 253 insertions(+), 11 deletions(-) create mode 100644 src/Controls/samples/Controls.Sample.Sandbox/AppFlyoutPage.xaml create mode 100644 src/Controls/samples/Controls.Sample.Sandbox/AppFlyoutPage.xaml.cs create mode 100644 src/Controls/samples/Controls.Sample.Sandbox/Page1.xaml create mode 100644 src/Controls/samples/Controls.Sample.Sandbox/Page1.xaml.cs create mode 100644 src/Controls/samples/Controls.Sample.Sandbox/Page2.xaml create mode 100644 src/Controls/samples/Controls.Sample.Sandbox/Page2.xaml.cs create mode 100644 src/Controls/samples/Controls.Sample.Sandbox/Services/NavigationService.cs diff --git a/src/Controls/samples/Controls.Sample.Sandbox/App.xaml.cs b/src/Controls/samples/Controls.Sample.Sandbox/App.xaml.cs index 9512dea98e39..9dc08b9101db 100644 --- a/src/Controls/samples/Controls.Sample.Sandbox/App.xaml.cs +++ b/src/Controls/samples/Controls.Sample.Sandbox/App.xaml.cs @@ -9,16 +9,6 @@ public App() protected override Window CreateWindow(IActivationState? activationState) { - // To test shell scenarios, change this to true - bool useShell = false; - - if (!useShell) - { - return new Window(new NavigationPage(new MainPage())); - } - else - { - return new Window(new SandboxShell()); - } + return new Window(new AppFlyoutPage()); } } diff --git a/src/Controls/samples/Controls.Sample.Sandbox/AppFlyoutPage.xaml b/src/Controls/samples/Controls.Sample.Sandbox/AppFlyoutPage.xaml new file mode 100644 index 000000000000..d43d0013bfc7 --- /dev/null +++ b/src/Controls/samples/Controls.Sample.Sandbox/AppFlyoutPage.xaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + diff --git a/src/Controls/samples/Controls.Sample.Sandbox/AppFlyoutPage.xaml.cs b/src/Controls/samples/Controls.Sample.Sandbox/AppFlyoutPage.xaml.cs new file mode 100644 index 000000000000..a0fac3aae8ee --- /dev/null +++ b/src/Controls/samples/Controls.Sample.Sandbox/AppFlyoutPage.xaml.cs @@ -0,0 +1,42 @@ +using Maui.Controls.Sample.Services; + +namespace Maui.Controls.Sample; + +public partial class AppFlyoutPage : FlyoutPage +{ + public class MenuItem + { + public string Title { get; set; } = string.Empty; + public Type PageType { get; set; } = typeof(Page); + } + + public List MenuItems { get; set; } + + public AppFlyoutPage() + { + InitializeComponent(); + + MenuItems = new List + { + new MenuItem { Title = "page 1", PageType = typeof(Page1) }, + new MenuItem { Title = "page 2", PageType = typeof(Page2) } + }; + + BindingContext = this; + + // Set default detail page + Detail = new NavigationPage(new Page1()); + } + + private void OnMenuItemSelected(object? sender, SelectionChangedEventArgs e) + { + if (e.CurrentSelection.FirstOrDefault() is MenuItem selectedItem) + { + var navigationService = new NavigationService(); + navigationService.Navigate(selectedItem.PageType); + + // Close the flyout after selection + IsPresented = false; + } + } +} diff --git a/src/Controls/samples/Controls.Sample.Sandbox/Page1.xaml b/src/Controls/samples/Controls.Sample.Sandbox/Page1.xaml new file mode 100644 index 000000000000..5a186b6b4ad2 --- /dev/null +++ b/src/Controls/samples/Controls.Sample.Sandbox/Page1.xaml @@ -0,0 +1,19 @@ + + + +