Home > Back-end >  WPF MVVM: Dependency Injection Without Constructor Overloading Many Parameters
WPF MVVM: Dependency Injection Without Constructor Overloading Many Parameters

Time:12-29

I am building a WPF Application, which has a single Window, a tabbed interface with multiple view models (2 view models, the first tab is ViewModel1 and the rest will always be ViewModel2) these are loaded by user controls using data triggers.

Finally there are also a other windows such as Dialogs/Settings etc.

Project Structure

My project is split into several layers as described below:

  • Core Layer (Base Code Layer/Interfaces very little 3rd party libraries)
  • Repo Layer (Repository Layer with EF, basic CRUD functions without business logic)
  • Service Layer (Individual service layers which make use of the repos, also contain business logic)
  • WPF Layer (Main UI Layer with View Models)

One of the issues I have is using DI within an WPF/MVVM setting, I have seen many resources online and a lot of this is ASP.NET Core DI which is not relevant to what I am focusing on. The issue I find is that in WPF there seems to be a single applications Start-up point (MainWindow) which when using DI everything is injected into the constructor, this is then passed down further to other dialogs/models this seems very cumbersome (or I am doing it wrong). For example I currently have something similar to this in the App code behind using Microsoft DI.

(Dialog is for creating dialogs/opening windows etc, Messaging is for Message Boxes)

services.AddSingleton<IRepo1, Repo1>();
services.AddSingleton<IRepo1Service, Repo1Service>();
services.AddSingleton<IDialog, Dialog>();
services.AddSingleton<IMessaging, Messaging>();

To use these within a viewmodel, they needed to be injected within the MainWindow constructor like so

MainWindow(repo1service, dialog, messaging)

These are then passed down to the viewmodels

MainWindowViewModel(repo1service,dialog,messaging)

This process seems like I am awfully doing a lot of work within the constructors and traversing down from a single application point, also some of these viewmodels may not even use dialogs or messaging, but only the DB context

MainWindowViewModel has the tabbed control, this then calls the corresponding viewmodel when adding another Tab, so for example I will need to do :

Tabs.Add(new ViewModel1(repo1service,dialog,messaging))

When a user clicks new tab.

I have realized I can use DI and add the viewmodel like as a Singleton:

 services.AddSingleton<ViewModel1>();

but, if this viewmodel is called only after a button click, I still have the issue of needing to pass parameters down to the constructor.

How can I avoid passing many parameters to viewmodels, how can DI resolve this for me? Could I pass the IServiceCollection to the models and retrieve as needed or is this a bad approach?

I have read up on these resources, but I am still unsure about how I can resolve my issue.

  1. https://docs.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-usage

  2. How to handle dependency injection in a WPF/MVVM application

  3. Dependency injection & constructor madness revised

  4. How to avoid Dependency Injection constructor madness?

CodePudding user response:

A simple example how to wire up your WPF application when using Dependency Injection. It also shows how to register a factory (in this case a delegate) to enable dynamic instance creation. The idea is to let the container create any instances for you. DI allows you to avoid the new keyword and therefore elimintaes your responsibility to care for constructors and their dependencies.

The general recommended approach in WPF to display views is the view-model-first principle: basically, add data models to the view (for example by binding the view model to a ContentControl or ContentPresenter) and use a DataTemplate to let the framework load the related view.

App.xaml.cs

private async void OnStartup(object sender, StartupEventArgs e)
{
  var services = new ServiceCollection();
 
  // Look how you can chain the service registrations:
  services.AddSingleton<IMainViewModel, MainViewModel>()
    .AddSingleton<IRepository, Repository>()
    .AddSingleton<IViewModel1, ViewModel1>()
    
    // Factory is used to create instances dynamically.
    // Alternatively, instead of a delegate you can register a factory class.
    .AddSingleton<Func<IViewModel1>>(serviceProvider => () => serviceProvider.GetService<IViewModel1>())

    .AddSingleton<IViewModel2, ViewModel2>()
    .AddSingleton<IRepository, Repository>()
    .AddSingleton<MainView>();
  
  ServiceProvider container = services.BuildServiceProvider();

  // Let the container compose the dependency graph 
  // and export the MainView to start the GUI
  var mainWindow = container.GetService<MainView>();

  // Launch the main view
  mainWindow.Show();
}

MainViewModel.cs

class MainViewModel : IMainViewModel
{
  public IViewModel2 ViewModel2 { get; }
  private IViewModel1 ViewModel1 { get; }
  private Func<IViewModel1> TabItemFactory { get; }

  public MainViewModel(
    IViewMode1 viewModel1, 
    Func<IViewModel1> viewModel1Factory, 
    IViewModel2 viewModel2)
  {
    this.ViewModel = viewModel2; // public read-only for data binding
    this.ViewModel1 = viewModel1; // private read-only for internal use only
    this.TabItemFactory = viewModel1Factory; // private read-only for internal use only
  }

  private void ExecuteAddTabCommand(object commandParameter)
  {
    // Uses a shared instance for every tab
    this.Tabs.Add(this.ViewModel1);

    // Create a fresh instance for every tab using a factory
    IViewModel1 tabViewModel = this.TabItemFactory.Invoke();
    this.Tabs.Add(tabViewModel);
  }
}

MainWindow.xaml.cs

partial class MainWindow : Window
{
  public MainWindow(IMainViewModel mainViewModel)
  {
    InitializeComponent();

    this.DataContext = mainViewModel;
  }
}

MainWindow.xaml

<Window>

 <!-- Bind to a nested view model of the MainViewModel -->
 <MyUserControl DataContext="{Binding ViewModel2}" />
</Window>

ViewModel1.cs

class ViewModel1 : IViewModel1
{
  private IRepository Repository { get; }

  public ViewModel1(IRepository repository)
  {
    this.Repository = repository; // private read-only for internal use only
  }
}
  • Related