Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Controls/src/Xaml/Hosting/AppHostBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ public void Initialize(IServiceProvider services)
{
#if WINDOWS
var dispatcher =
services.GetService<IDispatcher>() ??
IPlatformApplication.Current?.Services.GetRequiredService<IDispatcher>();
services.GetService<ApplicationDispatcher>()?.AppDispatcher ??
IPlatformApplication.Current?.Services?.GetRequiredService<IDispatcher>();

dispatcher
.DispatchIfRequired(() =>
Expand Down
27 changes: 27 additions & 0 deletions src/Controls/tests/DeviceTests/Elements/Window/WindowTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -199,5 +202,29 @@ await CreateHandlerAndAddToWindow<WindowHandlerStub>(window, async (handler) =>
}
#endif

[Fact(DisplayName = "Initial Dispatch from Background Thread Succeeds")]
public async Task InitialDispatchFromBackgroundThreadSucceeds()
{
EnsureHandlerCreated(builder =>
{
builder.Services.RemoveAll<IDispatcher>();
builder.ConfigureDispatching();
});

var firstPage = new ContentPage();
var window = new Window(firstPage);
bool passed = true;

await CreateHandlerAndAddToWindow<WindowHandlerStub>(window, async (handler) =>
{
await Task.Run(async () =>
{
await firstPage.Handler.MauiContext.Services.GetRequiredService<IDispatcher>()
.DispatchAsync(() => passed = true);
});
});

Assert.True(passed);
}
}
}
34 changes: 29 additions & 5 deletions src/Core/src/Hosting/Dispatching/AppHostBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,43 @@ public static MauiAppBuilder ConfigureDispatching(this MauiAppBuilder builder)
if (DispatcherProvider.SetCurrent(provider))
svc.CreateLogger<Dispatcher>()?.LogWarning("Replaced an existing DispatcherProvider with one from the service provider.");

return Dispatcher.GetForCurrentThread()!;
var dispatch = Dispatcher.GetForCurrentThread();

return dispatch ?? svc.GetRequiredService<ApplicationDispatcher>().AppDispatcher;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the coalescing here, because we don't want to break users that are retrieving the scoped IDispatcher from the root service provider on a background thread. Because we are no longer saturating the IDispatcher on the root provider we need to account for this.

});
builder.Services.TryAddEnumerable(ServiceDescriptor.Scoped<IMauiInitializeScopedService, DispatcherInitializer>());

builder.Services.TryAddSingleton<ApplicationDispatcher>();


builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IMauiInitializeService, DispatcherInitializer>());
Copy link
Copy Markdown
Member Author

@PureWeen PureWeen Dec 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this ok @mattleibow ?

I can't really tell a difference here between using IMauiInitializeService vs IMauiInitializeScopedService. They are both fired from OnLaunched in MauiWinUIApplication so they both will capture the dispatcher from the main thread

The purpose of these are to initialize at the app level not the "Scoped" windows level so it seems like they should just be IMauiInitializeService

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMauiInitializeService should only run once per app during the MauiAppBuilder.Build() method. The scoped services are supposed to be initialized for each window. However... I see that it is not working like that so I want to see what is really happening and if this is all a bug because we are not doing something...


return builder;
}

class DispatcherInitializer : IMauiInitializeScopedService
class DispatcherInitializer : IMauiInitializeService
{
public void Initialize(IServiceProvider services)
{
_ = services.GetRequiredService<IDispatcher>();
_ = services.GetRequiredService<ApplicationDispatcher>();
}
}
}
}

/// <summary>
/// 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
/// </summary>
internal class ApplicationDispatcher
{
public IDispatcher AppDispatcher { get; }

public ApplicationDispatcher() : this(Dispatcher.GetForCurrentThread()!)
{
}

internal ApplicationDispatcher(IDispatcher dispatcher)
{
AppDispatcher = dispatcher;
}
}
}
1 change: 1 addition & 0 deletions src/Core/src/Hosting/IMauiInitializeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/Core/src/Hosting/MauiAppBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IDispatcher>();
var dispatcher = services.GetRequiredService<ApplicationDispatcher>().AppDispatcher;
if (dispatcher.IsDispatchRequired)
dispatcher.Dispatch(() => SetupResources());
else
Expand Down
39 changes: 27 additions & 12 deletions src/Core/src/MauiContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,7 +35,7 @@ public static IDispatcher GetDispatcher(this IMauiContext mauiContext) =>
public static IDispatcher? GetOptionalDispatcher(this IMauiContext mauiContext) =>
mauiContext.Services.GetService<IDispatcher>();

public static IMauiContext MakeApplicationScope(this IMauiContext mauiContext, NativeApplication platformApplication)
public static IMauiContext MakeApplicationScope(this IMauiContext mauiContext, PlatformApplication platformApplication)
{
var scopedContext = new MauiContext(mauiContext.Services);

Expand All @@ -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<IDispatcher>();
}

public static IMauiContext MakeWindowScope(this IMauiContext mauiContext, PlatformWindow platformWindow, out IServiceScope scope)
{
scope = mauiContext.Services.CreateScope();

Expand All @@ -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;
}

Expand Down
8 changes: 7 additions & 1 deletion src/Core/src/Platform/Windows/MauiWinUIWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ public MauiWinUIWindow()
{
_windowManager = WindowMessageManager.Get(this);
_viewSettings = new ViewManagement.UISettings();

Activated += OnActivated;
Activated += AssignDispatcher;
Closed += OnClosedPrivate;
VisibilityChanged += OnVisibilityChanged;

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public void EnsureHandlerCreated(Action<MauiAppBuilder> additionalCreationAction

appBuilder.Services.AddSingleton<IDispatcherProvider>(svc => TestDispatcher.Provider);
appBuilder.Services.AddScoped<IDispatcher>(svc => TestDispatcher.Current);
appBuilder.Services.AddSingleton<ApplicationDispatcher>(svc => new ApplicationDispatcher(TestDispatcher.Current));
appBuilder.Services.AddSingleton<IApplication>((_) => new CoreApplicationStub());

appBuilder = ConfigureBuilder(appBuilder);
Expand Down
7 changes: 7 additions & 0 deletions src/Core/tests/DeviceTests.Shared/MauiProgramDefaults.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -69,6 +70,12 @@ public static MauiApp CreateMauiApp(List<Assembly> testAssemblies)
#endif
appBuilder.UseVisualRunner();

appBuilder.ConfigureContainer(new DefaultServiceProviderFactory(new ServiceProviderOptions
{
ValidateOnBuild = true,
ValidateScopes = true,
}));

var mauiApp = appBuilder.Build();

DefaultTestApp = mauiApp.Services.GetRequiredService<IApplication>();
Expand Down
4 changes: 3 additions & 1 deletion src/TestUtils/src/DeviceTests.Runners/TestDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ public static IDispatcher Current
get
{
if (s_dispatcher is null)
s_dispatcher = TestServices.Services.GetService<IDispatcher>();
{
s_dispatcher = TestServices.Services.GetRequiredService<Hosting.ApplicationDispatcher>().AppDispatcher;
}

if (s_dispatcher is null)
throw new InvalidOperationException($"Test app did not provide a dispatcher.");
Expand Down