Enhancing Testability in MAUI Applications with a View Factory
In the world of software development, ensuring that our code is testable is paramount to maintaining high-quality standards. When working with .NET MAUI (Multi-platform App UI) applications, I encountered a significant challenge: making our view-models more testable. The solution I found was implementing a view factory, a technique often considered an anti-pattern but proved to be a vital piece in our development puzzle.
The Challenge: Testability in MVVM
In a typical MVVM (Model-View-ViewModel) architecture, view-models are responsible for handling the logic and data-binding for views. However, injecting views directly into view-models can make unit testing cumbersome and less effective. This is because the tight coupling between views and view-models introduces dependencies that are hard to mock or isolate during testing.
The Solution: Implementing a View Factory
To address this, I implemented a view factory in our MAUI application. While view factories are often seen as an anti-pattern due to their potential to obscure dependencies and making code harder to understand, in our case, it was a game-changer. Here’s why:
- Decoupling Views and View-Models: By using a view factory, I could obtain pages through the service provider rather than injecting them directly into the view-models. This decoupling allowed us to isolate view-models from their views, making them easier to unit test.
- Leveraging the Service Provider: MAUI’s built-in dependency injection system enabled us to use the service provider to create instances of our views. This approach ensured that our view-models remained agnostic of the specific views they were associated with, further enhancing testability.
- Improved Unit Testing: With the view factory in place, I could mock the service provider and the views it created. This allowed us to write comprehensive unit tests for our view-models without worrying about the complexities of the UI layer. As a result, I could focus on testing the business logic and data-binding aspects of our view-models, leading to higher quality assurance.
How this looks in code
Creating the View Factory
The first thing we need to do is to create a View Factory and interface it like in the examples below:
namespace MyApp.Views;
public class ViewFactory : IViewFactory
{
private readonly IServiceProvider _serviceProvider;
public ViewFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
TView IViewFactory.CreateView<TView>() where TView : default => _serviceProvider.GetRequiredService<TView>();
}
namespace MyApp.Views;
public interface IViewFactory
{
TView CreateView<TView>();
}
The above code injects the IServiceProvider into the factory, and accepts a generic view type that the service provider will use to create the view
Plumbing this into the App
Once we have created the ViewFactory class and interface we will then want to register it as a service in our Maui Program file like in the example below
// Reference any supporting imports
namespace MyApp;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
// any other supporting added services
.RegisterServices();
return builder.Build();
}
public static MauiAppBuilder RegisterServices(this MauiAppBuilder mauiAppBuilder)
{
mauiAppBuilder.Services.AddSingleton<IViewFactory, ViewFactory>();
return mauiAppBuilder;
}
}
Using the factory in a view-model
Once the service is registered you can now use it in your view-models to get pages without injecting the view directly.
using MyApp.Views;
namespace MyApp.ViewModels;
public partial class PageViewModel : ViewModelBase
{
private readonly IViewFactory _viewFactory;
private readonly IAppNavigationService _appNavigationService;
public PageViewModel(
IViewFactory viewFactory,
IAppNavigationService appNavigationService)
{
_viewFactory = viewFactory;
_appNavigationService = appNavigationService;
}
[RelayCommand]
private async Task LoadNextPage()
{
await _appNavigationService.PushNewPage(_viewFactory.CreateView<SecondPage>()).PreserveThreadContext();
}
}
In the above example you can see we are injecting our IViewFactory into the view model and we are using it to create a view for our second page and pushing that to our navigation service instead of injecting the Second page into the view-model. This IViewFactory as it is injectable becomes mockable in a unit testing scenario.
Why I Recommend This Approach
For anyone developing an MVVM-based MAUI application, I highly recommend considering the implementation of a view factory. Despite its reputation as an anti-pattern, in the context of MAUI, it can be a powerful tool to enhance the testability of your view-models. By decoupling views from view-models and leveraging the service provider, you can achieve a more modular and testable codebase.
In conclusion, while the view factory might not be the go-to solution for every scenario, it proved to be invaluable in our MAUI application. It allowed us to write better unit tests, improve our code quality, and ultimately deliver a more robust application. If you’re facing similar challenges in your MAUI projects, give the view factory a try – it might just be the solution you need.