I wanted to host a Blazor WebAssembly application natively as a desktop application, and to achieve that, I planned to use a MAUI Blazor app. However, I wanted to avoid duplicating the Razor pages between both of my projects since I intended to continue using the web version as well. In this blog post, I will show you how to accomplish this. Consider using this approach to host your SaaS application in the cloud and simultaneously offer support for a native desktop app, much like Slack.
To keep things simple, I’ll be using the Tic Tac Toe project we made in our previous blog post. We’ve already created a Blazor WebAssembly app and will now adapt it to run on both desktop and mobile. You can find the complete source code here: mhdbouk/tictactoe-blazor.
What is .NET MAUI?
.NET MAUI is a multi-platform native UI framework developed by Microsoft that allows deployment to multiple devices across mobile and desktop using a single project codebase.
On the other hand, Blazor is also used to build native client apps, but it takes a hybrid approach. Hybrid apps are native applications that make use of web technologies for their functionality. In a Blazor Hybrid app or MAUI Blazor App, Razor components run directly within the native app (rather than on WebAssembly), alongside other .NET code, and they render the web UI (HTML, CSS) to a web view control called BlazorWebView
. Utilizing Blazor with .NET MAUI provides a convenient way to create cross-platform Blazor Hybrid apps for both mobile and desktop.
Setting Up the Development Environment
If you don’t have a Blazor web app and a MAUI Blazor app in your solution, you can easily create them using the following commands or by using the Visual Studio interface.
dotnet workload install wasm-tools // install the blazor wasm tools
dotnet workload install maui // install maui
// Create the solution & the projects
dotnet new sln -n TicTacToe
dotnet new blazorwasm -o TicTacToe.Web
dotnet new maui-blazor -o TicTacToe.Maui
dotnet sln add TicTacToe.Web/TicTacToe.Web.csproj
dotnet sln add TicTacToe.Maui/TicTacToe.Maui.csproj
New Shared
Class Library Project
In your solution, start by creating a new Class Library project named Shared
. This project will serve as a central repository for all the common Blazor functionality, including Razor files, components, pages, and any static CSS/JS files.
After creating the project, proceed to update the .csproj
file by replacing its content with the following:
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="7.0.0" />
</ItemGroup>
</Project>
First, we changed the SDK type from Microsoft.NET.Sdk
to Microsoft.NET.Sdk.Razor
. This is a very important step, as, without it, the Blazor and MAUI projects will not be able to communicate with the Shared project. Also, make sure to add the Microsoft.AspNetCore.Components.Web
NuGet package so we can resolve our Blazor dependencies.
As of the date of writing this blog post, I had to downgrade all my projects to the initial release of .NET 7 due to an unsupported package (Microsoft.Extensions.Logging.Abstractions ).
reference: https://github.com/dotnet/maui/issues/16244
and this is the error messageTicTacToe.Maui.csproj: Error NU1605 : Warning As Error: Detected package downgrade: Microsoft.Extensions.Logging.Abstractions from 7.0.1 to 7.0.0. Reference the package directly from the project to select a different version.
TicTacToe.Maui -> TicTacToe.Shared -> Microsoft.AspNetCore.Components.Web 7.0.11 -> Microsoft.AspNetCore.Components 7.0.11 -> Microsoft.AspNetCore.Authorization 7.0.11 -> Microsoft.Extensions.Logging.Abstractions (>= 7.0.1)
TicTacToe.Maui -> Microsoft.Extensions.Logging.Abstractions (>= 7.0.0)
Move common files to Shared
Start by moving everything that is shared between the two projects. This includes pages, components, and layout files like the MainLayout.razor file. This step will ensure that both projects have access to the same components and resources.
Create a new file called _Imports.razor
in the Shared project and add the following to it:
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
Once you’ve copied all the shared logic, make sure you update all the namespaces to reflect the Shared project. For example, if you are moving files from your web Blazor project and the namespace is called TicTacToe.Web
, rename it to TicTacToe.Shared
:
namespace TicTacToe.Web; // <- change this to
namespace TicTacToe.Shared; // <- this one
Another thing you should do is create a new wwwroot
folder and add all the CSS/JS/assets files from your web project (we will update the references in index.html later in the post).
Update Blazor Web App and MAUI Blazor App projects
We are going to make a few changes in both of our projects (Blazor Web and MAUI Blazor App), These changes are necessary to establish a connection between the projects and make shared components accessible.
First, we are going to add a reference to the Shared project, update the _Imports.razor
file, update the App.razor
& Main.razor
files with the additional assembly attribute, and fix our index.html static file mapping.
_Index.razor
A few things are needed here. First, add a reference to the new Shared project in the web project. Then, add the using
statement in the _Imports.razor
file in both the Blazor Web app and the MAUI Blazor App. This step will let other files like App.razor
or Main.razor
to resolve the shared component and files.
... // other using
@using TicTacToe.Shared;
App.razor
/ Main.razor
We need to update the App.razor
in the Blazor web project and the Main.razor
in the MAUI app project, by including a reference to the MainLayout class using the AdditionalAssemblies attribute. Open both files and update the content with the following:
<Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="new[] { typeof(MainLayout).Assembly }" PreferExactMatches="true">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
Notice how we added AdditionalAssemblies="new[] { typeof(MainLayout).Assembly }."
This is needed to let the project resolve the MainLayout
class from outside of the current assembly.
index.html
Open the index.html file and make sure to reference the static files in the following format:
...
<head>
...
<link href="_content/TicTacToe.Shared/css/app.css" rel="stylesheet" />
...
</head>
...
Start the hypertext reference (href) with _content/{{Shared project Name}}/{{the old file path}}
.
Repeat the same for other files (JS, fonts, …).
While working on this, XCode automatically updated to version 15, and unfortunately, MAUI in .NET 7 does not yet offer support for this version. The good news is that it will be supported in .NET 8. To address this situation, I had to use Xcodes to install the older version of Xcode, allowing me to run the app on an iPhone with the older iOS 16 simulation.
Conclusion
In completing these steps, you’ve successfully achieved seamless integration between your Blazor WebAssembly application and the MAUI platform, allowing you to run your app both in a web browser and as a desktop or mobile application. Now, it’s time to enjoy the flexibility of this setup. Happy coding!