Dependency Injection in a WPF MVVM Application

WPF Team Blog
07 February 2022

When a solution grows in size and scope, it becomes much harder to maintain overall app flexibility. Dependencies between objects grows and altering one class may require updating others. Dependency Injection (DI) can help address this challenge.

As you know, dependency injection is a form of “inversion of the control” (IoC) programming principle. This means that classes don’t create the objects they rely on. DI frameworks have containers responsible for revealing and resolving dependencies.

Which Issues can be Resolved by Dependency Injection?

Let’s say you have a view model that uses a data service to obtain data:

    public class UserViewModel
    {
        MyDataService dataService;
        public UserViewModel() {
            this.dataService = new MyDataService();
        }
    }

The view model depends on a service - which means that MyDataService is UserViewModel’s dependency. It’s very easy to create the service directly in the view model class, but there are several drawbacks to this approach:

  • Classes are closely connected. Each time you use UserViewModel, it implicitly creates an instance of MyDataService.
  • If you modify the manner in which MyDataService is initialized in the future, you will have to modify all view models wherein MyDataService is initialized.
  • You cannot create a unit test specifically for the UserViewModel class, because it depends on MyDataService. If a test fails, you may not be able to determine whether the error is in UserViewModel or MyDataService.

You can avoid these issues if you pass MyDataService to the UserViewModel’s constructor:

    public class UserViewModel
    {
        MyDataService dataService;
        public UserViewModel(MyDataService dataService) {
            this.dataService = dataService;
        }
    }

Unfortunately, this technique also has flaws:

  • Different view models are created in each class and all of them must know how to create MyDataService.
  • It can be difficult to share the same instance of MyDataService between multiple view models without creating a static property.

The main idea of dependency injection is to resolve all dependencies centrally. This means that you have a separate block in your program to initialize new class instances and pass parameters to them. Although you can implement your own logic for this, it’s much more convenient to use a DI framework to help avoid/eliminate boilerplate code.

The dependency injection pattern has the following advantages:

  • Classes are loosely coupled with one another. As a result, you can easily modify dependencies. For example, replace MyDataService with MyDataServiceEx.
  • It’s easy to create unit tests, because you can pass mock parameters to tested classes.
  • Your project is well structured, because you always know where all dependencies are managed.

Apply Dependency Injection to a WPF Application

The .NET community has many great frameworks to help you implement the Dependency Injection pattern in your application. All these frameworks have two main features:

  • You can register classes in containers.
  • You can create objects with initialized dependencies

Containers are central objects in DI frameworks and automatically detect and resolve class dependencies. Certain frameworks can inject parameters into class properties, but the most common way is to inject parameters into a constructor.

Let’s modify the MainViewModel constructor so that it accepts an interface instead of a class:

    public class MainViewModel
    {
        IDataService dataService;
        public MainViewModel(IDataService dataService) {
            this.dataService = dataService;
        }
    }

This will allow you to use different IDataService implementations in the future.

Once we make this change, we need to create a DI container to register MyDataService and instantiate MainViewModel:

protected override void OnStartup(StartupEventArgs e) {
    base.OnStartup(e);
    var builder = new ContainerBuilder();
    //allow the Autofac container resolve unknown types
    builder.RegisterSource(new AnyConcreteTypeNotAlreadyRegisteredSource());
    //register the MyDataService class as the IDataService interface in the DI container
    builder.RegisterType<MyDataService>().As<IDataService>().SingleInstance();
    IContainer container = builder.Build();
    //get a MainViewModel instance
    MainViewModel mainViewModel = container.Resolve<MainViewModel>();
}

In this example, we used the Autofac framework, but you can use any other DI framework, such as Unity or Ninject. The DI container creates MainViewModel and automatically injects MyDataService into the MainViewModel constructor. This allows you to avoid MyDataService initialization each time a class with the IDataService parameter type is created.

We then need to connect the MainViewModel to its view: MainView. The most obvious strategy is to set DataContext in the view constructor:

public MainView() {
    InitializeComponent();
    this.DataContext = container.Resolve<MainViewModel>();
}

However, to access the DI container, you will have to either make it static or pass it to each view constructor. A more elegant solution is to create a markup extension that returns a view model instance based on its type:

public class DISource : MarkupExtension
{
    public static Func<Type, object> Resolver { get; set; }
    public Type Type { get; set; }
    public override object ProvideValue(IServiceProvider serviceProvider) => Resolver?.Invoke(Type);
}
<UserControl DataContext="{local:DISource Type=local:MainViewModel}">

Initially, the markup extension is not bound to any DI container. To allow the extension to use your container, specify the view model resolver in the following manner:

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e) {
        base.OnStartup(e);
        //...

        IContainer container = builder.Build();
        DISource.Resolver = (type) => {
            return container.Resolve(type);
        };
    }
}

This technique allows DISource to work with any possible container.

You can find a complete sample project here: How to register POCO type in dependency injection container.

Dependency Injection and DevExpress Services

In the examples above, we used an abstract data service that doesn’t communicate with visual controls. As you know, many DevExpress WPF services work with view controls. Therefore, a service must know which control to work with. For example, if you wish to inject NavigationFrameService into your view model, you also need to attach this service to a corresponding NavigationFrame control.

Create a public property that contains the service in your view model and bind the service to NavigationFrame:

public class MainViewModel {
    public INavigationService NavigationService { get; }

    public MainViewModel(INavigationService navigationService) =>
        NavigationService = navigationService;
}
<dxwui:NavigationFrame>
    <dxmvvm:Interaction.Behaviors>
        <common:AttachServiceBehavior Service="{Binding NavigationService}"/>
    </dxmvvm:Interaction.Behaviors>
</dxwui:NavigationFrame>

AttachServiceBehavior is a simple attached behavior that calls NavigationFrameService.Attach when the Service property is changed. Although AttachServiceBehavior is not included in our library, you can obtain its code here:
How to use our Services with Dependency Injection/AttachServiceBehavior. Even though MainViewModel uses NavigationFrameService, it doesn’t have to implement the ISupportServices interface. Moreover, all child views involved in navigation can use the service without attaching to NavigationFrame - because it’s already configured at the main view level.

Not all DevExpress services require a visual component. Certain services, such as DXMessageBoxService or DXOpenFileDialogService, don’t need to be explicitly attached, so you can inject them as any other non-DevExpress service.

In certain instances, it might not be wise to inject a service using a DI container if the service needs to be configured for a specific view. For example, this is the case if you have a service that binds to your view model’s commands. It’s much easier to define the service directly in the view and configure all bindings there. To access this service from the view model, use the techniques described here: Services -> Getting Started. A complete example for this topic is available here: How to use DevExpress Services with Dependency Injection.

Should you have any questions about this post, feel free to comment below or create a new support ticket via the DevExpress Support Center.

Free DevExpress Products - Get Your Copy Today

The following free DevExpress product offers remain available. Should you have any questions about the free offers below, please submit a ticket via the DevExpress Support Center at your convenience. We'll be happy to follow-up.
Tags
No Comments

Please login or register to post comments.