Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rectify the scopes in the builder #19932

Merged
merged 1 commit into from
Jan 24, 2024
Merged
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
7 changes: 7 additions & 0 deletions src/Controls/samples/Controls.Sample/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/Controls/samples/Controls.Sample/XamlApp.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ITextService>();
Debug.WriteLine($"The requested text service had a message: '{requested.GetText()}'");

Debug.WriteLine($"Current app theme: {RequestedTheme}");

RequestedThemeChanged += (sender, args) =>
Expand Down Expand Up @@ -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<Page>())
var services = activationState!.Context.Services;
var window = new MauiWindow(services.GetRequiredService<Page>())
Comment on lines -51 to +55
Copy link
Member Author

Choose a reason for hiding this comment

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

This has to be changed because the IServiceProvider in the app's ctor is actually the root povider and does not have a window dispatcher.

The page uses a VM that uses the dispatcher. However, the services coming in here via the activation state is a window provider and does have the dispatcher.

{
Title = ".NET MAUI Samples Gallery"
};
Expand Down
4 changes: 1 addition & 3 deletions src/Controls/src/Xaml/Hosting/AppHostBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,7 @@ class MauiControlsInitializer : IMauiInitializeService
public void Initialize(IServiceProvider services)
{
#if WINDOWS
var dispatcher =
services.GetService<IDispatcher>() ??
IPlatformApplication.Current?.Services.GetRequiredService<IDispatcher>();
var dispatcher = services.GetRequiredApplicationDispatcher();

dispatcher
.DispatchIfRequired(() =>
Expand Down
26 changes: 26 additions & 0 deletions src/Controls/tests/DeviceTests/Elements/Window/WindowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -199,5 +201,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);
}
}
}
22 changes: 22 additions & 0 deletions src/Core/src/Dispatching/ApplicationDispatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Microsoft.Maui.Dispatching
{
/// <summary>
/// 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.
/// </summary>
internal class ApplicationDispatcher
{
public IDispatcher Dispatcher { get; }

public ApplicationDispatcher(IDispatcher dispatcher)
{
Dispatcher = dispatcher;
}
}
}
62 changes: 55 additions & 7 deletions src/Core/src/Hosting/Dispatching/AppHostBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IDispatcherProvider>(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<IDispatcherProvider>();
if (DispatcherProvider.SetCurrent(provider))
svc.CreateLogger<Dispatcher>()?.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<ApplicationDispatcher>((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<IMauiInitializeService, ApplicationDispatcherInitializer>());

return Dispatcher.GetForCurrentThread()!;
});
// register the Dispatcher as a scoped service as there may be different dispatchers per window
builder.Services.TryAddScoped<IDispatcher>((svc) => GetDispatcher(svc));
// register the initializer so we can init the dispatcher in the window thread for that window
builder.Services.TryAddEnumerable(ServiceDescriptor.Scoped<IMauiInitializeScopedService, DispatcherInitializer>());

return builder;
}

internal static IDispatcher GetRequiredApplicationDispatcher(this IServiceProvider provider)
{
if (provider is IKeyedServiceProvider keyed)
{
var dispatcher = keyed.GetKeyedService<IDispatcher>(typeof(IApplication));
if (dispatcher is not null)
{
return dispatcher;
}
}

return provider.GetRequiredService<ApplicationDispatcher>().Dispatcher;
}

internal static IDispatcher? GetOptionalApplicationDispatcher(this IServiceProvider provider)
{
if (provider is IKeyedServiceProvider keyed)
{
var dispatcher = keyed.GetKeyedService<IDispatcher>(typeof(IApplication));
if (dispatcher is not null)
{
return dispatcher;
}
}

return provider.GetService<ApplicationDispatcher>()?.Dispatcher;
}

static IDispatcher GetDispatcher(IServiceProvider services)
{
var provider = services.GetRequiredService<IDispatcherProvider>();
if (DispatcherProvider.SetCurrent(provider))
{
services.CreateLogger<Dispatcher>()?.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)
Expand Down
14 changes: 14 additions & 0 deletions src/Core/src/Hosting/IMauiInitializeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,25 @@

namespace Microsoft.Maui.Hosting
{
/// <summary>
/// Represents a service that is initialized during the application construction.
/// </summary>
/// <remarks>
/// This service is initialized during the MauiAppBuilder.Build() method. It is
/// executed once per application using the root service provider.
/// </remarks>
public interface IMauiInitializeService
{
void Initialize(IServiceProvider services);
}

/// <summary>
/// Represents a service that is initialized during the window construction.
/// </summary>
/// <remarks>
/// This service is initialized during the creation of a window. It is
/// executed once per window using the window-scoped service provider.
/// </remarks>
public interface IMauiInitializeScopedService
{
void Initialize(IServiceProvider services);
Expand Down
16 changes: 5 additions & 11 deletions 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.GetRequiredApplicationDispatcher();
if (dispatcher.IsDispatchRequired)
dispatcher.Dispatch(() => SetupResources());
else
Expand Down Expand Up @@ -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<IMauiInitializeService>();
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;
}
Expand Down
19 changes: 17 additions & 2 deletions src/Core/src/MauiContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

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

FYI #8538

scope = mauiContext.Services.CreateScope();

#if ANDROID
Expand All @@ -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<IMauiInitializeService>();
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<IMauiInitializeScopedService>();
if (scopedServices is null)
return;

foreach (var service in scopedServices)
service.Initialize(scopedContext.Services);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public void EnsureHandlerCreated(Action<MauiAppBuilder> additionalCreationAction
var appBuilder = MauiApp.CreateBuilder();

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

Expand Down
6 changes: 6 additions & 0 deletions src/Core/tests/DeviceTests.Shared/MauiProgramDefaults.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,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
2 changes: 1 addition & 1 deletion src/TestUtils/src/DeviceTests.Runners/TestDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public static IDispatcher Current
get
{
if (s_dispatcher is null)
s_dispatcher = TestServices.Services.GetService<IDispatcher>();
s_dispatcher = TestServices.Services.GetService<ApplicationDispatcher>()?.Dispatcher;

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