My thanks to Eric Grover (@Eric_Grover), Gerald Versluis (@jfversluis) and James Montemagno (@JamesMontemagno) for their help whilst I was researching this topic.
The short answer is that you need to create a common interface and separate implementations for the MAUI and WASM projects.
The MAUI team built MAUI Essentials directly into the MAUI Project templates. This makes it really easy to use native functionality inside MAUI applications and its implementation is truly fantastic – so a big thank you to the MAUI Essentials team! The downside for Blazor developers is that there isn’t a MAUI Essentials project that you can simply include via NuGet into a standard Blazor Class Library.
To get around this you create a MAUI Class Library (which has access to MAUI Essentials). You can then add this MAUI Class Library as either a project reference or through a published NuGet package into your Blazor Class Library. James pointed out there there is a property (not currently well-documented) that gets added to your .csproj file that enables you to use MAUI (<UseMauiEssentials/>), when I checked my example MAUI Class Library I noted the following:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net6.0-android;net6.0-ios;net6.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net6.0-windows10.0.19041.0</TargetFrameworks>
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
<!-- <TargetFrameworks>$(TargetFrameworks);net6.0-tizen</TargetFrameworks> -->
<RootNamespace>GreatIdeaz.trellispark.UX.MAUI.Services</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">14.2</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">14.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\UX-WASM-Services\UX-WASM-Services\UX-WASM-Services.csproj" />
</ItemGroup>
</Project>
For my demonstration I wanted to add the Geolocation functionality into an Address component in a Blazor Class Library.
Starting with a MAUI Class Library project I created an Interface.
public class GI_Location
{
public double Latitude = -1;
public double Longitude = -1;
}
public interface IGI_Geolocation
{
public bool isAvailable { get; }
public GI_Location CurrentLocation { get; }
public Task GetCurrentLocation();
}
The isAvailable flag will be used by the Blazor component to determine whether the Geolocation functionality is available in the application. Assuming that Geolocation is available, then the Blazor component can call the GetCurrentLocation async function and retrieve the result from the CurrentLocation property.
I then created a WASM Class that implements the IGI_Geolocation interface which assumes that Geolocation is not available. In a real production system we may implement some other functionality that could return location data through other means.
public class GI_WASM_Geolocation : IGI_Geolocation
{
public bool isAvailable
{
get { return false; }
}
GI_Location mLocation = new();
public GI_Location CurrentLocation
{
get { return mLocation; }
}
public Task GetCurrentLocation()
{
throw new Exception("Geolocation not available");
}
}
Next I added a MAUI Class that implements the IGI_Geolocation interface which assumes that Geolocation is available from the MAUI Essentials.
public class GI_MAUI_Geolocation : IGI_Geolocation
{
public bool isAvailable
{
get { return true; }
}
GI_Location mLocation = new();
public GI_Location CurrentLocation
{
get { return mLocation; }
}
public async Task GetCurrentLocation()
{
try
{
if (Geolocation.Default != null)
{
// Need to expose the location returned from MAUI in a type that will be understood by Blazor
Location location;
location = await Geolocation.GetLocationAsync();
if (location != null)
{
CurrentLocation.Latitude = location.Latitude;
CurrentLocation.Longitude = location.Longitude;
}
}
}
catch (Exception e)
{
// log the exception
}
}
}
Now in my Blazor Class Library I can simply include a reference to my MAUI Class library and use dependency injection to access the shared interface.
_Imports.razor:
@inject IGI_Geolocation Geolocation
Address.razor:
@if (Geolocation.isAvailable)
{
<table width="100%">
<tr>
<td width="20%">
<TelerikButton OnClick="@Geolocation.GetCurrentLocation">Refresh</TelerikButton>
</td>
<td width="30%">
Lat: @Geolocation.CurrentLocation.Latitude.ToString()
</td>
<td width="30%">
Long: @Geolocation.CurrentLocation.Longitude.ToString()
</td>
</tr>
</table>
}
else
{
<p>Geolocation not available</p>
}
In my WASM Client Project I now have to include BOTH my Blazor Class Library and my MAUI Class Library add the following:
_Imports.razor:
@inject IGI_Geolocation Geolocation
Program.cs:
builder.Services.AddSingleton<IGI_Geolocation, GI_WASM_Geolocation>();
This builds the WASM implementation of the IGI_Geolocation interface into my application which sends my Address Blazor component down the else path to show a “Geolocation not available” message.
In my MAUI project the process is similar:
_Imports.razor:
@inject IGI_Geolocation Geolocation
MauiProgram.cs:
builder.Services.AddSingleton<IGI_Geolocation, GI_MAUI_Geolocation>();
This builds in the MAUI implementation which makes the MAUI Essentials functionality available.
To actually make this work on a device, you must also set up the device permissions to allow your application to access the native functionality. To get the required settings you need to go to the MAUI Essentials documentation. In this case I needed:
https://docs.microsoft.com/en-us/dotnet/maui/platform-integration/device/geolocation?tabs=android
Under the Getting Started is a tab that details how you need to modify your platform specific configuration files to request access to the Geolocation functionality.
You can build and publish your MAUI Class Library in the same way as your Blazor Class Library. In my case, I simply cloned the Blazor Class Library devops Azure build pipeline and changed the Repo and folder locations. Note: if this is the first time you try to build for MAUI on your build machine, then you need to ensure that you have also installed all of the necessary MAUI workloads on your Visual Studio. Caught me out first time 🙂
In real production what would I do differently?
- Put the Interface in a Shared Blazor Class Library
- Put the WASM Implementation in a Services Blazor Class Library – the WASM build no longer needs the MAUI Class Library reference
- Leave the MAUI Implementation is a Services MAUI Class Library
Final thoughts?
As with most things in IT when you don’t know how to do it you can spend days figuring this out. It took me a couple of days and the help of Eric, Gerald and James to research this and get it working correctly. However, once you find out how it all works it is surprisingly easy.
Massive thanks to the Blazor and MAUI teams – they have built an awesome framework and the wait has been well worth while!