Skip to content
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
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
{
/// <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()
{
ArgumentNullException.ThrowIfNull(builder);
builder.Services.ConfigureMauiHandlers(handlers =>
handlers.AddHandler<IBlazorWebView, THandler>());
return builder;
Comment on lines +32 to +39

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

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

UsePlatformHandler<THandler>() registers the handler via handlers.AddHandler<IBlazorWebView, THandler>(), which ultimately creates handler instances using Activator.CreateInstance(ImplementationType) in MauiFactory. This requires THandler to have a public parameterless constructor; otherwise it will fail at runtime with a MissingMethodException. Consider enforcing this contract with a new() generic constraint, or switch to the factory overload (AddHandler<TType>(Func<IServiceProvider, IElementHandler>)) so handlers with non-parameterless constructors can be created safely.

Copilot uses AI. Check for mistakes.
}

/// <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)
{
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();
});
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
{
// 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