diff --git a/src/Controls/src/Core/Window/Window.cs b/src/Controls/src/Core/Window/Window.cs index b4a744343101..9c0015232793 100644 --- a/src/Controls/src/Core/Window/Window.cs +++ b/src/Controls/src/Core/Window/Window.cs @@ -545,7 +545,12 @@ void IWindow.Destroying() AlertManager.Unsubscribe(); Application?.RemoveWindow(this); + + var mauiContext = Handler?.MauiContext as MauiContext; Handler?.DisconnectHandler(); + + // Dispose the window-scoped service scope + mauiContext?.DisposeWindowScope(); } void IWindow.Resumed() diff --git a/src/Controls/tests/Core.UnitTests/WindowsTests.cs b/src/Controls/tests/Core.UnitTests/WindowsTests.cs index d63f48204ea4..e7f4608157b0 100644 --- a/src/Controls/tests/Core.UnitTests/WindowsTests.cs +++ b/src/Controls/tests/Core.UnitTests/WindowsTests.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Maui.Graphics; using Xunit; @@ -856,13 +857,94 @@ public void BindingIsActivatedProperty() Assert.False(vm.IsWindowActive); } - } - class ViewModel - { - public bool IsWindowActive + [Fact] + public void WindowServiceScopeIsDisposedOnDestroying() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddTransient(); + var serviceProvider = serviceCollection.BuildServiceProvider(); + + var scope = serviceProvider.CreateScope(); + var mauiContext = new MauiContext(scope.ServiceProvider); + mauiContext.SetWindowScope(scope); + + var window = new TestWindow(new ContentPage()); + var handler = new WindowHandlerStub(); + handler.SetMauiContext(mauiContext); + window.Handler = handler; + + // Verify the scope service works before disposal + var service = mauiContext.Services.GetService(); + Assert.NotNull(service); + + // Destroy the window - this should dispose the scope + ((IWindow)window).Destroying(); + + // After disposal, the scope should be disposed + // We can't directly test if scope is disposed, but we can test that trying to use it throws + Assert.Throws(() => scope.ServiceProvider.GetService()); + } + + class ViewModel + { + public bool IsWindowActive + { + get; + set; + } + } + + [Fact] + public void WindowServiceScopeHandlesNullScope() + { + // Test that destroying a window without a scope doesn't throw + var mauiContext = new MockMauiContext(); + var window = new TestWindow(new ContentPage()); + var handler = new WindowHandlerStub(); + handler.SetMauiContext(mauiContext); + window.Handler = handler; + + // This should not throw even though there's no scope to dispose + ((IWindow)window).Destroying(); + } + + [Fact] + public void WindowServiceScopeWorksWithWindowCreationFlow() + { + // Test the full flow as it would happen in real usage + var serviceCollection = new ServiceCollection(); + serviceCollection.AddScoped(); + var rootServiceProvider = serviceCollection.BuildServiceProvider(); + + var appContext = new MauiContext(rootServiceProvider); + + // Simulate the window creation flow + var windowContext = appContext.MakeWindowScope(new object(), out var scope); + + // Verify we can get scoped services + var service1 = windowContext.Services.GetRequiredService(); + var service2 = windowContext.Services.GetRequiredService(); + + // Should be the same instance since it's scoped + Assert.Same(service1, service2); + + // Create window and set up handler + var window = new TestWindow(new ContentPage()); + var handler = new WindowHandlerStub(); + handler.SetMauiContext(windowContext); + window.Handler = handler; + + // Destroy the window + ((IWindow)window).Destroying(); + + // Scope should be disposed + Assert.Throws(() => scope.ServiceProvider.GetService()); + } + + private class TestScopedService { - get; set; + public string TestProperty { get; set; } = "test"; } } -} +} \ No newline at end of file diff --git a/src/Core/src/MauiContext.cs b/src/Core/src/MauiContext.cs index d9b8df659d4b..8b313e48d441 100644 --- a/src/Core/src/MauiContext.cs +++ b/src/Core/src/MauiContext.cs @@ -11,6 +11,7 @@ public class MauiContext : IMauiContext { readonly WrappedServiceProvider _services; readonly Lazy _handlers; + IServiceScope? _windowScope; #if ANDROID readonly Lazy _context; @@ -53,6 +54,17 @@ internal void AddWeakSpecific(TService instance) _services.AddSpecific(typeof(TService), static state => ((WeakReference)state).Target, new WeakReference(instance)); } + internal void SetWindowScope(IServiceScope scope) + { + _windowScope = scope; + } + + internal void DisposeWindowScope() + { + _windowScope?.Dispose(); + _windowScope = null; + } + class WrappedServiceProvider : IServiceProvider { readonly ConcurrentDictionary)> _scopeStatic = new(); diff --git a/src/Core/src/MauiContextExtensions.cs b/src/Core/src/MauiContextExtensions.cs index e8c515d56220..ab841cfde59e 100644 --- a/src/Core/src/MauiContextExtensions.cs +++ b/src/Core/src/MauiContextExtensions.cs @@ -47,7 +47,6 @@ public static IMauiContext MakeApplicationScope(this IMauiContext mauiContext, N public static IMauiContext MakeWindowScope(this IMauiContext mauiContext, NativeWindow platformWindow, out IServiceScope scope) { // Create the window-level scopes that will only be used for the lifetime of the window - // TODO: We need to dispose of these services once the window closes scope = mauiContext.Services.CreateScope(); #if ANDROID @@ -56,6 +55,9 @@ public static IMauiContext MakeWindowScope(this IMauiContext mauiContext, Native var scopedContext = new MauiContext(scope.ServiceProvider); #endif + // Store the scope in the scoped context so it can be disposed when the window is destroyed + scopedContext.SetWindowScope(scope); + scopedContext.AddWeakSpecific(platformWindow); #if ANDROID