6
votes

I'm familiar with WPF and MVVM pattern. Now, I'm trying to practice Dependency Injection Pattern in my new WPF app with Autofac. And I want to figure out how to inject dependency into MVVM View Model class.

As code attached below, I have a root view model class MainViewModel, it can create other view model (eg. MonitorPageViewModel) instance when needed. In MonitorPageViewModel, it also need to create other sub view model (eg. MonitorDashboardViewModel) instance when required. And all these view models may have several dependencies (ILogger, IRepository<RawMessage>, IParsingService, and so on).

In my previous WPF projects without using any IoC container, I always new a sub view model in parent view model when needed and use a ServiceLocator to provide the services needed. Now, I want to figure out a more dependency injection way to do this.

I get several ways (demonstrated in code below), but I'm not satisfied with any one of it.

  1. use the IoC container take the response to create MainViewModel; and inject the IoC Container instance to MainViewModel; then, use the IoC container instance to resolve every object the child view models constructor need. If the child view model class need to resolve other class, then inject the IoC into it. [It sounds another ServiceLocator].
  2. inject all the services needed by MainViewModel and its descendant view models and pass services along the view model chain. And new the view model instance at where it's needed. [Need to inject a lot of services, and pass them down]

I don't want to use a MVVM framework such as Caliburn.Micro in this project. Is there a simple yet elegant solution?

public interface ILogger
{
    // ...
}

public interface IRepository<T>
{
    // ...
}

public interface IStatisticsService
{
    // ...
}

public class RawMessage
{
    // ...
}

public class Device
{
    // ...
}

public class Parser
{
    // ...
}


public interface IParsingService
{
    void Parse(Parser parser);
}

public class DockPaneViewModel : ViewModelBase
{
    // ...
}

public class HomePageViewModel : DockPaneViewModel
{
    public HomePageViewModel(ILogger logger)
    {
        // ...
    }
}

public class MonitorDashboardViewModel : DockPaneViewModel
{
    public MonitorDashboardViewModel(IStatisticsService statisticsService)
    {
        // ...
    }
}

public class MonitorPageViewModel : DockPaneViewModel
{
    public MonitorPageViewModel(ILogger logger, IRepository<RawMessage> repository,
        IRepository<Parser> parserRepository, IParsingService parsingService)
    {
        // ...
    }

    public void CreateDashboard()
    {
        IStatisticsService statisticsService = ??; // how to resolve the service?
        var dashBoardVm = new MonitorDashboardViewModel(statisticsService); // how to create this? 
    }
}

public class ResourceManagementViewModel : DockPaneViewModel
{
    public ResourceManagementViewModel(ILogger logger, IRepository<Device> deviceRepository)
    {
        // ...
    }
}

Here is the MainViewModel with alternative constructors

public class MainViewModel : ViewModelBase
{
    public ObservableCollection<DockPaneViewModel> DockPanes
    {
        get;
        set;
    } = new ObservableCollection<DockPaneViewModel>();

    #region approach 1
    // use the IOC container take the response to create MainViewModel;
    // and inject the Ioc Container instance to MainViewModel;
    // then, use the IOC container instance to resovle every thing the child view models need
    private readonly ISomeIocContainer _ioc;
    public MainViewModel(ISomeIocContainer ioc)
    {
        _ioc = ioc;
    }

    public void ResetPanes_1()
    {
        DockPanes.Clear();
        DockPanes.Add(new HomePageViewModel(_ioc.Resolve<ILogger>())); // how to new child view model and how to provide the constructor parameters?
        DockPanes.Add(new MonitorPageViewModel(_ioc.Resolve<ILogger>(),
            _ioc.Resolve< IRepository<RawMessage>>(), 
            _ioc.Resolve<IRepository<Parser>>(),
            _ioc.Resolve<IParsingService>())); // also need to inject ISomeIocContainer to MonitorDashboardViewModel for resolve IStatisticsService
        DockPanes.Add(new ResourceManagementViewModel(_ioc.Resolve<ILogger>(), 
            _ioc.Resolve<IRepository<Device>>()));
        // add other panes
    }
    #endregion

    #region approach 2
    // pasing all dependencies of MainViewModel and all descendant View Models in to MainViewModel,
    // and pass dependencies along the ViewModel chain.
    private readonly ILogger _logger;
    private readonly IRepository<RawMessage> _repository;
    private readonly IRepository<Parser> _parserRepository;
    private readonly IRepository<Device> _deviceRepository;
    private readonly IParsingService _parsingService;
    private readonly IStatisticsService _statisticsService;
    public MainViewModel(ILogger logger, IRepository<RawMessage> repository,
        IRepository<Parser> parserRepository, IRepository<Device> deviceRepository,
        IParsingService parsingService, IStatisticsService statisticsService)
    {
        _logger = logger;
        _repository = repository;
        _parserRepository = parserRepository;
         _deviceRepository = deviceRepository;
        _parsingService = parsingService;
        _statisticsService = statisticsService;
    }

    public void ResetPanes_2()
    {
        DockPanes.Clear();
        DockPanes.Add(new HomePageViewModel(_logger)); // how to new child view model and how to provide the constructor parameters?
        DockPanes.Add(new MonitorPageViewModel(_logger, _repository, _parserRepository, _parsingService)); // also need pass statisticsService down 
        DockPanes.Add(new ResourceManagementViewModel(_logger, _deviceRepository));
        // add other panes
    }
    #endregion
}
1
a) way too much code...Henk Holterman
b) DI and MVVM works better with a View-first approach. You seem to be doing VM first, I would pick the Resolve() route here.Henk Holterman
You may retrieve the container from a static property rather than injecting every view model with it. Otherwise I would prefer the second approach. Injecting a lot of services means that the view models do have a lot of depdendencies.mm8

1 Answers

8
votes

Some times going back to basics and keeping things simple (KISS) tends to work.

What comes to mind for this scenario is The Explicit Dependency Principle and Pure Dependency Injection.

The MainViewModel is doing way too much as evident by either injecting the container (big no no) or have way to many dependencies (code smell). Try to narrow down what it is that class is suppose to be doing (SRP)

So let's say the main view model needs a collection of panes. Then why not give it what it needs.

public class MainViewModel : ViewModelBase {
    public ObservableCollection<DockPaneViewModel> DockPanes { get; set; }

    //Give the view model only what it needs
    public MainViewModel(IEnumerable<DockPaneViewModel> panes) {
        DockPanes = new ObservableCollection<DockPaneViewModel>(panes);
    }

    public void ResetPanes() {
        foreach (var pane in DockPanes) {
            pane.Reset();
        }
        //notify view
    }
}

Note the slight change to the base panel

public abstract class DockPaneViewModel : ViewModelBase {
    // ...

    public virtual void Reset() {
        //...
    }
}

The main view model should not concern itself with how the dependencies are created. It only cares that it gets what it explicitly asks for.

The same applies to the different pane implementations.

If a view model needs to be able to create multiple children then delegate that responsibility out to a factory.

public class MonitorPageViewModel : DockPaneViewModel {
    public MonitorPageViewModel(ILogger logger, IRepository<RawMessage> repository,
        IRepository<Parser> parserRepository, IParsingService parsingService, 
        IPaneFactory factory) {
        // ...
    }

    public void CreateDashboard() {
        var dashBoardVm = factory.Create<MonitorDashboardViewModel>();
        
        //...
    }
}

Again the subject should have as few responsibilities as possible.

View First or ViewModel First are considered implementation concerns and really does not matter if following a convention over configuration model.

If the design is well done, it really does not matter whether you use a framework or pure code.

Those frameworks though, do come in handy when it comes to putting everything together. The most simple and elegant solution is to have something create the object graph, but without that something, you are left to build it up yourself in the composition root.