1

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://learn.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?

user13499502
  • 91
  • 1
  • 5
  • Passing around the IServiceCollection is a bad idea, indeed. All you have to do, is to register the dependencies (constructor parameters) of the ViewModel1, the same way you have registered the ViewModel1. The DI container will then assemble the instance for you. Then add a ViewModel1 parameter to the MainWindowViewModel in order to receive the shared ViewModel1 instance, that you can pass to your Tabs collection (after storing the injected instance in a private property). – BionicCode Dec 28 '21 at 16:44
  • Also note that in an IoC context, the entry point is never the MainWindow (as you have stated). You must start the application manually from App.xaml.cs. This way you can wire up the dependency graph without using any Service Locator (like the ViewModelLocator class you find in many not so good examples). Inject your MainWindowViewModel into the MainWIndow. Retrieve the composed MainWindow instance from the container and show it. – BionicCode Dec 28 '21 at 17:06

1 Answers1

1

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>)

    // Register a factory delegate taking an argument
    .AddSingleton<Func<User, IViewModel2>>(serviceProvider => user =>
      { 
        var repository = serviceProvider.GetService<IRepository>();
        var dependency1 = serviceProvider.GetService<IDependency1>();
        var dependency2 = serviceProvider.GetService<IDependency2>();
        return new ViewModel2(
          repository, dependency1, dependency2, user);

        // Alternativelyly set property instead of using constructor
        var viewModel2 = serviceProvider.GetService<IViewModel2>();
        viewModel2.User = user;
        return viewModel2;
      })

    // Alternatively register an abstract factory
    .AddSingleton<IViewModel2Factory, ViewModel2Factory>()

    .AddSingleton<IDependency1, Dependency1>()
    .AddSingleton<IDependency2, Dependency2>()
    .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();
}

ViewModel2Factory.cs

class ViewModel2Factory : IViewModel2Factory
{
  private IRepository Repository { get; }
  private Func<IRepository> RepositoryFactory { get; }
  private Func<IDependency1> Dependency1Factory { get; }
  private Func<IDependency2> Dependency2Factory { get; }

  public (
    IRepository repository, 
    IDependency1 dependency1, 
    IDependency2 dependency2)
  {
    // TDODO::Initialize properties
  }

  public IViewModel2 Create(User user)
  {
    var repository = this.RepositoryFactory.Invoke();
    var dependency1 = this.Dependency1Factory.Invoke(); 
    var dependency2 = this.Dependency2Factory.Invoke(); 
    return new ViewModel2(repository, dependency1, dependency2, user);
  }
}

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; }
  private IViewModel2Factory ViewModel2Factory { get; }

  public ViewModel1(IRepository repository, IViewModel2Factory viewModel2Factory)
  {
    this.Repository = repository; // private read-only for internal use only
    this.ViewModel2Factory = viewModel2Factory;
  }

  public void CreateViewModel2()
  {
    IViewModel2 newTab = this.ViewModel2Factory.Create(this.User);
  }
}

ViewModel2.cs

class ViewModel2 : IViewModel2
{
  public ViewModel2(
    IRepository repository, 
    IDependency1 dependency1, 
    IDependency2 dependency2,
    User user)
  {
    ...
  }
}
BionicCode
  • 1
  • 4
  • 28
  • 44
  • Just to clarify - The MainWindowViewModel, houses the tab control and adds the first ViewModel1 to the tab collection, this tab cannot be removed, let's say this is primary tab. Additional tabs are created from viewmodel1 to viewmodel2 as viewmodel2 will require the selected user from viewmodel1. The constructor for viewmodel2 will still require repo, dialog and an additional user parameter, how would I call this without using new keyword? Or can I directly initiate it from viewmodel1? – user13499502 Dec 29 '21 at 11:04
  • I have registered another Func factory that accepts a User as parameter to give another example. If instantiation can't avoid the constructor, you have to implement the Abstract Factory (see IViewModel2Factory in my example). Generally the pattern is always the same: you need dynamic instance creation then register a factory or Func delegate. – BionicCode Dec 29 '21 at 12:22
  • Your flow sounds odd. If MainWindowViewModel hosts and creates the tab items, then ViewModel1 should not create tab items too. This should be in one place. ViewModel1 should raise a e.g., UserReady event that MainWindowViewModel listens to. Then MainWindowViewModel can create the new ViewModel2 using the new User and add it to its Tabs collection. – BionicCode Dec 29 '21 at 12:22