6

I am creating a WPF application using .NET core 3.1 I have developed ASP.Net applications in the past and I was excited to use this in WPF. I did some searching and realized DI in WPF isn't as straightforward as it is in ASP.Net, meaning you have to register Views and ViewModels.

My structure is like this

MainWindow
|---BalanceIntegrationPage
   |---BalanceIntegrationViewModel

Everything is handled in XAML with the MainWindow.xaml.cs having only generated code, and the BalanceIntegrationPage.xaml.cs has a single line added to it in the constructor

DataContext = new ScaleIntegrationViewModel();  

That could not be handled in the xaml because DI requires parameters in the constructor.

Here is my app.xaml.cs:

protected override async void OnStartup(StartupEventArgs startupEventArgs)
        {
            base.OnStartup(startupEventArgs);
            ServiceCollection services = new ServiceCollection();
            services.AddScoped<MainWindow>();
            services.AddScoped<ScaleInterfacePage>();
            services.AddScoped<ScaleIntegrationViewModel>();
            services.AddScoped<IScale>(provider => new Scale("1234"));

            ServiceProvider serviceProvider = services.BuildServiceProvider();
            MainWindow mainWindow = serviceProvider.GetService<MainWindow>();
            mainWindow.Show();

        }

My ScaleIntegrationViewModel looks like:

public ScaleIntegrationViewModel(IJMDataIntegration jmContext = null, IBalanceIntegrationContext localContext = null, IScale scale = null)
        {
            _jmContext = jmContext ?? new JMDataIntegration();
            _localContext = localContext ?? new BalanceIntegrationContext();
            _scale = scale ?? new Scale("1234");
            //JK read from config
            _commPort = "1234";
        }

I also attempted to use the pattern described here

When I step through the code, my IScale object in the ViewModel constructor is always null.

Any Suggestions??

edit:

Based off a comment, I removed the ViewModel call in the page constructor and instead assigned it on the .xaml This forced me to create a default paramaterless constructor which then breaks DI.

It almost starts to seem like I need to inject the services into the MainWindow ctor and then pass them down to everything I call from there. Makes no sense to me, since at that point, I may as well throw away DI and just new them up when I need them.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Joe K
  • 231
  • 7
  • 16
  • 1
    Your instantiate your viewmodel manually and provide no values for the constructor arguments, thus the default `null` values are used. What exactly is unclear to you? – dymanoid Mar 10 '20 at 16:26
  • @dymanoid how would I set this up to use DI then? I edited the question based on things I tried from your comment. Any suggestions? – Joe K Mar 10 '20 at 17:48
  • You really should use a MVVM framework, most have DI built in. – mxmissile Mar 10 '20 at 19:32
  • 1
    @mxmissile Dependency Injection is a feature of .NET Core 3.1 so this should be able to be done natively. If I wanted to use a framework or something, I would go back to using UnityContainers and doing DI that way, like I do in full framework applications. The goal is to use the native DI for this project. Thanks though! – Joe K Mar 10 '20 at 20:16
  • 1
    *"meaning you have to register Views and ViewModels"* - this depends on the framework you are using, and whether you are relying on dependency mapping by convention rather than defining it explicitly. Relying on the convention method simply means that magic happens in the background. – slugster Mar 10 '20 at 21:11

2 Answers2

6

You are missing the configuration for some dependencies. From the code you've posted, you missed to configure IJMDataIntegration and IBalanceIntegrationContext:

protected override async void OnStartup(StartupEventArgs startupEventArgs)
{
  base.OnStartup(startupEventArgs);
  ServiceCollection services = new ServiceCollection();
  services.AddScoped<MainWindow>();
  services.AddScoped<ScaleInterfacePage>();
  services.AddScoped<IJMDataIntegration, JMDataIntegration>();
  services.AddScoped<IBalanceIntegrationContext, BalanceIntegrationContext>();
  services.AddScoped<IScale>(provider => new Scale("1234"));
  services.AddScoped<ScaleIntegrationViewModel>();

  ServiceProvider serviceProvider = services.BuildServiceProvider();
  MainWindow mainWindow = serviceProvider.GetService<MainWindow>();
  mainWindow.Show();

}

Also, as already mentioned, you have to inject the view model into the MainWindow too. This where the dependency graph starts, the root of the application:

partial class MainWindow : Window
{
  public MainWindow(ScaleIntegrationViewModel viewModel)
  {
    this.DataContext = viewModel;
  }
}

To enable the full power of Dependency Injection (and make mocking easier) you should make use of Dependency Inversion throughout the application. This means you should only depend on interfaces and therefore only have interface types in your constructors:

partial class MainWindow : Window
{
  public MainWindow(IScaleIntegrationViewModel viewModel)
  {
    this.DataContext = viewModel;
  }
}

Controls like pages should be generated via DataTemplate and not instantiated directly in XAML. All you need to do, is to inject the page view models e.g. into another view model. Bind them to a ContentPresenter and define an implicit DataTemplate which targets the type of the page view model. This template contains the actual page. See this example.

Search for View-Model-First pattern if you need more details. Basically, a view can be defined as a data template and associated with a view model type. Data templates can be defined as resources, or they can be defined inline within the control that will display the view model. The content of the control is the view model instance, and the data template is used to visually represent it. This technique is an example of a situation in which the view model is instantiated first, followed by the creation of the view.
This is the preferred way, especially in conjunction with Dependency Injection.

BionicCode
  • 1
  • 4
  • 28
  • 44
  • 1
    This helps make a bit more sense, at least more so than others, thanks! I didn't miss the configuration for the datacontexts, they are set up the exact same way as the IScale interface, I just had them commented out while I was working on the scale part. I think for now I am just going to leave the viewmodels with the optional parameters so I can Mock the Interfaces I need when I am testing. The main reason I was using DI was to make testing a bit easier. This way I don't have to change my entire paradigm for the views. Thanks again! – Joe K Mar 11 '20 at 13:21
1

Dependency injection means that you inject the dependencies, but don't construct them by yourself.

In your example, you construct the viewmodel manually inside of your page. This is not DI. You have to inject an instance of the viewmodel into the page.

You don't show us how you inject the page into the main window. I am not sure that using DI to inject a page into a window is a good decision. This is UI, and you can describe everything without DI (by using the data templating and pure XAML).

Anyway, to inject your viewmodel into the page, just introduce a constructor parameter in your page:

public ScaleInterfacePage(ScaleIntegrationViewModel vm)
{
    InitializeComponent();
    DataContext = vm;
}

That's it, you're all set.

dymanoid
  • 14,771
  • 4
  • 36
  • 64
  • My page isn't injected into the Window. It is declared in the XAML with a tag ``` ``` If I remove the paramaterless page constructor, it stops loading. I guess here is where my disconnect is. In ASP.Net it seemed anything I registered as a service I could inject into a constructor. It doesn't seem to work like that in WPF I think? It almost seems like a complex dependency chain is needed, I call the main window, then inject the page into that, then inject the viewmodel into that, and so on and so forth. – Joe K Mar 10 '20 at 21:11
  • @JoeK, no, that is not how the DI works - it is not necessary to forward the dependencies from the top to the bottom. Since you don't show the whole setup, it's impossible for me to point out where exactly your issues are. – dymanoid Mar 10 '20 at 21:33
  • @dyamanoid what do you mean I don't know the whole set up? Was there some question you had I didn't address in the reply? – Joe K Mar 11 '20 at 13:18