Update: We now have an official tutorial and sample in the Blazor hybrid docs: Build a .NET MAUI Blazor Hybrid app with a Blazor Web App. Starting in .NET 9 Preview 5, we also have a solution template that will set this up automatically for you. Take a look at the .NET 9 Preview 5 release notes for more info or the 9.0 version of the tutorial.
This repo demonstrates a starter solution that contains a MAUI hybrid (native, cross-platform) app, a Blazor web app and a Razor class library that contains all the shared UI that is used by both native and web apps.
It also demonstrates how to use Blazor render modes on the web app but ignore them in the MAUI app. It does this by setting up a helper class in the RCL called InteractiveRenderSettings
that has properties that are used as render mode in the components while running in the web app. The MauiProgram.cs
sets these properties to null
so they are ignored on the client instead of throwing an exception.
To manually set this up yourself in Visual Studio, follow these steps
-
Create an empty Solution and name it
MyApp
-
Add new project MAUI Blazor Hybrid app and name it
MyApp.MAUI
-
Add new project Blazor Web App and name it
MyApp.Web
. Select the following options:a. Authentication type = none
b. Configure for HTTPS is checked
c. Interactive render mode = Server
d. Interactivity location = Global <-- This setting is important because hybrid apps always run interactive and will throw errors on pages or components that explicitly specify a render mode. See #51235. If you do not use a global render mode, you will need to implement the pattern shown in this repository.
e. Uncheck Include sample pages
-
Add new project Razor Class Library (RCL) and name it
MyApp.Shared
a. don't select "support pages and views" (default)
-
Now add project references to
MyApp.Shared
from bothMyApp.MAUI
&MyApp.Web
project -
Move the
Components
folder and all of its contents fromMyApp.MAUI
toMyApp.Shared
(Ctrl+X, Ctrl+V) -
Move
wwwroot/css
folder and all of its contents from fromMyApp.MAUI
toMyApp.Shared
(Ctrl+X, Ctrl+V) -
Move
_Imports.razor
fromMyApp.MAUI
toMyApp.Shared
(overwrite the one that is there) and rename the last two@using
s toMyApp.Shared
...
@using MyApp.Shared
@using MyApp.Shared.Components
- Open the
_Imports.razor
inMyApp.Web
add a@using
toMyApp.Shared
...
@using MyApp.Shared
-
Move
Routes.razor
fromMyApp.MAUI
toMyApp.Shared
(Ctrl+X, Ctrl+V). -
Open the
Routes.razor
file and changeMauiProgram
toRoutes
:
<Router AppAssembly="@typeof(Routes).Assembly">
...
- Open the
MainPage.xaml
in theMyApp.MAUI
project and add axmlns:shared
reference to theMyApp.Shared
RCL and update theBlazorWebView
RootComponent
ComponentType
fromlocal
toshared
:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MyApp.MAUI"
xmlns:shared="clr-namespace:MyApp.Shared;assembly=MyApp.Shared"
x:Class="MyApp.MAUI.MainPage"
BackgroundColor="{DynamicResource PageBackgroundColor}">
<BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/index.html">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app" ComponentType="{x:Type shared:Routes}" />
</BlazorWebView.RootComponents>
</BlazorWebView>
</ContentPage>
- In the
MyApp.MAUI
project openwwwroot/index.html
and change stylesheets to point to_content/MyApp.Shared/
:
<link rel="stylesheet" href="_content/MyApp.Shared/css/bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="_content/MyApp.Shared/css/app.css" />
- Open
App.razor
fromMyApp.Web
projectComponents
folder and add the stylesheet references to theMyApp.Shared
there too:
<link rel="stylesheet" href="_content/MyApp.Shared/css/bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="_content/MyApp.Shared/css/app.css" />
-
In the
MyApp.Web
project, delete filesRoutes.razor
,Layouts
folder & all its contents, andPages\Home.razor
(leave theError.razor
page) -
Open
MyApp.Web
projectProgram.cs
file andAddAddionalAssemblies
toMapRazorComponents
:
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddAdditionalAssemblies(typeof(MyApp.Shared._Imports).Assembly);
You should now be all set! F5 and party on.
This sample also shows how to use interfaces on the UI to call into different implementations across the web app and the native (MAUI Hybrid) app. We will make a component that displays the device form factor. We can use the MAUI abstraction layer for all the native apps but we will need to provide our own implementation for the web app.
- In the
MyApp.Shared
project, create anInterfaces
folder and add file calledIFormFactor.cs
with the following code:
namespace MyApp.Shared.Interfaces
{
public interface IFormFactor
{
public string GetFormFactor();
public string GetPlatform();
}
}
- Move
Component1.razor
in theMyApp.Shared
project toComponents
folder and add the following code:
@using MyApp.Shared.Interfaces
@inject IFormFactor FormFactor
<div class="my-component">
<p>You are running on:</p>
<h3>@factor</h3>
<h3>@platform</h3>
<em>This component is defined in the <strong>MyApp.Shared</strong> library.</em>
</div>
@code {
private string factor => FormFactor.GetFormFactor();
private string platform => FormFactor.GetPlatform();
}
- Now that we have the interface defined we need to provide implementations in the web and native apps. In the
MyApp.Web
project, add a folder calledServices
and add a file calledFormFactor.cs
. Add the following code:
using MyApp.Shared.Interfaces;
namespace MyApp.Web.Services
{
public class FormFactor : IFormFactor
{
public string GetFormFactor()
{
return "Web";
}
public string GetPlatform()
{
return Environment.OSVersion.ToString();
}
}
}
- Now in the
MyApp.MAUI
project, add a folder calledServices
and add a file calledFormFactor.cs
. We can use the MAUI abstractions layer to write code that will work on all the native device platforms. Add the following code:
using MyApp.Shared.Interfaces;
namespace MyApp.MAUI.Services
{
public class FormFactor : IFormFactor
{
public string GetFormFactor()
{
return DeviceInfo.Idiom.ToString();
}
public string GetPlatform()
{
return DeviceInfo.Platform.ToString() + " - " + DeviceInfo.VersionString;
}
}
}
- Use dependency injection to get the implementations of these services into the right place. In the
MyApp.MAUI
project openMauiProgram.cs
and add theusing
s at the top:
using Microsoft.Extensions.Logging;
using MyApp.MAUI.Services;
using MyApp.Shared.Interfaces;
- And right before the call to
builder.Build();
add the following code:
...
// Add device specific services used by Razor Class Library (MyApp.Shared)
builder.Services.AddSingleton<IFormFactor, FormFactor>();
return builder.Build();
- Similarly, in the
MyApp.Web
project, open theProgram.cs
and right before the call tobuilder.Build();
add theusing
s at the top:
using MyApp.Web.Components;
using MyApp.Shared.Interfaces;
using MyApp.Web.Services;
- And right before the call to
builder.Build();
add the following code:
...
// Add device specific services used by Razor Class Library (MyApp.Shared)
builder.Services.AddScoped<IFormFactor, FormFactor>();
var app = builder.Build();
You can also use compiler preprocessor directives in your RCL to implement different UI depending on the device you are running on. In that case you need to multi-target your RCL like you do in your MAUI app. For an example of that see: BethMassi/BethTimeUntil repo.
That's it! Have fun.