How To Implement Dependency Injection in Avalonia
Dependency injection (DI) allows developers to write cleaner, more modular, and testable code. It accomplishes this by creating discrete services that are passed around/created as needed.
This guide will show you step by step how to use Dependency Injection (DI) with Avalonia UI and the MVVM pattern.
Step 0: Context and Initial Code
Let's assume that you have an app with a MainViewModel
, a BusinessService
and a Repository
. MainViewModel
has a dependency on IBusinessService
and BusinessService
on IRepository
. A simple implementation would look like this:
public partial class MainViewModel
{
private readonly IBusinessService _businessService;
public MainViewModel(IBusinessService businessService)
{
_businessService = businessService;
}
}
public class BusinessService : IBusinessService
{
private readonly IRepository _repository;
public BusinessService(IRepository repository)
{
_repository = repository;
}
}
public class Repository : IRepository
{
}
Traditionally, you would directly instantiate Repository
and pass it into BusinessService
then pass it into MainViewModel
, like this:
var window = new MainWindow
{
DataContext = new MainViewModel(new BusinessService(new Repository()))
}
This works great for simple constructors that are not used very often and don't ever change. But this pattern does not scale well, because:
- The more dependencies your constructor has the more things you will need to instantiate yourself and pass in. Instantiating the dependencies locally in the constructor (such as by doing
new MainViewModel(new MyService())
) results in direct rigid coupling to a specific instance of the dependencies. - Similarly if the
MainViewModel
constructor itself creates its own dependencies, it also becomes directly coupled to the creation of the dependencies which can result in many of the same problems. - Furthermore, if
MainViewModel
is instantiated in many places, every instantiation ofMainViewModel
would also need to be updated should the dependencies ofMainViewModel
ever change (such as the addition of new dependencies, or changing which implementation of a dependency to use).
Dependency injection solves these problem by abstracting away the creation of objects and their dependencies. This allows for well encapsulated services that will be automatically passed into any other service that is registered to use them.
Step 1: Install the NuGet package for DI
There are many dependency injection (DI) container providers available (DryIoC, Autofac, Pure.DI) but this guide will only focus on Microsoft.Extensions.DependencyInjection
which is a lightweight, extensible dependency injection container. It provides an easy-to-use and convention-based way to add DI to .NET applications, including Avalonia-based desktop applications.
Run the following command in a terminal inside your project directory to install the DI package:
dotnet add package Microsoft.Extensions.DependencyInjection
Step 2: Add ServiceCollectionExtensions
The following code creates an extension method for IServiceCollection
. The method will register services to the service collection and make them available for injection.
public static class ServiceCollectionExtensions
{
public static void AddCommonServices(this IServiceCollection collection)
{
collection.AddSingleton<IRepository, Repository>();
collection.AddTransient<BusinessService>();
collection.AddTransient<MainViewModel>();
}
}
Step 3: Modify App.axaml.cs
Next: the App.xaml.cs
class should be modified to use the DI container. This will allow the view model which was registered in the previous step to be resolved via the dependency injection container. The fully realised view model can then be set to the data context of the MainWindow
/MainView
.
public class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
// If you use CommunityToolkit, line below is needed to remove Avalonia data validation.
// Without this line you will get duplicate validations from both Avalonia and CT
BindingPlugins.DataValidators.RemoveAt(0);
// Register all the services needed for the application to run
var collection = new ServiceCollection();
collection.AddCommonServices();
// Creates a ServiceProvider containing services from the provided IServiceCollection
var services = collection.BuildServiceProvider();
var vm = services.GetRequiredService<MainViewModel>();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = vm
};
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
singleViewPlatform.MainView = new MainView
{
DataContext = vm
};
}
base.OnFrameworkInitializationCompleted();
}
}