Skip to content
75 changes: 75 additions & 0 deletions src/BlazorWebView/src/Maui/MauiBlazorWebViewBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Maui;
using Microsoft.Maui.Hosting;

namespace Microsoft.AspNetCore.Components.WebView.Maui
Comment thread
kubaflo marked this conversation as resolved.
{
/// <summary>
/// Extension methods for <see cref="IMauiBlazorWebViewBuilder"/>.
/// </summary>
public static class MauiBlazorWebViewBuilderExtensions
{
/// <summary>
/// Registers a custom handler for <see cref="IBlazorWebView"/>, replacing the default
/// <see cref="BlazorWebViewHandler"/> registered by
/// <see cref="M:Microsoft.Extensions.DependencyInjection.BlazorWebViewServiceCollectionExtensions.AddMauiBlazorWebView(Microsoft.Extensions.DependencyInjection.IServiceCollection)"/>.
/// This allows custom platform backends to provide their own BlazorWebView handler
/// while reusing all shared service registrations.
/// </summary>
/// <remarks>
/// Replacement is "last-registration-wins" through the underlying MAUI handler collection.
/// Call this method <em>after</em> <c>AddMauiBlazorWebView()</c> so the custom handler
/// overrides the default registration. If a downstream library calls
/// <c>AddMauiBlazorWebView()</c> 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.
/// </remarks>
/// <typeparam name="THandler">The custom handler type to use for <see cref="IBlazorWebView"/>.
/// Must have a public parameterless constructor.</typeparam>
/// <param name="builder">The <see cref="IMauiBlazorWebViewBuilder"/>.</param>
/// <returns>The <see cref="IMauiBlazorWebViewBuilder"/> for chaining.</returns>
public static IMauiBlazorWebViewBuilder UsePlatformHandler<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>(
this IMauiBlazorWebViewBuilder builder)
where THandler : IViewHandler, new()
Comment thread
kubaflo marked this conversation as resolved.
Outdated
{
ArgumentNullException.ThrowIfNull(builder);
builder.Services.ConfigureMauiHandlers(handlers =>
handlers.AddHandler<IBlazorWebView, THandler>());
return builder;
}

/// <summary>
/// Registers a custom handler for <see cref="IBlazorWebView"/> using a factory method,
/// replacing the default <see cref="BlazorWebViewHandler"/> registered by
/// <see cref="M:Microsoft.Extensions.DependencyInjection.BlazorWebViewServiceCollectionExtensions.AddMauiBlazorWebView(Microsoft.Extensions.DependencyInjection.IServiceCollection)"/>.
/// 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.
/// </summary>
/// <remarks>
/// The <see cref="IServiceProvider"/> passed to <paramref name="factory"/> is the MAUI
/// handler factory's service provider, not the application's root <see cref="IServiceProvider"/>.
/// It can resolve services that were registered on the handler collection (via
/// <c>ConfigureMauiHandlers</c>); it cannot resolve arbitrary services from the app's
/// <see cref="Microsoft.Extensions.DependencyInjection.IServiceCollection"/>. The same call-ordering rule as
/// <see cref="UsePlatformHandler{THandler}(IMauiBlazorWebViewBuilder)"/> applies — call this
/// method after <c>AddMauiBlazorWebView()</c> (and after any later re-invocations from
/// downstream libraries) so the custom handler is the last registration to win.
/// </remarks>
/// <param name="builder">The <see cref="IMauiBlazorWebViewBuilder"/>.</param>
/// <param name="factory">A factory function that creates the handler instance.
/// The <see cref="IServiceProvider"/> argument is the MAUI handler factory's service provider
/// (see remarks).</param>
/// <returns>The <see cref="IMauiBlazorWebViewBuilder"/> for chaining.</returns>
public static IMauiBlazorWebViewBuilder UsePlatformHandler(
this IMauiBlazorWebViewBuilder builder,
Func<IServiceProvider, IViewHandler> factory)
{
Comment thread
kubaflo marked this conversation as resolved.
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(factory);
builder.Services.ConfigureMauiHandlers(handlers =>
handlers.AddHandler<IBlazorWebView>(factory));
return builder;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<System.IServiceProvider!, Microsoft.Maui.IViewHandler!>! factory) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!
static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler<THandler>(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
Original file line number Diff line number Diff line change
@@ -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<System.IServiceProvider!, Microsoft.Maui.IViewHandler!>! factory) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!
static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler<THandler>(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!
Original file line number Diff line number Diff line change
@@ -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<System.IServiceProvider!, Microsoft.Maui.IViewHandler!>! factory) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!
static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler<THandler>(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!
Original file line number Diff line number Diff line change
@@ -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<System.IServiceProvider!, Microsoft.Maui.IViewHandler!>! factory) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!
static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler<THandler>(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!
Original file line number Diff line number Diff line change
@@ -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<System.IServiceProvider!, Microsoft.Maui.IViewHandler!>! factory) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!
static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler<THandler>(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!
Original file line number Diff line number Diff line change
@@ -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<System.IServiceProvider!, Microsoft.Maui.IViewHandler!>! factory) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!
static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler<THandler>(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -119,4 +120,55 @@ await Assert.ThrowsAsync<InvalidOperationException>(async () =>
});
});
}

[Fact]
public void UsePlatformHandlerGenericReplacesDefaultBlazorWebViewHandler()
{
// Verifies that the public UsePlatformHandler<THandler>() 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<CustomBlazorWebViewHandlerStub>();
using var app = builder.Build();

var handlersFactory = app.Services.GetRequiredService<IMauiHandlersFactory>();
Assert.Equal(typeof(CustomBlazorWebViewHandlerStub), handlersFactory.GetHandlerType(typeof(BlazorWebView)));
}

[Fact]
public void UsePlatformHandlerFactoryReplacesDefaultBlazorWebViewHandler()
{
// Companion to UsePlatformHandlerGenericReplacesDefaultBlazorWebViewHandler — verifies the
// factory overload (Func<IServiceProvider, IViewHandler>) 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();
Comment thread
kubaflo marked this conversation as resolved.
});
using var app = builder.Build();

var handlersFactory = app.Services.GetRequiredService<IMauiHandlersFactory>();
var handler = handlersFactory.GetHandler(typeof(BlazorWebView));

Assert.True(factoryWasCalled, "Factory delegate should have been invoked when the handler was resolved.");
Assert.IsType<CustomBlazorWebViewHandlerStub>(handler);
}

private class CustomBlazorWebViewHandlerStub : BlazorWebViewHandler
Comment thread
kubaflo marked this conversation as resolved.
Outdated
{
// 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() { }
}
}
34 changes: 34 additions & 0 deletions src/Core/tests/UnitTests/Hosting/HostBuilderHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IViewStub, ViewHandlerStub>())
.ConfigureMauiHandlers(handlers => handlers.AddHandler<IViewStub, AlternateViewHandlerStub>())
.Build();

var mauiHandlersFactory = mauiApp.Services.GetRequiredService<IMauiHandlersFactory>();

var handlerService = mauiHandlersFactory.GetHandler(typeof(ViewStub));
Assert.NotNull(handlerService);
Assert.IsType<AlternateViewHandlerStub>(handlerService);
}

[Fact]
public void FactoryBasedHandlerRegistrationReplacesHandler()
{
var mauiApp = MauiApp.CreateBuilder()
.ConfigureMauiHandlers(handlers => handlers.AddHandler<IViewStub, ViewHandlerStub>())
.ConfigureMauiHandlers(handlers => handlers.AddHandler<IViewStub>(_ => new AlternateViewHandlerStub()))
.Build();

var mauiHandlersFactory = mauiApp.Services.GetRequiredService<IMauiHandlersFactory>();

var handlerService = mauiHandlersFactory.GetHandler(typeof(ViewStub));
Assert.NotNull(handlerService);
Assert.IsType<AlternateViewHandlerStub>(handlerService);
}

class AlternateViewHandlerStub : ViewHandlerStub
{
}
}
}
Loading