diff --git a/src/BlazorWebView/src/Maui/MauiBlazorWebViewBuilderExtensions.cs b/src/BlazorWebView/src/Maui/MauiBlazorWebViewBuilderExtensions.cs new file mode 100644 index 000000000000..ad9553b07fc5 --- /dev/null +++ b/src/BlazorWebView/src/Maui/MauiBlazorWebViewBuilderExtensions.cs @@ -0,0 +1,75 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Maui; +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 . + /// Must have a public parameterless constructor. + /// The . + /// The for chaining. + public static IMauiBlazorWebViewBuilder UsePlatformHandler<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>( + this IMauiBlazorWebViewBuilder builder) + where THandler : IViewHandler, 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..038b7546d70e 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,5 @@ #nullable enable +Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions +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..79428f930adf 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,4 @@ #nullable enable +Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions +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..79428f930adf 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,4 @@ #nullable enable +Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions +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..79428f930adf 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,4 @@ #nullable enable +Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions +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..79428f930adf 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,4 @@ #nullable enable +Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions +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..79428f930adf 100644 --- a/src/BlazorWebView/src/Maui/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/BlazorWebView/src/Maui/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions +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/tests/DeviceTests/Elements/BlazorWebViewTests.Services.cs b/src/BlazorWebView/tests/DeviceTests/Elements/BlazorWebViewTests.Services.cs index 9a4c8fd9132a..b29a23d27192 100644 --- a/src/BlazorWebView/tests/DeviceTests/Elements/BlazorWebViewTests.Services.cs +++ b/src/BlazorWebView/tests/DeviceTests/Elements/BlazorWebViewTests.Services.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Components.WebView.Maui; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Maui.Hosting; using Microsoft.Maui.MauiBlazorWebView.DeviceTests.Components; using WebViewAppShared; using Xunit; @@ -119,4 +120,55 @@ 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 void 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(); + var handler = handlersFactory.GetHandler(typeof(BlazorWebView)); + + Assert.True(factoryWasCalled, "Factory delegate should have been invoked when the handler was resolved."); + Assert.IsType(handler); + } + + private class CustomBlazorWebViewHandlerStub : BlazorWebViewHandler + { + // Marker subclass used only to prove that UsePlatformHandler replaced the default + // BlazorWebViewHandler registration. Inheriting from BlazorWebViewHandler keeps the + // IViewHandler contract honored on every device-test target framework without forcing + // us to reimplement the full handler surface. + public CustomBlazorWebViewHandlerStub() { } + } } 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