Blazor and Xamarin Code Sharing
Blazor + Xamarin Code Sharing
I love C# and I love code reuse.
This is what drew me to Xamarin. When I first heard about Xamarin.Forms I was astounded to find out it was possible to write one app and run it on both iOS and Android.
iOS and Android sharing is all well and good, but I want my code to be shared everywhere. I want the same classes in the browser and on the server too. This was possible to a certain extent with Portable Class Libraries, but it got a whole lot better with .Net Standard, and now that we have Blazor, this is even easier and broader reaching.
For the examples in this blog I’ll be using an app I’m building for the Melbourne Xamarin and Blazor Meetup. The app is called Melbourne Modern Apps, and it will have a list of presenters and presentations from our meetup.
You can try it out at https://melbournexamarinblazor.z26.web.core.windows.net/ or checkout the sourcecode https://github.com/lachlanwgordon/MelbourneModernApps/tree/BlogPost1.
This project will be updated with more pages and features so the latest content may be quite different to the content of this blog post depending on when you’re reading this. The tag “BlogPost1” in the link will refer to the repo as it was at the time of writing this blog post.
Architecture
A new Xamarin.Forms app will have a solution structure something like this
Here we have all the shared code inside the MelbourneModernApps project. This is fine for any platforms with Xamarin.Forms, but if you want to use it more elsewhere you might encounter a couple of issues, including:
Many classes are not reusable: e.g. xaml views Nuget Dependencies: I love nugets, but I don’t need Xamarin Forms clogging up my server. Build Times: unused code + unused nugets = unnecessary build time. To address these issues, I split it up using a .Net Standard project called MelbourneModernApp.Core which holds my Models, ViewModels and Services. This means the Forms project only needs to look after mobile specific code.
I’ve also added in a project for my Blazor Web Assembly (WASM) site. Instructions for spinning up a new Blazor site are here. https://docs.microsoft.com/en-us/aspnet/core/blazor/get-started?view=aspnetcore-3.1&tabs=visual-studio.
Along with adding projects and moving some code I’ve also had to add some references (Right Click Dependecies> Add Reference). ALL the projects (.iOS, .Droid, .Forms, .Blazor) need a reference to the Core project. In addition to this, iOS and Android need a reference to .Forms as well but this should have already been set up.
Challenges
This structure creates a few restrictions compared to a typical Xamarin.Forms app. For the most part these restrictions are actually enforcing good practice of how we would want to write apps anyway, following the MVVM pattern, but we all like to break those rules from time to time.
Some of the issues I’ve come across are:
Colors – Without Xamarin Forms in our ViewModels we can use a hex string or value converters. Commands – No Xamarin Forms Commands so I like to use MVVMHelpers. Plugins – If you’ve got a plugin that depends on a platform, you can shift that code into your views, or use dependency injection to inject an alternative implementation. Navigation – As with plugins, you can handle navigation in your views, or abstract it out and handle it into a service that has implementations for each platform.
Feature Structure
Every feature in my app I want to be available on the mobile app and on the web. As an example, here’s my PresentersPage…
Demo app running in Blazor on Chrome on a mac and the Xamarin app running in iOS and Android. I start by making a ViewModel, and then write two different views to use it. My ‘PresentersViewModel.cs’ is in the core project. It’s got two Commands, an ObservableCollection, a NavigationService, and a DataStore.
public class PresentersViewModel : BaseViewModel
{
IDataStore<Presenter> DataStore;
INavigationService NavigationService;
public ObservableRangeCollection<Presenter> Items { get; set; } = new ObservableRangeCollection<Presenter>();
public ICommand LoadItemsCommand => new AsyncCommand(LoadItems);
public ICommand OpenPresenterCommand => new AsyncCommand<Presenter>(OpenPresenter);
public PresentersViewModel(IDataStore<Presenter> dataStore, INavigationService navigationService)
{
DataStore = dataStore;
NavigationService = navigationService;
}
public async Task LoadItems()
{
IsBusy = true;
Items.Clear();
var items = await DataStore.GetItemsAsync(true);
Items.AddRange(items);
IsBusy = false;
}
public async Task OpenPresenter(Presenter presenter = null)
{
if (presenter == null)
{
await NavigationService.NavigateToPageAsync($"presenters/detail");
}
else
{
await NavigationService.NavigateToPageAsync($"presenters/detail", "presenter", presenter.Id);
}
}
}
I won’t go into detail here about what’s in the ‘Presenter’ model and DataStore or this post will get really long, but you can check them out on github.
In my ‘PresentersPage.xaml’ in the Forms project I’ve got a CollectionView to display the list of presenters, and a ToolBar Item. Both of which use the ViewModel for all of their data and for the commands they call.
<?xml version="1.0" encoding="utf-8"?>
<ContentPage
BackgroundColor="{StaticResource PageBackground}"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="MelbourneModernApps.Views.PresentersPage"
x:Name="BrowseItemsPage"
xmlns:controls="clr-namespace:MelbourneModernApps.Controls">
<ContentPage.ToolbarItems>
<ToolbarItem
Text="Add"
Command="{Binding OpenPresenterCommand}" />
</ContentPage.ToolbarItems>
<RefreshView
IsRefreshing="{Binding IsBusy, Mode=TwoWay}"
Command="{Binding LoadItemsCommand}">
<CollectionView
Margin="10"
x:Name="ItemsCollectionView"
ItemsSource="{Binding Items}">
<CollectionView.ItemsLayout>
<LinearItemsLayout
Orientation="Vertical"
ItemSpacing="20" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid>
<controls:CardLayout
Clicked="OnItemSelected" />
<Grid.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding BindingContext.OpenPresenterCommand, Source={x:Reference BrowseItemsPage}}"
CommandParameter="{Binding .}">
</TapGestureRecognizer>
</Grid.GestureRecognizers>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</RefreshView>
</ContentPage>
In my Blazor project, I have a similarly titled PresentersPage.razor. It covers all the same functionality as the xaml but using Razor Components and HTML instead of XAML with the Xamarin.FormsControls.
@page "/presenters"
@page "/"
@using MelbourneModernApp.Core.Models
<h1>Presenters</h1>
<p>Presenters and Melbourne Meetup.</p>
@if (VM.Items == null || VM.Items.Count == 0)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Picture</th>
<th>Name</th>
<th>Blurb</th>
<th>Twitter</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var presenter in VM.Items)
{
<tr>
<td><img src="@presenter.ImageUrl" class="circle-image" /></td>
<td>@presenter.Name</td>
<td>@presenter.Description</td>
<td>@presenter.TwitterHandle</td>
<td><MatIconButton Icon="edit" OnClick="@(() => VM. OpenPresenter(presenter))"></MatIconButton></td>
</tr>
}
</tbody>
</table>
}
Both these pages have code behind them, which I’ve omitted for brevity. Once again, you can find them in the github repo if you’re curious.
These two alternative implementations of views, sitting on top of a shared ViewModel (and everything behind it), are the essence of the pattern I’m using for code sharing.
Dependency Injection
I wasn’t always a fan of Dependency Injection and IOC, which was initially because I didn’t really understand it, in addition to it being a barrier to entry for unfamiliar devs in a codebase. For me, it seemed any explanation that was simple enough to explain how to use it, was also so simple that not much was gained. Any serious explanation of why to use it would end up with my eyes glazing over. Not meaning to cast any shade on the authors, teachers and friends who tried, I think I just had to spend a few years bumbling around and getting burned before I could appreciate DI.
But now I’m all in.
For a project like this, DI is essential. My ViewModel needs access to a NavigationService, which may be built on top of Xamarin Forms Shell, or Blazor’s Navigation Manager. For the ViewModels code to work in either situation it can’t know about the difference. Similarly my DataService will need to be built on a Rest Api if I’m running BlazorWasm, but it can talk to the database directly if running server side.
It doesn’t matter much which DI container I use, as long as it plays nicely in Forms and Blazor (this rules out the Xamarin Dependency Service). I’ve settled on Microsoft.Extensions.DependencyInjection, largely because it’s baked into the .Net Core templates and is well documented for use there.
Xamarin Startup
In my App.xaml.cs I set up a ServiceCollection for all my dependencies. There’s a blog post from James Montemagno that goes into more detail on this.
InitializeComponent();
var services = new ServiceCollection();
services.AddSingleton<IDataStore<Presenter>, PresenterDataStore>();
services.AddSingleton<INavigationService, NavigationService>();
services.AddTransient<PresentersViewModel>();
services.AddTransient<PresenterDetailViewModel>();
var serviceProvider = services.BuildServiceProvider(validateScopes: true);
var scope = serviceProvider.CreateScope();
Container.Current.Services = serviceProvider;
MainPage = new AppShell();
The main point to note is that the NavigationService here is my XamarinForms Shell Based Navigation service, and that I also register my ViewModels, so that any ViewModel that asks for a NavigationService will get the correct implementation of INavigationService.
Blazor Startup
In Program.cs/Startup.cs the Service Collection will already be set up if you’ve created your blazor apps from a template. I’ve added in my ViewModels, as shown below.
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddBaseAddressHttpClient();
builder.Services.AddSingleton<IDataStore<Presenter>, PresenterDataStore>();
builder.Services.AddTransient<PresentersViewModel>();
builder.Services.AddTransient<PresenterDetailViewModel>();
builder.Services.AddTransient<INavigationService, NavigationService>();
var host = builder.Build();
Container.Current.Services = host.Services;
await host.RunAsync();
Navigation
I like to handle Navigation in my ViewModels. Most MVVM frameworks seem to also like this pattern, but my main reason is that it’s usually when I’m using a ViewModel method that I can make the right decision about what should come next. In this situation it plays nicely because that means my navigation is reusable.
To make this work I start by writing an interface INavigationService which exposes any Navigation methods I need. For each of them I’ll need to implement the method in Blazor and in Xamarin.Forms.
public interface INavigationService
{
Task NavigateToPageAsync(string url);
Task NavigateToPageAsync(string url, string parameterKey, string parameterValue);
}
This is a very simple navigation service, with only two methods, and when needed I can expand it. My navigation is based on URLs and string parameters. I’ve opted for this approach because it is easy to implement in both Blazor and with Xamarin.Forms Shell navigation. You could also use other navigation schemes e.g. ViewModel types instead of URLs or POCOs (Plain Old Class Objects) instead of strings. While other approaches have benefits and may be easy on one platform, the underlying navgation can make it challenging on the other.
My Forms implementation works out as only one line for each method using Shell navigation.
public class NavigationService : INavigationService
{
public async Task NavigateToPageAsync(string url)
{
await Shell.Current.GoToAsync($"/{url}");
}
public async Task NavigateToPageAsync(string url, string parameterKey, string parameterValue)
{
await Shell.Current.GoToAsync($"/{url}?{parameterKey}={parameterValue}");
}
}
My Blazor implementation isn’t much more complex, although it does show a few signs of the compromises made to allow this abstraction to sit on top of two implementations. While the Shell Navigation methods are async, Blazor methods are synchronous so we’re returning Task.CompleteTask which feels like noise that doesn’t really achieve anything. The URL structure is also a little different because .Net Core uses / separated routes instead of query string syntax.
public class NavigationService : INavigationService
{
public NavigationService(NavigationManager navigationManager)
{
NavigationManager = navigationManager;
}
public NavigationManager NavigationManager { get; }
public Task NavigateToPageAsync(string url)
{
NavigationManager.NavigateTo($"/{url}");
return Task.CompletedTask;
}
public Task NavigateToPageAsync(string url, string parameterKey, string parameterValue)
{
NavigationManager.NavigateTo($"/{url}/{parameterValue}");
return Task.CompletedTask;
}
}
Adding Blazor To An Existing App
This post has been an overview of the pieces needed to start a project from scratch. In my next blog post, I’ll be applying this pattern to add a Blazor UI to the Travel Monkey app built by Steven Thewissen, Gerald Versluis and Matt Soucoup for the The Cognitive Services + Xamarin Combo Challenge!.
Resources The sample app used for this blog is available on github https://github.com/lachlanwgordon/MelbourneModernApps/tree/BlogPost1.
This app was also used for my presentation “Blazor For Xamarin Developers” at our Melbourne Blazor and Xamarin Meetup. The slides are available here and the recording in on youtube.
I developed most of this application live on Twitch, you can watch me on twitch at twitch.tv/lachlanwgordon. The recordings are available on youtube.