diff --git a/src/BlazorWebView/src/Maui/BlazorWebView.cs b/src/BlazorWebView/src/Maui/BlazorWebView.cs index d54ee207cf77..3cd69927be08 100644 --- a/src/BlazorWebView/src/Maui/BlazorWebView.cs +++ b/src/BlazorWebView/src/Maui/BlazorWebView.cs @@ -106,7 +106,7 @@ public string StartPath public virtual IFileProvider CreateFileProvider(string contentRootDir) { // Call into the platform-specific code to get that platform's asset file provider - return ((BlazorWebViewHandler)(Handler!)).CreateFileProvider(contentRootDir); + return GetBlazorWebViewHandler().CreateFileProvider(contentRootDir); } /// @@ -125,14 +125,32 @@ public virtual IFileProvider CreateFileProvider(string contentRootDir) public virtual async Task TryDispatchAsync(Action workItem) { ArgumentNullException.ThrowIfNull(workItem); - if (Handler is null) + var handler = Handler; + if (handler is null) { return false; } - return await ((BlazorWebViewHandler)(Handler!)).TryDispatchAsync(workItem); + return await GetBlazorWebViewHandler(handler).TryDispatchAsync(workItem); } + private IBlazorWebViewHandler GetBlazorWebViewHandler() + { + var handler = Handler; + if (handler is null) + { + throw new InvalidOperationException( + $"{nameof(BlazorWebView)} must be connected to a handler before this operation can be performed."); + } + + return GetBlazorWebViewHandler(handler); + } + + private static IBlazorWebViewHandler GetBlazorWebViewHandler(IViewHandler handler) => + handler as IBlazorWebViewHandler ?? + throw new InvalidOperationException( + $"The handler type '{handler.GetType().FullName}' must implement {nameof(IBlazorWebViewHandler)}."); + /// void IBlazorWebView.UrlLoading(UrlLoadingEventArgs args) => UrlLoading?.Invoke(this, args); diff --git a/src/BlazorWebView/src/Maui/BlazorWebViewHandler.cs b/src/BlazorWebView/src/Maui/BlazorWebViewHandler.cs index df638d6d7cd1..149ef276995d 100644 --- a/src/BlazorWebView/src/Maui/BlazorWebViewHandler.cs +++ b/src/BlazorWebView/src/Maui/BlazorWebViewHandler.cs @@ -1,8 +1,10 @@ using System; using System.Linq; using System.Runtime.Versioning; +using System.Threading.Tasks; using Microsoft.AspNetCore.Components.WebView; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; using Microsoft.Maui; using Microsoft.Maui.Handlers; @@ -15,7 +17,7 @@ namespace Microsoft.AspNetCore.Components.WebView.Maui #elif MACCATALYST [SupportedOSPlatform(BlazorWebView.MacCatalystSupportedOSPlatformVersion)] #endif - public partial class BlazorWebViewHandler + public partial class BlazorWebViewHandler : IBlazorWebViewHandler { private const string UseBlockingDisposalSwitch = "BlazorWebView.UseBlockingDisposal"; @@ -50,6 +52,12 @@ public BlazorWebViewHandler(PropertyMapper? mapper) : base(mapper ?? BlazorWebVi { } + IFileProvider IBlazorWebViewHandler.CreateFileProvider(string contentRootDir) => + CreateFileProvider(contentRootDir); + + Task IBlazorWebViewHandler.TryDispatchAsync(Action workItem) => + TryDispatchAsync(workItem); + internal BlazorWebViewDeveloperTools DeveloperTools => MauiContext!.Services.GetRequiredService(); /// diff --git a/src/BlazorWebView/src/Maui/IBlazorWebViewHandler.cs b/src/BlazorWebView/src/Maui/IBlazorWebViewHandler.cs new file mode 100644 index 000000000000..9cb2e9478655 --- /dev/null +++ b/src/BlazorWebView/src/Maui/IBlazorWebViewHandler.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.FileProviders; +using Microsoft.Maui; + +namespace Microsoft.AspNetCore.Components.WebView.Maui +{ + /// + /// Defines the operations required by a handler for . + /// + /// + /// On Windows, an implementation that owns a WebView2 must either expose it directly as + /// its PlatformView so the framework can close it when the window is destroyed, + /// or close the wrapped control from its own disconnect logic. This preserves the built-in workaround + /// for microsoft-ui-xaml issue 6872. + /// + public interface IBlazorWebViewHandler : IViewHandler + { + /// + /// Creates the file provider used to serve static web assets. + /// + /// The base directory for static web assets. + /// The file provider for the handler's platform. + IFileProvider CreateFileProvider(string contentRootDir); + + /// + /// Dispatches work to the scoped services available to Razor components. + /// + /// The work to dispatch. + /// + /// A task representing when the work was dispatched, + /// or when Blazor is not currently running. + /// + Task TryDispatchAsync(Action workItem); + } +} diff --git a/src/BlazorWebView/src/Maui/MauiBlazorWebViewBuilderExtensions.cs b/src/BlazorWebView/src/Maui/MauiBlazorWebViewBuilderExtensions.cs new file mode 100644 index 000000000000..8ae1eda2c187 --- /dev/null +++ b/src/BlazorWebView/src/Maui/MauiBlazorWebViewBuilderExtensions.cs @@ -0,0 +1,75 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Maui.Hosting; + +namespace Microsoft.AspNetCore.Components.WebView.Maui +{ + /// + /// Extension methods for . + /// + public static class MauiBlazorWebViewBuilderExtensions + { + /// + /// Registers a custom handler for , replacing the default + /// registered by + /// . + /// This allows custom platform backends to provide their own BlazorWebView handler + /// while reusing all shared service registrations. + /// + /// + /// Replacement is "last-registration-wins" through the underlying MAUI handler collection. + /// Call this method after AddMauiBlazorWebView() so the custom handler + /// overrides the default registration. If a downstream library calls + /// AddMauiBlazorWebView() again later in the pipeline, that subsequent default + /// registration will silently re-override this custom handler — call this method last, + /// after every other library's MAUI Blazor configuration, when composing multiple sources. + /// + /// The custom handler type to use for . + /// The handler must implement and have a public + /// parameterless constructor. + /// The . + /// The for chaining. + public static IMauiBlazorWebViewBuilder UsePlatformHandler<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>( + this IMauiBlazorWebViewBuilder builder) + where THandler : IBlazorWebViewHandler, new() + { + ArgumentNullException.ThrowIfNull(builder); + builder.Services.ConfigureMauiHandlers(handlers => + handlers.AddHandler()); + return builder; + } + + /// + /// Registers a custom handler for using a factory method, + /// replacing the default registered by + /// . + /// Use this overload for handlers that lack a public parameterless constructor or that + /// need to pull dependencies from the MAUI handler service container at construction time. + /// + /// + /// The passed to is the MAUI + /// handler factory's service provider, not the application's root . + /// It can resolve services that were registered on the handler collection (via + /// ConfigureMauiHandlers); it cannot resolve arbitrary services from the app's + /// . The same call-ordering rule as + /// applies — call this + /// method after AddMauiBlazorWebView() (and after any later re-invocations from + /// downstream libraries) so the custom handler is the last registration to win. + /// + /// The . + /// A factory function that creates the handler instance. + /// The argument is the MAUI handler factory's service provider + /// (see remarks). + /// The for chaining. + public static IMauiBlazorWebViewBuilder UsePlatformHandler( + this IMauiBlazorWebViewBuilder builder, + Func factory) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(factory); + builder.Services.ConfigureMauiHandlers(handlers => + handlers.AddHandler(factory)); + return builder; + } + } +} diff --git a/src/BlazorWebView/src/Maui/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/BlazorWebView/src/Maui/PublicAPI/net-android/PublicAPI.Unshipped.txt index 51e039fc88da..6d5e2df8808e 100644 --- a/src/BlazorWebView/src/Maui/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/BlazorWebView/src/Maui/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -1,2 +1,8 @@ #nullable enable +Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions +Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler +Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.CreateFileProvider(string! contentRootDir) -> Microsoft.Extensions.FileProviders.IFileProvider! +Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.TryDispatchAsync(System.Action! workItem) -> System.Threading.Tasks.Task! +static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder, System.Func! factory) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! +static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! override Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler.ConnectHandler(Android.Webkit.WebView! platformView) -> void diff --git a/src/BlazorWebView/src/Maui/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/BlazorWebView/src/Maui/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 7dc5c58110bf..4ba43186bc90 100644 --- a/src/BlazorWebView/src/Maui/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/BlazorWebView/src/Maui/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions +Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler +Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.CreateFileProvider(string! contentRootDir) -> Microsoft.Extensions.FileProviders.IFileProvider! +Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.TryDispatchAsync(System.Action! workItem) -> System.Threading.Tasks.Task! +static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder, System.Func! factory) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! +static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! diff --git a/src/BlazorWebView/src/Maui/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/BlazorWebView/src/Maui/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 7dc5c58110bf..4ba43186bc90 100644 --- a/src/BlazorWebView/src/Maui/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/BlazorWebView/src/Maui/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions +Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler +Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.CreateFileProvider(string! contentRootDir) -> Microsoft.Extensions.FileProviders.IFileProvider! +Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.TryDispatchAsync(System.Action! workItem) -> System.Threading.Tasks.Task! +static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder, System.Func! factory) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! +static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! diff --git a/src/BlazorWebView/src/Maui/PublicAPI/net-tizen/PublicAPI.Unshipped.txt b/src/BlazorWebView/src/Maui/PublicAPI/net-tizen/PublicAPI.Unshipped.txt index 7dc5c58110bf..4ba43186bc90 100644 --- a/src/BlazorWebView/src/Maui/PublicAPI/net-tizen/PublicAPI.Unshipped.txt +++ b/src/BlazorWebView/src/Maui/PublicAPI/net-tizen/PublicAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions +Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler +Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.CreateFileProvider(string! contentRootDir) -> Microsoft.Extensions.FileProviders.IFileProvider! +Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.TryDispatchAsync(System.Action! workItem) -> System.Threading.Tasks.Task! +static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder, System.Func! factory) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! +static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! diff --git a/src/BlazorWebView/src/Maui/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/BlazorWebView/src/Maui/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 7dc5c58110bf..4ba43186bc90 100644 --- a/src/BlazorWebView/src/Maui/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/BlazorWebView/src/Maui/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions +Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler +Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.CreateFileProvider(string! contentRootDir) -> Microsoft.Extensions.FileProviders.IFileProvider! +Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.TryDispatchAsync(System.Action! workItem) -> System.Threading.Tasks.Task! +static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder, System.Func! factory) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! +static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! diff --git a/src/BlazorWebView/src/Maui/PublicAPI/net/PublicAPI.Unshipped.txt b/src/BlazorWebView/src/Maui/PublicAPI/net/PublicAPI.Unshipped.txt index 7dc5c58110bf..4ba43186bc90 100644 --- a/src/BlazorWebView/src/Maui/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/BlazorWebView/src/Maui/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions +Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler +Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.CreateFileProvider(string! contentRootDir) -> Microsoft.Extensions.FileProviders.IFileProvider! +Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.TryDispatchAsync(System.Action! workItem) -> System.Threading.Tasks.Task! +static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder, System.Func! factory) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! +static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! diff --git a/src/BlazorWebView/src/Maui/Windows/BlazorWebView.Windows.cs b/src/BlazorWebView/src/Maui/Windows/BlazorWebView.Windows.cs index a19a5308cfe9..9eaaa3fefab1 100644 --- a/src/BlazorWebView/src/Maui/Windows/BlazorWebView.Windows.cs +++ b/src/BlazorWebView/src/Maui/Windows/BlazorWebView.Windows.cs @@ -1,4 +1,5 @@ using System; +using WebView2Control = Microsoft.UI.Xaml.Controls.WebView2; namespace Microsoft.AspNetCore.Components.WebView.Maui { @@ -25,7 +26,8 @@ protected override void OnPropertyChanged(string? propertyName = null) private void Window_Destroying(object? sender, EventArgs e) { // see: https://github.com/microsoft/microsoft-ui-xaml/issues/6872 - ((BlazorWebViewHandler?)Handler)?.PlatformView.Close(); + if (Handler?.PlatformView is WebView2Control webView) + webView.Close(); } } } diff --git a/src/BlazorWebView/tests/DeviceTests/Elements/BlazorWebViewTests.Services.cs b/src/BlazorWebView/tests/DeviceTests/Elements/BlazorWebViewTests.Services.cs index 9a4c8fd9132a..b04efa10d51c 100644 --- a/src/BlazorWebView/tests/DeviceTests/Elements/BlazorWebViewTests.Services.cs +++ b/src/BlazorWebView/tests/DeviceTests/Elements/BlazorWebViewTests.Services.cs @@ -1,8 +1,12 @@ +#nullable enable annotations using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.WebView.Maui; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Hosting; using Microsoft.Maui.MauiBlazorWebView.DeviceTests.Components; using WebViewAppShared; using Xunit; @@ -81,6 +85,33 @@ await InvokeOnMainThreadAsync(async () => }); } + [Fact] + public void BlazorWebViewCreateFileProviderBeforeHandlerThrowsClearException() + { + var exception = Assert.Throws( + () => new BlazorWebView().CreateFileProvider("wwwroot")); + + Assert.Contains("must be connected to a handler", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public async Task BlazorWebViewCreateFileProviderWithIncompatibleHandlerThrowsClearException() + { + await InvokeOnMainThreadAsync(() => + { + var handler = new ViewHandlerStub(); + var blazorWebView = new BlazorWebView(); + handler.SetVirtualView(blazorWebView); + blazorWebView.Handler = handler; + + var exception = Assert.Throws( + () => blazorWebView.CreateFileProvider("wwwroot")); + + Assert.Contains(nameof(ViewHandlerStub), exception.Message, StringComparison.Ordinal); + Assert.Contains(nameof(IBlazorWebViewHandler), exception.Message, StringComparison.Ordinal); + }); + } + [Fact] public async Task BlazorWebViewWithoutDispatchFailsToGetScopedServices() { @@ -119,4 +150,144 @@ await Assert.ThrowsAsync(async () => }); }); } + + [Fact] + public void UsePlatformHandlerGenericReplacesDefaultBlazorWebViewHandler() + { + // Verifies that the public UsePlatformHandler() extension on IMauiBlazorWebViewBuilder + // actually replaces the default BlazorWebViewHandler registered by AddMauiBlazorWebView() + // for the IBlazorWebView service type. The two HostBuilderHandlerTests in Core verify the + // underlying ConfigureMauiHandlers replacement mechanism with stub types; this test exercises + // the new public API surface end-to-end with the real BlazorWebView/IBlazorWebView types. + var builder = MauiApp.CreateBuilder(); + builder.Services.AddMauiBlazorWebView() + .UsePlatformHandler(); + using var app = builder.Build(); + + var handlersFactory = app.Services.GetRequiredService(); + Assert.Equal(typeof(CustomBlazorWebViewHandlerStub), handlersFactory.GetHandlerType(typeof(BlazorWebView))); + } + + [Fact] + public async Task UsePlatformHandlerFactoryReplacesDefaultBlazorWebViewHandler() + { + // Companion to UsePlatformHandlerGenericReplacesDefaultBlazorWebViewHandler — verifies the + // factory overload (Func) also replaces the default handler. + // The factory overload registers a different ServiceDescriptor shape (ImplementationFactory + // rather than ImplementationType), so GetHandlerType returns null here; we resolve through + // GetHandler instead and assert the produced instance type. + var builder = MauiApp.CreateBuilder(); + var factoryWasCalled = false; + builder.Services.AddMauiBlazorWebView() + .UsePlatformHandler(_ => + { + factoryWasCalled = true; + return new CustomBlazorWebViewHandlerStub(); + }); + using var app = builder.Build(); + + var handlersFactory = app.Services.GetRequiredService(); + await InvokeOnMainThreadAsync(() => + { + var handler = handlersFactory.GetHandler(typeof(BlazorWebView)); + + Assert.True(factoryWasCalled, "Factory delegate should have been invoked when the handler was resolved."); + Assert.IsType(handler); + }); + } + + [Fact] + public async Task BlazorWebViewUsesCustomHandlerOperations() + { + await InvokeOnMainThreadAsync(async () => + { + var handler = new CustomBlazorWebViewHandlerStub(); + var blazorWebView = new BlazorWebView(); + handler.SetVirtualView(blazorWebView); + blazorWebView.Handler = handler; + var workItemCalled = false; + + Assert.Same(handler.FileProvider, blazorWebView.CreateFileProvider("wwwroot")); + Assert.True(await blazorWebView.TryDispatchAsync(_ => workItemCalled = true)); + Assert.True(workItemCalled); + }); + } + + private class ViewHandlerStub : IViewHandler +#if ANDROID || IOS || MACCATALYST || WINDOWS + , IPlatformViewHandler +#endif + { + public bool HasContainer { get; set; } + + public object? ContainerView => null; + +#if ANDROID || IOS || MACCATALYST || WINDOWS + public object? PlatformView => _platformView; +#else + public object? PlatformView => null; +#endif + + public IView? VirtualView { get; private set; } + + IElement? IElementHandler.VirtualView => VirtualView; + + public IMauiContext? MauiContext { get; private set; } + + public void SetMauiContext(IMauiContext mauiContext) => MauiContext = mauiContext; + + public void SetVirtualView(IElement view) => VirtualView = (IView)view; + + public void UpdateValue(string property) { } + + public void Invoke(string command, object? args = null) { } + + public void DisconnectHandler() { } + + public Size GetDesiredSize(double widthConstraint, double heightConstraint) => Size.Zero; + + public void PlatformArrange(Rect frame) { } + +#if ANDROID + private readonly global::Android.Views.View _platformView = new(global::Android.App.Application.Context); + + global::Android.Views.View IPlatformViewHandler.PlatformView => _platformView; + + global::Android.Views.View? IPlatformViewHandler.ContainerView => null; +#elif IOS || MACCATALYST + private readonly UIKit.UIView _platformView = new(); + + UIKit.UIView IPlatformViewHandler.PlatformView => _platformView; + + UIKit.UIView? IPlatformViewHandler.ContainerView => null; + + UIKit.UIViewController? IPlatformViewHandler.ViewController => null; +#elif WINDOWS + private readonly Microsoft.UI.Xaml.FrameworkElement _platformView = new Microsoft.UI.Xaml.Controls.Grid(); + + Microsoft.UI.Xaml.FrameworkElement IPlatformViewHandler.PlatformView => _platformView; + + Microsoft.UI.Xaml.FrameworkElement? IPlatformViewHandler.ContainerView => null; +#endif + } + + private class CustomBlazorWebViewHandlerStub : ViewHandlerStub, IBlazorWebViewHandler + { + public IFileProvider FileProvider { get; } = new NullFileProvider(); + + public IFileProvider CreateFileProvider(string contentRootDir) => FileProvider; + + public Task TryDispatchAsync(Action workItem) + { + workItem(EmptyServiceProvider.Instance); + return Task.FromResult(true); + } + } + + private sealed class EmptyServiceProvider : IServiceProvider + { + public static EmptyServiceProvider Instance { get; } = new(); + + public object? GetService(Type serviceType) => null; + } } diff --git a/src/Core/tests/UnitTests/Hosting/HostBuilderHandlerTests.cs b/src/Core/tests/UnitTests/Hosting/HostBuilderHandlerTests.cs index ded85f07a562..f2b2d570bbd0 100644 --- a/src/Core/tests/UnitTests/Hosting/HostBuilderHandlerTests.cs +++ b/src/Core/tests/UnitTests/Hosting/HostBuilderHandlerTests.cs @@ -276,5 +276,39 @@ public void HostBuilderCannotResolveHandlerTypeForServiceRegisteredWithFactory() Type handlerType = mauiHandlersFactory.GetHandlerType(typeof(ViewStub)); Assert.Null(handlerType); } + + [Fact] + public void SecondConfigureMauiHandlersCallReplacesHandler() + { + var mauiApp = MauiApp.CreateBuilder() + .ConfigureMauiHandlers(handlers => handlers.AddHandler()) + .ConfigureMauiHandlers(handlers => handlers.AddHandler()) + .Build(); + + var mauiHandlersFactory = mauiApp.Services.GetRequiredService(); + + var handlerService = mauiHandlersFactory.GetHandler(typeof(ViewStub)); + Assert.NotNull(handlerService); + Assert.IsType(handlerService); + } + + [Fact] + public void FactoryBasedHandlerRegistrationReplacesHandler() + { + var mauiApp = MauiApp.CreateBuilder() + .ConfigureMauiHandlers(handlers => handlers.AddHandler()) + .ConfigureMauiHandlers(handlers => handlers.AddHandler(_ => new AlternateViewHandlerStub())) + .Build(); + + var mauiHandlersFactory = mauiApp.Services.GetRequiredService(); + + var handlerService = mauiHandlersFactory.GetHandler(typeof(ViewStub)); + Assert.NotNull(handlerService); + Assert.IsType(handlerService); + } + + class AlternateViewHandlerStub : ViewHandlerStub + { + } } } \ No newline at end of file