Xamarin.Forms is a powerful toolkit to build cross-platform mobile apps in the .NET ecosystem. It’s fast, stable, and relies on a mature development stack. Unfortunately, it’s only a toolkit and not a full-featured framework. This can make it tough to start a new app or to keep an existing app maintainable as there’s no “correct” way to organize your code.
Thankfully using some common .NET packages and techniques, it’s easy to build a simple, extendable, and unit-testable MVVM setup in Xamarin.Forms. AWH Xamarin Flow is the setup we’ve developed at AWH to make it easy to build an app from the ground up and to keep it maintainable as you go.
I’ve created a sample Xamarin.Forms app using the AWH Xamarin Flow setup and have put the code in a public repo so you can follow along with this post. I recommend really digging through the code as I won’t be covering every aspect of the Flow. Instead, I’m going to call attention to some key pieces, look at why they are important, and how you may want to adapt the Flow to suit your app’s needs.
The heart of the Flow is the Startup class. Normally, in a Xamarin.Forms app each platform would initialize a new instance of the App class. On Android, this is done in MainActivity.cs and on iOS this is done in AppDelegate.cs.
In the Flow, we have created an Initialize method on the static Startup class. This method will create a new HostBuilder (from the Microsoft.Extensions.Hosting NuGet package), that will do things like register services and configuration, then return an instance of the App class. We can then call this method in each platform’s project in place of creating the App instance directly.
Using the HostBuilder lets us take advantage of features used in other .NET applications like dependency injection. It also takes an Action parameter which will let each platform-specific project register services that can use native platform APIs. If your app doesn’t need native services, then you can simply remove the parameter from Initialize and remove the .ConfigureServices() line which uses that parameter.
Let’s look at two of the main advantages of using HostBuilder: configuration and dependency injection.
One of the main advantages to using HostBuilder in the Flow is the ability to use familiar appsettings.*.json files for configuration. We can load JSON files embedded in the shared project and register them as sources of configuration values. These values can then be injected using an IConfiguration instance into whatever class will need them (though you will likely want to create a wrapper service to make your values strongly-typed).
// The GetManifestResourceStream functions cannot be executed from within the
// ConfigureHostConfiguration's configureDelegate because that causes a "Stream
// was not readable" ArgumentException.
var assembly = Assembly.GetExecutingAssembly();
var environment = Constants.Environment.Current;
using var appSettingsStream = assembly.GetManifestResourceStream("XamSample.appsettings.json");
using var appSettingsEnvironmentStream = assembly.GetManifestResourceStream($"XamSample.appsettings.{environment}.json");
var host = new HostBuilder()
// (Removed some code for brevity)
.ConfigureHostConfiguration(config =>
{
config.AddJsonStream(appSettingsStream);
config.AddJsonStream(appSettingsEnvironmentStream);
})
// (Removed some code for brevity)
Using a Constants.Environment class lets change what files are included based on the project's current build configuration. For instance, in an API-driven app we may have different URLs for the dev, test, UAT, and production APIs. Having each of those as different build configurations will let us build the app so it can pull in the correct settings to connect to the correct API.
Your app may not need configuration files like this, or it may not need environment-specific files. You can customize what files are loaded and even how they are loaded. There are many options available for loading configurations, but we prefer using appsettings.json as it is the most common pattern.
Dependency Injection
The other main advantage of using HostBuilder in the Flow is that we can use dependency injection. ASP.NET Core popularized a style of constructor injection that should be very familiar to any experienced .NET developer. By using the ConfigureServices method on the HostBuilder instance, we can register all of our application's services, views, and view models so they can receive dependencies and so they can be dependencies. We even register our App class so it can receive an instance of the NavigatorService (more on that later).
public static App Initialize(Action<HostBuilderContext, IServiceCollection> configureNativeServices)
{
// (Removed some code for brevity)
var host = new HostBuilder()
// (Removed some code for brevity)
.ConfigureServices(configureNativeServices)
.ConfigureServices(ConfigureServices)
// (Removed some code for brevity)
.Build();
App.ServiceProvider = host.Services;
return App.ServiceProvider.GetService<App>()
?? throw new Exception("The App service provider isn't set up properly.");
}
private static void ConfigureServices(HostBuilderContext context, IServiceCollection services)
{
// Custom services for this app. Each service should have a matching
// interface so it can be mocked for unit tests.
services.AddSingleton<IPageService, PageService>();
services.AddSingleton<INavigatorService, NavigatorService>();
// Add Pages and their ViewModels as transients so they can be retrieved using DI
services.AddTransient<ItemListPage>();
services.AddTransient<ItemListViewModel>();
services.AddTransient<ItemDetailPage>();
services.AddTransient<ItemDetailViewModel>();
// Finally register the App so it can access the NavigatorService
services.AddSingleton<App>();
}
One notable thing we do is to set the static ServiceProvider property of the App class to the finished collection of registered services. This is useful when we need to dynamically pull pages and view models in the PageService and in rare cases where we don’t have access to normal constructor injection. We also use that to retrieve the singleton instance of the App class we registered which will be returned to the platform projects to be loaded on application start.
A note about service lifetimes: normally, we have three registration types to choose from (singleton, scoped, and transient) but since we are not in a request/response environment (like a web application or API), we can’t use a scoped service so only singletons or transients should be registered.
Having a solid dependency injection setup in the AWH Xamarin Flow helps keep our services, view models, and other classes loosely-coupled. This in turn, makes unit testing a lot easier since we can more readily mock dependencies. As a bonus, you can include the Xamarin.Essentials.Interfaces package which includes generated interfaces for the device features included in the Xamarin.Essentials package. That package makes it even easier to make your code testable.
One of the key aspects of any MVVM setup is the relationship between the view and view model. You need to make sure they are paired up correctly and with the correct separation between them. This is typically done using a “View Model Locator” class which maps views to view models. In the AWH Xamarin Flow, we use the PageService (detailed in the next section) to load pages and find the correct view model. But the service only receives the type of page and not the view model type. In order to keep them linked, we use an interface called IPageWithViewModel which every page should implement:
public interface IPageWithViewModel
{
Type ViewModelType { get; }
}
Then each page implements the interface in the code-behind class and set’s what view model it requires:
public partial class ItemDetailPage : ContentPage, IPageWithViewModel
{
public Type ViewModelType => typeof(ItemDetailViewModel);
public ItemDetailPage() => InitializeComponent();
}
This ensures that we can easily get the correct view model for the page while keeping the view and view model loosely-coupled. We want the view to not directly deal with the view model but to know only what view model it requires. For the view model, it should know nothing about the view where it is used.
Base View Models
If you're digging through the code, you may notice something strange in the ViewModels folder: there are two base view models. We have separated out BaseViewModel and BasePageViewModel. The BaseViewModel class contains our implementation of INotifyPropertyChanged which is necessary for our bindings in XAML to work. The BasePageViewModel class contains a NavigatorService instance and two lifecycle methods: InitializeAsync and RefreshAsync. The benefit of separating these comes when you have view models that represent smaller visual data objects than a whole page. This could be a view model that represents an item in a ListView where you still need the INotifyPropertyChanged goodness, but don't need the NavigatorService.
The lifecycle methods are incredibly useful for correctly calling async methods when the page is loaded (or returned to). You don't need to use any trickery to call an async method from the constructor and you should never need to use an async void method. This is a particularly difficult problem, especially for new Xamarin developers, and these methods are an extremely valuable aspect of the Flow.
Also noteworthy is how we've implemented INotifyPropertyChanged. The SetProperty method is the cleanest and simplest way to handle a property with a backing field while still invoking the PropertyChanged event as needed. This implementation requires the least amount of boilerplate code for any given property on a view model:
private string _title = string.Empty;
public string Title
{
get => _title;
set => SetProperty(ref _title, value);
}
The PageService mentioned earlier takes the role of the “View Model Locator” in the AWH Xamarin Flow. It’s responsible for getting an instance of a page by type, getting an instance of the page’s view model, and setting the view model as the binding context of the page. It does all of this using the ServiceProvider property of the App class so it leans on the dependency injection we set up in Startup.cs.
public Page GetPage<PageType>()
{
if (App.ServiceProvider == null)
throw new InvalidOperationException("App.ServiceProvider is null and has not been set up correctly");
var page = App.ServiceProvider.GetService<PageType>() as Page;
if (page == null)
throw new ArgumentException("Unable to locate page in ServiceProvider", nameof(PageType));
if (page is IPageWithViewModel pageWithViewModel)
{
var viewModel = App.ServiceProvider.GetService(pageWithViewModel.ViewModelType) as BasePageViewModel;
if (viewModel == null)
throw new ArgumentException("Unable to locate page view model in ServiceProvider", pageWithViewModel.ViewModelType.Name);
page.BindingContext = viewModel;
}
return page;
}
Most of the time your app will not (and should not) deal with the PageService directly and instead will interact with the NavigatorService.
The NavigatorService relies heavily on the PageService and acts as a wrapper around a Xamarin.Forms NavigationPage instance. It provides the methods you may need for basic app navigation like pushing and popping pages to and from the navigation stack, as well as similar methods for modal pages. Depending on your app’s UI, though, the NavigatorService may not be a perfect fit as is. You may need a different service that serves a similar role but wraps a TabbedPage or a FlyoutPage. Of all the pieces in the AWH Xamarin Flow, this one may require the most changes for any particular app.
The AWH Xamarin Flow makes it easy to build and maintain a Xamarin.Forms app using techniques similar to other .NET applications. There are other frameworks out there, but this setup achieves much of the same functionality while reducing the number of “black-box” libraries you have to handle. By using some standard packages, we have pieced together a solid and extendible framework of our own.
This setup was created by Tommy Elliott, a Software Developer Team Lead at AWH. A huge thank you to him for putting the setup together, helping me create the sample project, and generally helping me get my head around Xamarin. He also coined the name, which is far better than constantly calling it “our setup”.
Andrew Moscardino is a Software Developer at AWH and has worked on websites, APIs, data structures, and software architecture since 2013. He has experience in full-stack development with an interest in modern front-end technologies. Andrew has worked on projects for a wide range of industries, including healthcare, libraries, maintenance, and nonprofits.