diff --git a/src/Controls/src/Xaml/Hosting/AppHostBuilderExtensions.cs b/src/Controls/src/Xaml/Hosting/AppHostBuilderExtensions.cs index 9dd5733c06da..d5eca072ab04 100644 --- a/src/Controls/src/Xaml/Hosting/AppHostBuilderExtensions.cs +++ b/src/Controls/src/Xaml/Hosting/AppHostBuilderExtensions.cs @@ -197,8 +197,8 @@ public void Initialize(IServiceProvider services) { #if WINDOWS var dispatcher = - services.GetService() ?? - IPlatformApplication.Current?.Services.GetRequiredService(); + services.GetService()?.AppDispatcher ?? + IPlatformApplication.Current?.Services?.GetRequiredService(); dispatcher .DispatchIfRequired(() => diff --git a/src/Controls/tests/DeviceTests/Elements/Window/WindowTests.cs b/src/Controls/tests/DeviceTests/Elements/Window/WindowTests.cs index 408db0674023..e2cb71d827a2 100644 --- a/src/Controls/tests/DeviceTests/Elements/Window/WindowTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/Window/WindowTests.cs @@ -1,15 +1,18 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection.PortableExecutable; 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 +202,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/Hosting/Dispatching/AppHostBuilderExtensions.cs b/src/Core/src/Hosting/Dispatching/AppHostBuilderExtensions.cs index e8472470abfc..0e3ef0d5f607 100644 --- a/src/Core/src/Hosting/Dispatching/AppHostBuilderExtensions.cs +++ b/src/Core/src/Hosting/Dispatching/AppHostBuilderExtensions.cs @@ -21,19 +21,43 @@ public static MauiAppBuilder ConfigureDispatching(this MauiAppBuilder builder) if (DispatcherProvider.SetCurrent(provider)) svc.CreateLogger()?.LogWarning("Replaced an existing DispatcherProvider with one from the service provider."); - return Dispatcher.GetForCurrentThread()!; + var dispatch = Dispatcher.GetForCurrentThread(); + + return dispatch ?? svc.GetRequiredService().AppDispatcher; }); - builder.Services.TryAddEnumerable(ServiceDescriptor.Scoped()); + + builder.Services.TryAddSingleton(); + + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); return builder; } - class DispatcherInitializer : IMauiInitializeScopedService + class DispatcherInitializer : IMauiInitializeService { public void Initialize(IServiceProvider services) { - _ = services.GetRequiredService(); + _ = services.GetRequiredService(); } } } -} \ No newline at end of file + + /// + /// If the user tries to retrieve `IDispatcher` from the root `IServiceProvider` on a background thread + /// this serves as a way to retrieve the App level dispatcher + /// + internal class ApplicationDispatcher + { + public IDispatcher AppDispatcher { get; } + + public ApplicationDispatcher() : this(Dispatcher.GetForCurrentThread()!) + { + } + + internal ApplicationDispatcher(IDispatcher dispatcher) + { + AppDispatcher = dispatcher; + } + } +} diff --git a/src/Core/src/Hosting/IMauiInitializeService.cs b/src/Core/src/Hosting/IMauiInitializeService.cs index 85968c132468..ec3da88909c2 100644 --- a/src/Core/src/Hosting/IMauiInitializeService.cs +++ b/src/Core/src/Hosting/IMauiInitializeService.cs @@ -7,6 +7,7 @@ public interface IMauiInitializeService void Initialize(IServiceProvider services); } + // Obsolete/rework for NET9: https://github.com/dotnet/maui/issues/19591 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..1960906a2c63 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.GetRequiredService().AppDispatcher; if (dispatcher.IsDispatchRequired) dispatcher.Dispatch(() => SetupResources()); else diff --git a/src/Core/src/MauiContextExtensions.cs b/src/Core/src/MauiContextExtensions.cs index 7bcf4ad192ab..2f8ebc5ee716 100644 --- a/src/Core/src/MauiContextExtensions.cs +++ b/src/Core/src/MauiContextExtensions.cs @@ -6,20 +6,20 @@ using Microsoft.Maui.ApplicationModel; #if WINDOWS -using NativeApplication = Microsoft.UI.Xaml.Application; -using NativeWindow = Microsoft.UI.Xaml.Window; +using PlatformApplication = Microsoft.UI.Xaml.Application; +using PlatformWindow = Microsoft.UI.Xaml.Window; #elif __IOS__ || __MACCATALYST__ -using NativeApplication = UIKit.IUIApplicationDelegate; -using NativeWindow = UIKit.UIWindow; +using PlatformApplication = UIKit.IUIApplicationDelegate; +using PlatformWindow = UIKit.UIWindow; #elif __ANDROID__ -using NativeApplication = Android.App.Application; -using NativeWindow = Android.App.Activity; +using PlatformApplication = Android.App.Application; +using PlatformWindow = Android.App.Activity; #elif TIZEN -using NativeApplication = Tizen.Applications.CoreApplication; -using NativeWindow = Tizen.NUI.Window; +using PlatformApplication = Tizen.Applications.CoreApplication; +using PlatformWindow = Tizen.NUI.Window; #else -using NativeApplication = System.Object; -using NativeWindow = System.Object; +using PlatformApplication = System.Object; +using PlatformWindow = System.Object; #endif namespace Microsoft.Maui @@ -35,7 +35,7 @@ public static IDispatcher GetDispatcher(this IMauiContext mauiContext) => public static IDispatcher? GetOptionalDispatcher(this IMauiContext mauiContext) => mauiContext.Services.GetService(); - public static IMauiContext MakeApplicationScope(this IMauiContext mauiContext, NativeApplication platformApplication) + public static IMauiContext MakeApplicationScope(this IMauiContext mauiContext, PlatformApplication platformApplication) { var scopedContext = new MauiContext(mauiContext.Services); @@ -46,7 +46,15 @@ public static IMauiContext MakeApplicationScope(this IMauiContext mauiContext, N return scopedContext; } - public static IMauiContext MakeWindowScope(this IMauiContext mauiContext, NativeWindow platformWindow, out IServiceScope scope) + internal static void SetupDispatcher(this IMauiContext mauiContext) + { + // Capture the window level dispatcher. + // If the user first retrieves the IDispatcher from the scoped service on a background thread + // it'll just return null. This ensures that the user has access to the window dispatcher + _ = mauiContext?.Services?.GetService(); + } + + public static IMauiContext MakeWindowScope(this IMauiContext mauiContext, PlatformWindow platformWindow, out IServiceScope scope) { scope = mauiContext.Services.CreateScope(); @@ -65,6 +73,13 @@ public static IMauiContext MakeWindowScope(this IMauiContext mauiContext, Native scopedContext.AddSpecific(new NavigationRootManager(platformWindow)); #endif + // Windows creates its window from the application thread so we + // leave it to the MauiWinUIWindow to call `SetupDispatcher` + // Android/iOS create the window scope from their respective versions of "window" +#if !WINDOWS + scopedContext.SetupDispatcher(); +#endif + return scopedContext; } diff --git a/src/Core/src/Platform/Windows/MauiWinUIWindow.cs b/src/Core/src/Platform/Windows/MauiWinUIWindow.cs index 516cac5f12cd..cdc5dc6074e1 100644 --- a/src/Core/src/Platform/Windows/MauiWinUIWindow.cs +++ b/src/Core/src/Platform/Windows/MauiWinUIWindow.cs @@ -28,8 +28,8 @@ public MauiWinUIWindow() { _windowManager = WindowMessageManager.Get(this); _viewSettings = new ViewManagement.UISettings(); - Activated += OnActivated; + Activated += AssignDispatcher; Closed += OnClosedPrivate; VisibilityChanged += OnVisibilityChanged; @@ -58,6 +58,12 @@ public MauiWinUIWindow() SetIcon(); } + void AssignDispatcher(object sender, UI.Xaml.WindowActivatedEventArgs args) + { + Activated -= AssignDispatcher; + Window?.Handler?.MauiContext?.SetupDispatcher(); + } + protected virtual void OnActivated(object sender, UI.Xaml.WindowActivatedEventArgs args) { if (args.WindowActivationState != UI.Xaml.WindowActivationState.Deactivated) diff --git a/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBasement.cs b/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBasement.cs index d539d56e97bf..d491529a7f9e 100644 --- a/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBasement.cs +++ b/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBasement.cs @@ -40,6 +40,7 @@ public void EnsureHandlerCreated(Action additionalCreationAction appBuilder.Services.AddSingleton(svc => TestDispatcher.Provider); appBuilder.Services.AddScoped(svc => TestDispatcher.Current); + appBuilder.Services.AddSingleton(svc => new ApplicationDispatcher(TestDispatcher.Current)); appBuilder.Services.AddSingleton((_) => new CoreApplicationStub()); appBuilder = ConfigureBuilder(appBuilder); diff --git a/src/Core/tests/DeviceTests.Shared/MauiProgramDefaults.cs b/src/Core/tests/DeviceTests.Shared/MauiProgramDefaults.cs index b01d2202607f..7eef60b33791 100644 --- a/src/Core/tests/DeviceTests.Shared/MauiProgramDefaults.cs +++ b/src/Core/tests/DeviceTests.Shared/MauiProgramDefaults.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Maui.Hosting; using Microsoft.Maui.LifecycleEvents; using Microsoft.Maui.TestUtils.DeviceTests.Runners; @@ -69,6 +70,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..02842a1b2779 100644 --- a/src/TestUtils/src/DeviceTests.Runners/TestDispatcher.cs +++ b/src/TestUtils/src/DeviceTests.Runners/TestDispatcher.cs @@ -29,7 +29,9 @@ public static IDispatcher Current get { if (s_dispatcher is null) - s_dispatcher = TestServices.Services.GetService(); + { + s_dispatcher = TestServices.Services.GetRequiredService().AppDispatcher; + } if (s_dispatcher is null) throw new InvalidOperationException($"Test app did not provide a dispatcher.");