diff --git a/src/Controls/samples/Controls.Sample/MauiProgram.cs b/src/Controls/samples/Controls.Sample/MauiProgram.cs index e9f5f12c0f43..a03e23811c3c 100644 --- a/src/Controls/samples/Controls.Sample/MauiProgram.cs +++ b/src/Controls/samples/Controls.Sample/MauiProgram.cs @@ -45,6 +45,13 @@ enum PageType { Main, Blazor, Shell, Template, FlyoutPage, TabbedPage } public static MauiApp CreateMauiApp() { var appBuilder = MauiApp.CreateBuilder(); + + appBuilder.ConfigureContainer(new DefaultServiceProviderFactory(new ServiceProviderOptions + { + ValidateOnBuild = true, + ValidateScopes = true, + })); + #if __ANDROID__ || __IOS__ appBuilder.UseMauiMaps(); #endif diff --git a/src/Controls/samples/Controls.Sample/XamlApp.xaml.cs b/src/Controls/samples/Controls.Sample/XamlApp.xaml.cs index 265adc7e8a0a..46bd5d3123de 100644 --- a/src/Controls/samples/Controls.Sample/XamlApp.xaml.cs +++ b/src/Controls/samples/Controls.Sample/XamlApp.xaml.cs @@ -19,6 +19,9 @@ public XamlApp(IServiceProvider services, ITextService textService) Debug.WriteLine($"The injected text service had a message: '{textService.GetText()}'"); + var requested = services.GetRequiredService(); + Debug.WriteLine($"The requested text service had a message: '{requested.GetText()}'"); + Debug.WriteLine($"Current app theme: {RequestedTheme}"); RequestedThemeChanged += (sender, args) => @@ -48,7 +51,8 @@ async void LoadAsset() // Must not use MainPage for multi-window protected override Window CreateWindow(IActivationState? activationState) { - var window = new MauiWindow(Services.GetRequiredService()) + var services = activationState!.Context.Services; + var window = new MauiWindow(services.GetRequiredService()) { Title = ".NET MAUI Samples Gallery" }; diff --git a/src/Controls/src/Xaml/Hosting/AppHostBuilderExtensions.cs b/src/Controls/src/Xaml/Hosting/AppHostBuilderExtensions.cs index 9dd5733c06da..50eeb913fc75 100644 --- a/src/Controls/src/Xaml/Hosting/AppHostBuilderExtensions.cs +++ b/src/Controls/src/Xaml/Hosting/AppHostBuilderExtensions.cs @@ -196,9 +196,7 @@ class MauiControlsInitializer : IMauiInitializeService public void Initialize(IServiceProvider services) { #if WINDOWS - var dispatcher = - services.GetService() ?? - IPlatformApplication.Current?.Services.GetRequiredService(); + var dispatcher = services.GetRequiredApplicationDispatcher(); dispatcher .DispatchIfRequired(() => diff --git a/src/Controls/tests/DeviceTests/Elements/Window/WindowTests.cs b/src/Controls/tests/DeviceTests/Elements/Window/WindowTests.cs index 408db0674023..4fdd4e279896 100644 --- a/src/Controls/tests/DeviceTests/Elements/Window/WindowTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/Window/WindowTests.cs @@ -4,12 +4,14 @@ using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Maui; using Microsoft.Maui.Controls; using Microsoft.Maui.Controls.Handlers; using Microsoft.Maui.Controls.Platform; using Microsoft.Maui.Devices; using Microsoft.Maui.DeviceTests.Stubs; +using Microsoft.Maui.Dispatching; using Microsoft.Maui.Graphics; using Microsoft.Maui.Handlers; using Microsoft.Maui.Hosting; @@ -199,5 +201,29 @@ await CreateHandlerAndAddToWindow(window, async (handler) => } #endif + [Fact(DisplayName = "Initial Dispatch from Background Thread Succeeds")] + public async Task InitialDispatchFromBackgroundThreadSucceeds() + { + EnsureHandlerCreated(builder => + { + builder.Services.RemoveAll(); + builder.ConfigureDispatching(); + }); + + var firstPage = new ContentPage(); + var window = new Window(firstPage); + bool passed = true; + + await CreateHandlerAndAddToWindow(window, async (handler) => + { + await Task.Run(async () => + { + await firstPage.Handler.MauiContext.Services.GetRequiredService() + .DispatchAsync(() => passed = true); + }); + }); + + Assert.True(passed); + } } } diff --git a/src/Core/src/Dispatching/ApplicationDispatcher.cs b/src/Core/src/Dispatching/ApplicationDispatcher.cs new file mode 100644 index 000000000000..d72b5b0d68e0 --- /dev/null +++ b/src/Core/src/Dispatching/ApplicationDispatcher.cs @@ -0,0 +1,22 @@ +namespace Microsoft.Maui.Dispatching +{ + /// + /// The default service provider does not support a single service type for + /// BOTH a singleton (for the root app) AND a scoped (for the window scope). + /// This is a small wrapper so we can do the same thing. The preferred way is + /// actually a keyed service, but this is a new feature that existing factories + /// may not yet support. Also, this wrapper is not public so it is hard to + /// replace/substitute in tests. + /// + /// TODO: Remove in net9 and require a keyed service - or some other way. + /// + internal class ApplicationDispatcher + { + public IDispatcher Dispatcher { get; } + + public ApplicationDispatcher(IDispatcher dispatcher) + { + Dispatcher = dispatcher; + } + } +} \ No newline at end of file diff --git a/src/Core/src/Hosting/Dispatching/AppHostBuilderExtensions.cs b/src/Core/src/Hosting/Dispatching/AppHostBuilderExtensions.cs index e8472470abfc..516e7395769e 100644 --- a/src/Core/src/Hosting/Dispatching/AppHostBuilderExtensions.cs +++ b/src/Core/src/Hosting/Dispatching/AppHostBuilderExtensions.cs @@ -10,24 +10,72 @@ public static partial class AppHostBuilderExtensions { public static MauiAppBuilder ConfigureDispatching(this MauiAppBuilder builder) { + // register the DispatcherProvider as a singleton for the entire app builder.Services.TryAddSingleton(svc => // the DispatcherProvider might have already been initialized, so ensure that we are grabbing the // Current and putting it in the DI container. DispatcherProvider.Current); - builder.Services.TryAddScoped(svc => - { - var provider = svc.GetRequiredService(); - if (DispatcherProvider.SetCurrent(provider)) - svc.CreateLogger()?.LogWarning("Replaced an existing DispatcherProvider with one from the service provider."); + // register a fallback dispatcher when the service provider does not support keyed services + builder.Services.TryAddSingleton((svc) => new ApplicationDispatcher(GetDispatcher(svc))); + // register the initializer so we can init the dispatcher in the app thread for the app + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - return Dispatcher.GetForCurrentThread()!; - }); + // register the Dispatcher as a scoped service as there may be different dispatchers per window + builder.Services.TryAddScoped((svc) => GetDispatcher(svc)); + // register the initializer so we can init the dispatcher in the window thread for that window builder.Services.TryAddEnumerable(ServiceDescriptor.Scoped()); return builder; } + internal static IDispatcher GetRequiredApplicationDispatcher(this IServiceProvider provider) + { + if (provider is IKeyedServiceProvider keyed) + { + var dispatcher = keyed.GetKeyedService(typeof(IApplication)); + if (dispatcher is not null) + { + return dispatcher; + } + } + + return provider.GetRequiredService().Dispatcher; + } + + internal static IDispatcher? GetOptionalApplicationDispatcher(this IServiceProvider provider) + { + if (provider is IKeyedServiceProvider keyed) + { + var dispatcher = keyed.GetKeyedService(typeof(IApplication)); + if (dispatcher is not null) + { + return dispatcher; + } + } + + return provider.GetService()?.Dispatcher; + } + + static IDispatcher GetDispatcher(IServiceProvider services) + { + var provider = services.GetRequiredService(); + if (DispatcherProvider.SetCurrent(provider)) + { + services.CreateLogger()?.LogWarning("Replaced an existing DispatcherProvider with one from the service provider."); + } + + return Dispatcher.GetForCurrentThread()!; + } + + class ApplicationDispatcherInitializer : IMauiInitializeService + { + public void Initialize(IServiceProvider services) + { + _ = services.GetOptionalApplicationDispatcher(); + } + } + class DispatcherInitializer : IMauiInitializeScopedService { public void Initialize(IServiceProvider services) diff --git a/src/Core/src/Hosting/IMauiInitializeService.cs b/src/Core/src/Hosting/IMauiInitializeService.cs index 85968c132468..4b34df4612e4 100644 --- a/src/Core/src/Hosting/IMauiInitializeService.cs +++ b/src/Core/src/Hosting/IMauiInitializeService.cs @@ -2,11 +2,25 @@ namespace Microsoft.Maui.Hosting { + /// + /// Represents a service that is initialized during the application construction. + /// + /// + /// This service is initialized during the MauiAppBuilder.Build() method. It is + /// executed once per application using the root service provider. + /// public interface IMauiInitializeService { void Initialize(IServiceProvider services); } + /// + /// Represents a service that is initialized during the window construction. + /// + /// + /// This service is initialized during the creation of a window. It is + /// executed once per window using the window-scoped service provider. + /// public interface IMauiInitializeScopedService { void Initialize(IServiceProvider services); diff --git a/src/Core/src/Hosting/MauiAppBuilder.cs b/src/Core/src/Hosting/MauiAppBuilder.cs index cdbc3ef72b88..77791baa7f10 100644 --- a/src/Core/src/Hosting/MauiAppBuilder.cs +++ b/src/Core/src/Hosting/MauiAppBuilder.cs @@ -55,7 +55,7 @@ public void Initialize(IServiceProvider services) #if WINDOWS // WORKAROUND: use the MAUI dispatcher instead of the OS dispatcher to // avoid crashing: https://github.com/microsoft/WindowsAppSDK/issues/2451 - var dispatcher = services.GetRequiredService(); + var dispatcher = services.GetRequiredApplicationDispatcher(); if (dispatcher.IsDispatchRequired) dispatcher.Dispatch(() => SetupResources()); else @@ -150,19 +150,13 @@ public MauiApp Build() ? _createServiceProvider() : _services.BuildServiceProvider(); - MauiApp builtApplication = new MauiApp(serviceProvider); - // Mark the service collection as read-only to prevent future modifications _services.MakeReadOnly(); - var initServices = builtApplication.Services.GetServices(); - if (initServices != null) - { - foreach (var instance in initServices) - { - instance.Initialize(builtApplication.Services); - } - } + MauiApp builtApplication = new MauiApp(serviceProvider); + + // Initialize any singleton/app services, for example the OS hooks + builtApplication.InitializeAppServices(); return builtApplication; } diff --git a/src/Core/src/MauiContextExtensions.cs b/src/Core/src/MauiContextExtensions.cs index 7bcf4ad192ab..e72d414834a4 100644 --- a/src/Core/src/MauiContextExtensions.cs +++ b/src/Core/src/MauiContextExtensions.cs @@ -41,13 +41,13 @@ public static IMauiContext MakeApplicationScope(this IMauiContext mauiContext, N scopedContext.AddSpecific(platformApplication); - scopedContext.InitializeScopedServices(); - return scopedContext; } 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 @@ -65,12 +65,27 @@ public static IMauiContext MakeWindowScope(this IMauiContext mauiContext, Native scopedContext.AddSpecific(new NavigationRootManager(platformWindow)); #endif + // Initialize any window-scoped services, for example the window dispatchers and animation tickers + scopedContext.InitializeScopedServices(); + return scopedContext; } + public static void InitializeAppServices(this MauiApp mauiApp) + { + var initServices = mauiApp.Services.GetServices(); + if (initServices is null) + return; + + foreach (var instance in initServices) + instance.Initialize(mauiApp.Services); + } + public static void InitializeScopedServices(this IMauiContext scopedContext) { var scopedServices = scopedContext.Services.GetServices(); + if (scopedServices is null) + return; foreach (var service in scopedServices) service.Initialize(scopedContext.Services); diff --git a/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBasement.cs b/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBasement.cs index d539d56e97bf..b735f2bc2810 100644 --- a/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBasement.cs +++ b/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBasement.cs @@ -39,6 +39,7 @@ public void EnsureHandlerCreated(Action additionalCreationAction var appBuilder = MauiApp.CreateBuilder(); appBuilder.Services.AddSingleton(svc => TestDispatcher.Provider); + appBuilder.Services.AddKeyedSingleton(typeof(IApplication), (svc, key) => TestDispatcher.Current); appBuilder.Services.AddScoped(svc => TestDispatcher.Current); appBuilder.Services.AddSingleton((_) => new CoreApplicationStub()); diff --git a/src/Core/tests/DeviceTests.Shared/MauiProgramDefaults.cs b/src/Core/tests/DeviceTests.Shared/MauiProgramDefaults.cs index b01d2202607f..a99ca72d0133 100644 --- a/src/Core/tests/DeviceTests.Shared/MauiProgramDefaults.cs +++ b/src/Core/tests/DeviceTests.Shared/MauiProgramDefaults.cs @@ -69,6 +69,12 @@ public static MauiApp CreateMauiApp(List testAssemblies) #endif appBuilder.UseVisualRunner(); + appBuilder.ConfigureContainer(new DefaultServiceProviderFactory(new ServiceProviderOptions + { + ValidateOnBuild = true, + ValidateScopes = true, + })); + var mauiApp = appBuilder.Build(); DefaultTestApp = mauiApp.Services.GetRequiredService(); diff --git a/src/TestUtils/src/DeviceTests.Runners/TestDispatcher.cs b/src/TestUtils/src/DeviceTests.Runners/TestDispatcher.cs index 5530ac952abc..d551e8ac46b4 100644 --- a/src/TestUtils/src/DeviceTests.Runners/TestDispatcher.cs +++ b/src/TestUtils/src/DeviceTests.Runners/TestDispatcher.cs @@ -29,7 +29,7 @@ public static IDispatcher Current get { if (s_dispatcher is null) - s_dispatcher = TestServices.Services.GetService(); + s_dispatcher = TestServices.Services.GetService()?.Dispatcher; if (s_dispatcher is null) throw new InvalidOperationException($"Test app did not provide a dispatcher.");