diff --git a/src/Controls/src/Core/HandlerImpl/Window/Window.Impl.cs b/src/Controls/src/Core/HandlerImpl/Window/Window.Impl.cs index 305fef65c818..c2a30fb16c8e 100644 --- a/src/Controls/src/Core/HandlerImpl/Window/Window.Impl.cs +++ b/src/Controls/src/Core/HandlerImpl/Window/Window.Impl.cs @@ -616,6 +616,7 @@ void OnPageChanged(Page? oldPage, Page? newPage) { if (oldPage != null) { + _menuBarTracker.Target = null; InternalChildren.Remove(oldPage); oldPage.HandlerChanged -= OnPageHandlerChanged; oldPage.HandlerChanging -= OnPageHandlerChanging; diff --git a/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.Windows.cs b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.Windows.cs index 87ab469ce799..8b1b54e28763 100644 --- a/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.Windows.cs +++ b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.Windows.cs @@ -40,10 +40,10 @@ Task SetupWindowForTests(IWindow window, Func runTests, IMauiCon } finally { - window.Handler.DisconnectHandler(); + window.Handler?.DisconnectHandler(); await Task.Delay(10); newWindow?.Close(); - appStub.Handler.DisconnectHandler(); + appStub.Handler?.DisconnectHandler(); } }); } diff --git a/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs b/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs index a161651674f6..77cc6e9eb48a 100644 --- a/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs @@ -33,6 +33,7 @@ void SetupBuilder() handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); + handlers.AddHandler(); }); }); } @@ -287,5 +288,47 @@ await CreateHandlerAndAddToWindow(new Window(navPage), async }; }); } + + [Fact(DisplayName = "NavigationPage Does Not Leak")] + public async Task DoesNotLeak() + { + SetupBuilder(); + WeakReference pageReference = null; + var navPage = new NavigationPage(new ContentPage { Title = "Page 1" }); + + await CreateHandlerAndAddToWindow(new Window(navPage), async (handler) => + { + var page = new ContentPage { Title = "Page 2" }; + pageReference = new WeakReference(page); + await navPage.Navigation.PushAsync(page); + await navPage.Navigation.PopAsync(); + }); + + await Task.Yield(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + + Assert.NotNull(pageReference); + Assert.False(pageReference.IsAlive, "Page should not be alive!"); + } + + [Fact(DisplayName = "Can Reuse Pages")] + public async Task CanReusePages() + { + SetupBuilder(); + var navPage = new NavigationPage(new ContentPage { Title = "Page 1" }); + var reusedPage = new ContentPage + { + Content = new Label() + }; + + await CreateHandlerAndAddToWindow(new Window(navPage), async (handler) => + { + await navPage.Navigation.PushAsync(reusedPage); + await navPage.Navigation.PopAsync(); + await navPage.Navigation.PushAsync(reusedPage); + await OnLoadedAsync(reusedPage.Content); + }); + } } } \ No newline at end of file diff --git a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs index 319c0698c329..4f9c662d01ed 100644 --- a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs @@ -660,7 +660,7 @@ await CreateHandlerAndAddToWindow(shell, async (handler) => }); } -#if !IOS +#if !IOS && !MACCATALYST [Fact] public async Task ChangingToNewMauiContextDoesntCrash() { @@ -917,6 +917,62 @@ await CreateHandlerAndAddToWindow(new Window(navPage), async } #endif + [Fact(DisplayName = "Pages Do Not Leak")] + public async Task PagesDoNotLeak() + { + SetupBuilder(); + var shell = await CreateShellAsync(shell => + { + shell.CurrentItem = new ContentPage() { Title = "Page 1" }; + }); + + WeakReference pageReference = null; + + await CreateHandlerAndAddToWindow(shell, async (handler) => + { + await OnLoadedAsync(shell.CurrentPage); + + var page = new ContentPage { Title = "Page 2" }; + pageReference = new WeakReference(page); + + await shell.Navigation.PushAsync(page); + await shell.Navigation.PopAsync(); + }); + + // 3 GCs were required in Android API 23, 2 worked otherwise + for (int i = 0; i < 3; i++) + { + await Task.Yield(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + + Assert.NotNull(pageReference); + Assert.False(pageReference.IsAlive, "Page should not be alive!"); + } + + [Fact(DisplayName = "Can Reuse Pages")] + public async Task CanReusePages() + { + SetupBuilder(); + var rootPage = new ContentPage(); + var shell = await CreateShellAsync(shell => + { + shell.CurrentItem = rootPage; + }); + var reusedPage = new ContentPage + { + Content = new Label { Text = "HEY" } + }; + await CreateHandlerAndAddToWindow(shell, async (handler) => + { + await shell.Navigation.PushAsync(reusedPage); + await shell.Navigation.PopAsync(); + await shell.Navigation.PushAsync(reusedPage); + await OnLoadedAsync(reusedPage.Content); + }); + } + protected Task CreateShellAsync(Action action) => InvokeOnMainThreadAsync(() => { diff --git a/src/Core/src/Handlers/ContentView/ContentViewHandler.Android.cs b/src/Core/src/Handlers/ContentView/ContentViewHandler.Android.cs index 61d5c550319e..45f89dba74ca 100644 --- a/src/Core/src/Handlers/ContentView/ContentViewHandler.Android.cs +++ b/src/Core/src/Handlers/ContentView/ContentViewHandler.Android.cs @@ -52,6 +52,8 @@ public static void MapContent(IContentViewHandler handler, IContentView page) protected override void DisconnectHandler(ContentViewGroup platformView) { // If we're being disconnected from the xplat element, then we should no longer be managing its children + platformView.CrossPlatformMeasure = null; + platformView.CrossPlatformArrange = null; platformView.RemoveAllViews(); base.DisconnectHandler(platformView); } diff --git a/src/Core/src/Handlers/ContentView/ContentViewHandler.Windows.cs b/src/Core/src/Handlers/ContentView/ContentViewHandler.Windows.cs index b56b6cfc28ed..b1303431bafc 100644 --- a/src/Core/src/Handlers/ContentView/ContentViewHandler.Windows.cs +++ b/src/Core/src/Handlers/ContentView/ContentViewHandler.Windows.cs @@ -49,5 +49,12 @@ public static void MapContent(IContentViewHandler handler, IContentView page) { UpdateContent(handler); } + + protected override void DisconnectHandler(ContentPanel platformView) + { + platformView.CrossPlatformMeasure = null; + platformView.CrossPlatformArrange = null; + base.DisconnectHandler(platformView); + } } } diff --git a/src/Core/src/Handlers/ContentView/ContentViewHandler.iOS.cs b/src/Core/src/Handlers/ContentView/ContentViewHandler.iOS.cs index 9bebb97dec91..2a076b28fd2f 100644 --- a/src/Core/src/Handlers/ContentView/ContentViewHandler.iOS.cs +++ b/src/Core/src/Handlers/ContentView/ContentViewHandler.iOS.cs @@ -53,5 +53,13 @@ public static void MapContent(IContentViewHandler handler, IContentView page) { UpdateContent(handler); } + + protected override void DisconnectHandler(ContentView platformView) + { + platformView.CrossPlatformMeasure = null; + platformView.CrossPlatformArrange = null; + platformView.RemoveFromSuperview(); + base.DisconnectHandler(platformView); + } } } diff --git a/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs b/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs index f8df45ab8851..97a25c1e7954 100644 --- a/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs +++ b/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs @@ -78,6 +78,14 @@ public override void OnResume() base.OnResume(); } + public override void OnDestroy() + { + _currentView = null; + _fragmentContainerView = null; + + base.OnDestroy(); + } + public override Animation OnCreateAnimation(int transit, bool enter, int nextAnim) { int id = 0; diff --git a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt index aab2ae924bc8..a440b184d5f6 100644 --- a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -11,6 +11,7 @@ override Microsoft.Maui.Handlers.RadioButtonHandler.PlatformArrange(Microsoft.Ma override Microsoft.Maui.Handlers.ShapeViewHandler.GetDesiredSize(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size override Microsoft.Maui.Layouts.FlexBasis.Equals(object? obj) -> bool override Microsoft.Maui.Layouts.FlexBasis.GetHashCode() -> int +override Microsoft.Maui.Platform.NavigationViewFragment.OnDestroy() -> void override Microsoft.Maui.SizeRequest.Equals(object? obj) -> bool override Microsoft.Maui.SizeRequest.GetHashCode() -> int static Microsoft.Maui.FontSize.operator !=(Microsoft.Maui.FontSize left, Microsoft.Maui.FontSize right) -> bool diff --git a/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt index e99859a464d0..ba7714750fe1 100644 --- a/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -9,6 +9,7 @@ Microsoft.Maui.LifecycleEvents.iOSLifecycle.PerformFetch Microsoft.Maui.SizeRequest.Equals(Microsoft.Maui.SizeRequest other) -> bool Microsoft.Maui.Platform.MauiScrollView Microsoft.Maui.Platform.MauiScrollView.MauiScrollView() -> void +override Microsoft.Maui.Handlers.ContentViewHandler.DisconnectHandler(Microsoft.Maui.Platform.ContentView! platformView) -> void override Microsoft.Maui.Handlers.SwipeItemButton.Frame.get -> CoreGraphics.CGRect override Microsoft.Maui.Handlers.SwipeItemButton.Frame.set -> void override Microsoft.Maui.Handlers.SwipeItemMenuItemHandler.ConnectHandler(UIKit.UIButton! platformView) -> void diff --git a/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 00927d94a484..87539f3a96d5 100644 --- a/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -8,6 +8,7 @@ Microsoft.Maui.Layouts.FlexBasis.Equals(Microsoft.Maui.Layouts.FlexBasis other) Microsoft.Maui.SizeRequest.Equals(Microsoft.Maui.SizeRequest other) -> bool Microsoft.Maui.Platform.MauiScrollView Microsoft.Maui.Platform.MauiScrollView.MauiScrollView() -> void +override Microsoft.Maui.Handlers.ContentViewHandler.DisconnectHandler(Microsoft.Maui.Platform.ContentView! platformView) -> void override Microsoft.Maui.Handlers.SwipeItemButton.Frame.get -> CoreGraphics.CGRect override Microsoft.Maui.Handlers.SwipeItemButton.Frame.set -> void override Microsoft.Maui.Handlers.SwipeItemMenuItemHandler.ConnectHandler(UIKit.UIButton! platformView) -> void diff --git a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 5c7eddccdd84..f7e7b81f6a37 100644 --- a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -4,6 +4,7 @@ Microsoft.Maui.Hosting.MauiApp.DisposeAsync() -> System.Threading.Tasks.ValueTas Microsoft.Maui.Layouts.FlexBasis.Equals(Microsoft.Maui.Layouts.FlexBasis other) -> bool Microsoft.Maui.Platform.MauiWebView.MauiWebView(Microsoft.Maui.Handlers.WebViewHandler! handler) -> void Microsoft.Maui.SizeRequest.Equals(Microsoft.Maui.SizeRequest other) -> bool +override Microsoft.Maui.Handlers.ContentViewHandler.DisconnectHandler(Microsoft.Maui.Platform.ContentPanel! platformView) -> void override Microsoft.Maui.Layouts.FlexBasis.Equals(object? obj) -> bool override Microsoft.Maui.Layouts.FlexBasis.GetHashCode() -> int override Microsoft.Maui.SizeRequest.Equals(object? obj) -> bool