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.
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].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
}
First, create a module for your ViewModelFactory. This is important for you to provide ViewModelFactory for dagger. @Module abstract class ViewModelModule { @Binds abstract fun provideViewModelFactory(factory: Factory): ViewModelProvider. Factory // Binds your viewmodel in here. }
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.
To use @HiltViewModel , you'll need to add these 2 libraries on top of the Dagger Hilt Libraries added as shared in the article above. In your ViewModel, just add @HiltViewModel before the class and the usual @Inject to the constructor. Here we auto have the savedStateHandler injected too.
The injector class injects dependencies broadly in three ways: through a constructor, through a property, or through a method. Constructor Injection: In the constructor injection, the injector supplies the service (dependency) through the client class constructor.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With