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 9d512be5cf0c..19f048317041 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs @@ -198,6 +198,7 @@ void Destroy() _viewhandler = null; _shellContent = null; _shellPageContainer = null; + _page = null; } protected override void Dispose(bool disposing) diff --git a/src/Controls/src/Core/HandlerImpl/Window/Window.Impl.cs b/src/Controls/src/Core/HandlerImpl/Window/Window.Impl.cs index eb0e28e5a2d5..b9dd26e13c21 100644 --- a/src/Controls/src/Core/HandlerImpl/Window/Window.Impl.cs +++ b/src/Controls/src/Core/HandlerImpl/Window/Window.Impl.cs @@ -505,6 +505,7 @@ void IWindow.Destroying() Application?.RemoveWindow(this); Handler?.DisconnectHandler(); + ModalNavigationManager?.Disconnect(); } void IWindow.Resumed() @@ -596,6 +597,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/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.cs b/src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.cs index 458036834bef..485a1cf28ee8 100644 --- a/src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.cs +++ b/src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.cs @@ -60,5 +60,11 @@ internal void SettingNewPage() partial void OnPageAttachedHandler(); public void PageAttachedHandler() => OnPageAttachedHandler(); + + public void Disconnect() + { + _navModel?.Clear(); + _previousPage = null; + } } } 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/ControlsHandlerTestBase.iOS.cs b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.iOS.cs index a89cbd3225bd..9e15565ca7d0 100644 --- a/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.iOS.cs +++ b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.iOS.cs @@ -57,10 +57,8 @@ Task SetupWindowForTests(IWindow window, Func runTests, IMauiCon else window.Handler?.DisconnectHandler(); - var vc = - (window.Content?.Handler as IPlatformViewHandler)? - .ViewController; - + var platformHandler = windowHandler as IPlatformViewHandler; + var vc = platformHandler?.ViewController; vc?.RemoveFromParentViewController(); vc?.View?.RemoveFromSuperview(); diff --git a/src/Controls/tests/DeviceTests/Elements/Modal/ModalTests.iOS.cs b/src/Controls/tests/DeviceTests/Elements/Modal/ModalTests.iOS.cs index cbce8e60a94b..721edc5a8d35 100644 --- a/src/Controls/tests/DeviceTests/Elements/Modal/ModalTests.iOS.cs +++ b/src/Controls/tests/DeviceTests/Elements/Modal/ModalTests.iOS.cs @@ -34,6 +34,8 @@ await CreateHandlerAndAddToWindow(rootPage, var currentView = currentPage.Handler.PlatformView as UIView; Assert.NotNull(currentView); Assert.NotNull(currentView.Window); + + await currentPage.Navigation.PopModalAsync(); }); } } diff --git a/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs b/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs index a161651674f6..49dcc7d222a5 100644 --- a/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs @@ -287,5 +287,28 @@ 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!"); + } } } \ 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 642540b51515..1245b5a9777f 100644 --- a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs @@ -662,7 +662,7 @@ await CreateHandlerAndAddToWindow(shell, async (handler) => }); } -#if !IOS +#if !IOS && !MACCATALYST [Fact] public async Task ChangingToNewMauiContextDoesntCrash() { @@ -919,6 +919,40 @@ 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(); + }); + + // Two GCs required currently + for (int i = 0; i < 2; i++) + { + await Task.Yield(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + + Assert.NotNull(pageReference); + Assert.False(pageReference.IsAlive, "Page should not be alive!"); + } + protected Task CreateShellAsync(Action action) => InvokeOnMainThreadAsync(() => { diff --git a/src/Controls/tests/DeviceTests/Stubs/WindowHandlerStub.iOS.cs b/src/Controls/tests/DeviceTests/Stubs/WindowHandlerStub.iOS.cs index d3ea3b43df35..6957db7bd656 100644 --- a/src/Controls/tests/DeviceTests/Stubs/WindowHandlerStub.iOS.cs +++ b/src/Controls/tests/DeviceTests/Stubs/WindowHandlerStub.iOS.cs @@ -76,7 +76,8 @@ async void ReplaceCurrentView(IView view, UIWindow platformView, Action finished return; } - var vc = (view.Handler as IPlatformViewHandler).ViewController; + var handler = view.Handler as IPlatformViewHandler; + var vc = handler?.ViewController; var virtualView = VirtualView; if (view is IFlyoutView) @@ -113,18 +114,21 @@ async void ReplaceCurrentView(IView view, UIWindow platformView, Action finished // So, we're just simulating that cleanup here ourselves. var presentedVC = platformView?.RootViewController?.PresentedViewController ?? - vc.PresentedViewController; + vc?.PresentedViewController; if (presentedVC is Microsoft.Maui.Controls.Platform.ModalWrapper mw) { await mw.PresentingViewController.DismissViewControllerAsync(false); } - vc.RemoveFromParentViewController(); + if (vc != null) + { + vc.RemoveFromParentViewController(); - view - .ToPlatform() - .RemoveFromSuperview(); + view + .ToPlatform() + .RemoveFromSuperview(); + } finishedClosing.Invoke(); 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..7447ff30ca8f 100644 --- a/src/Core/src/Handlers/ContentView/ContentViewHandler.iOS.cs +++ b/src/Core/src/Handlers/ContentView/ContentViewHandler.iOS.cs @@ -53,5 +53,15 @@ public static void MapContent(IContentViewHandler handler, IContentView page) { UpdateContent(handler); } + + protected override void DisconnectHandler(ContentView platformView) + { + platformView.CrossPlatformMeasure = null; + platformView.CrossPlatformArrange = null; + // TODO: this specific call fixes leaks in iOS/Catalyst + // But it doesn't feel quite right + platformView.RemoveFromSuperview(); + base.DisconnectHandler(platformView); + } } } diff --git a/src/Core/src/Handlers/View/ViewHandlerOfT.cs b/src/Core/src/Handlers/View/ViewHandlerOfT.cs index 53040f66cff0..e629bd6bf918 100644 --- a/src/Core/src/Handlers/View/ViewHandlerOfT.cs +++ b/src/Core/src/Handlers/View/ViewHandlerOfT.cs @@ -65,6 +65,14 @@ protected virtual void ConnectHandler(TPlatformView platformView) protected virtual void DisconnectHandler(TPlatformView platformView) { +#if IOS || MACCATALYST + var vc = ViewController; + if (vc is not null) + { + vc.Dispose(); + ViewController = null; + } +#endif } private protected override PlatformView OnCreatePlatformView() 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/Platform/Android/Navigation/ScopedFragment.cs b/src/Core/src/Platform/Android/Navigation/ScopedFragment.cs index e91946f53a1f..64252a0c3e72 100644 --- a/src/Core/src/Platform/Android/Navigation/ScopedFragment.cs +++ b/src/Core/src/Platform/Android/Navigation/ScopedFragment.cs @@ -1,4 +1,5 @@ -using Android.OS; +using System; +using Android.OS; using Android.Views; using AndroidX.Fragment.App; @@ -8,7 +9,7 @@ class ScopedFragment : Fragment { readonly IMauiContext _mauiContext; - public IView DetailView { get; private set; } + public IView? DetailView { get; private set; } public ScopedFragment(IView detailView, IMauiContext mauiContext) { @@ -19,7 +20,14 @@ public ScopedFragment(IView detailView, IMauiContext mauiContext) public override View OnCreateView(LayoutInflater inflater, ViewGroup? container, Bundle? savedInstanceState) { var pageMauiContext = _mauiContext.MakeScoped(layoutInflater: inflater, fragmentManager: ChildFragmentManager); - return DetailView.ToPlatform(pageMauiContext); + var detailView = DetailView ?? throw new InvalidOperationException($"DetailView is null. OnDestroy must have been called!"); + return detailView.ToPlatform(pageMauiContext); + } + + public override void OnDestroy() + { + DetailView = null; + base.OnDestroy(); } } } diff --git a/src/Core/src/Platform/iOS/ContainerViewController.cs b/src/Core/src/Platform/iOS/ContainerViewController.cs index d7fc21feec7f..b89ab7ad8bbe 100644 --- a/src/Core/src/Platform/iOS/ContainerViewController.cs +++ b/src/Core/src/Platform/iOS/ContainerViewController.cs @@ -90,5 +90,22 @@ public override void ViewDidLayoutSubviews() } public void Reload() => SetView(CurrentView, true); + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_view is ContentView c) + { + c.CrossPlatformArrange = null; + c.CrossPlatformMeasure = null; + } + _view = null; + _pendingLoadedView = null; + currentPlatformView = null; + } + + base.Dispose(disposing); + } } } \ No newline at end of file diff --git a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt index 407a0a48dafc..5b36c4515232 100644 --- a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -9,6 +9,7 @@ override Microsoft.Maui.Handlers.EditorHandler.PlatformArrange(Microsoft.Maui.Gr override Microsoft.Maui.Handlers.RadioButtonHandler.PlatformArrange(Microsoft.Maui.Graphics.Rect frame) -> void 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 67ef7761bbfe..73f40aef1de2 100644 --- a/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -6,6 +6,7 @@ Microsoft.Maui.Hosting.MauiApp.DisposeAsync() -> System.Threading.Tasks.ValueTas Microsoft.Maui.Layouts.FlexBasis.Equals(Microsoft.Maui.Layouts.FlexBasis other) -> bool Microsoft.Maui.LifecycleEvents.iOSLifecycle.PerformFetch Microsoft.Maui.SizeRequest.Equals(Microsoft.Maui.SizeRequest other) -> bool +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 @@ -13,6 +14,7 @@ override Microsoft.Maui.Handlers.SwipeItemMenuItemHandler.DisconnectHandler(UIKi override Microsoft.Maui.Handlers.SwitchHandler.NeedsContainer.get -> bool override Microsoft.Maui.Layouts.FlexBasis.Equals(object? obj) -> bool override Microsoft.Maui.Layouts.FlexBasis.GetHashCode() -> int +override Microsoft.Maui.Platform.ContainerViewController.Dispose(bool disposing) -> void override Microsoft.Maui.Platform.MauiTextView.Font.get -> UIKit.UIFont? override Microsoft.Maui.Platform.MauiTextView.Font.set -> void override Microsoft.Maui.SizeRequest.Equals(object? obj) -> bool diff --git a/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index d7db88fdc7d6..01d67ded6ddb 100644 --- a/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -5,6 +5,7 @@ Microsoft.Maui.Handlers.SwipeItemButton.SwipeItemButton() -> void Microsoft.Maui.Hosting.MauiApp.DisposeAsync() -> System.Threading.Tasks.ValueTask Microsoft.Maui.Layouts.FlexBasis.Equals(Microsoft.Maui.Layouts.FlexBasis other) -> bool Microsoft.Maui.SizeRequest.Equals(Microsoft.Maui.SizeRequest other) -> bool +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 @@ -12,6 +13,7 @@ override Microsoft.Maui.Handlers.SwipeItemMenuItemHandler.DisconnectHandler(UIKi override Microsoft.Maui.Handlers.SwitchHandler.NeedsContainer.get -> bool override Microsoft.Maui.Layouts.FlexBasis.Equals(object? obj) -> bool override Microsoft.Maui.Layouts.FlexBasis.GetHashCode() -> int +override Microsoft.Maui.Platform.ContainerViewController.Dispose(bool disposing) -> void override Microsoft.Maui.Platform.MauiTextView.Font.get -> UIKit.UIFont? override Microsoft.Maui.Platform.MauiTextView.Font.set -> void override Microsoft.Maui.SizeRequest.Equals(object? obj) -> bool diff --git a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt index e3f9967ed041..33b50e2c998c 100644 --- a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -3,6 +3,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