How to handle dependency injection in a WPF/MVVM a

2020-01-23 10:17发布

I am starting a new desktop application and I want to build it using MVVM and WPF.

I am also intending to use TDD.

The problem is that I don´t know how I should use an IoC container to inject my dependencies on my production code.

Suppose I have the folowing class and interface:

public interface IStorage
{
    bool SaveFile(string content);
}

public class Storage : IStorage
{
    public bool SaveFile(string content){
        // Saves the file using StreamWriter
    }
}

And then I have another class that has IStorage as a dependency, suppose also that this class is a ViewModel or a business class...

public class SomeViewModel
{
    private IStorage _storage;

    public SomeViewModel(IStorage storage){
        _storage = storage;
    }
}

With this I can easily write unit tests to ensure that they are working properly, using mocks and etc.

The problem is when it comes to use it in the real application. I know that I must have an IoC container that links a default implementation for the IStorage interface, but how may I to do it?

For example, how would it be if I had the following xaml:

<Window 
    ... xmlns definitions ...
>
   <Window.DataContext>
        <local:SomeViewModel />
   </Window.DataContext>
</Window>

How can I correctly 'tell' WPF to inject dependencies in that case?

Also, suppose I need an instance of SomeViewModel from my cs code, how should I do it?

I feel I´m completely lost in this, I would appreciate any example or guidance of how is the best way to handle it.

I am familiar with StructureMap, but I´m not an expert. Also, if there is a better/easier/out-of-the-box framework, please let me know.

Thanks in advance.

9条回答
干净又极端
2楼-- · 2020-01-23 10:54

Remove the startup uri from your app.xaml.

App.xaml.cs

public partial class App
{
    protected override void OnStartup(StartupEventArgs e)
    {
        IoC.Configure(true);

        StartupUri = new Uri("Views/MainWindowView.xaml", UriKind.Relative);

        base.OnStartup(e);
    }
}

Now you can use your IoC class to construct the instances.

MainWindowView.xaml.cs

public partial class MainWindowView
{
    public MainWindowView()
    {
        var mainWindowViewModel = IoC.GetInstance<IMainWindowViewModel>();

        //Do other configuration            

        DataContext = mainWindowViewModel;

        InitializeComponent();
    }

}
查看更多
smile是对你的礼貌
3楼-- · 2020-01-23 10:55

Install MVVM Light.

Part of the installation is to create a view model locator. This is a class which exposes your viewmodels as properties. The getter of these properties can then be returned instances from your IOC engine. Fortunately, MVVM light also includes the SimpleIOC framework, but you can wire in others if you like.

With simple IOC you register an implementation against a type...

SimpleIOC.Default.Register<MyViewModel>(()=> new MyViewModel(new ServiceProvider()), true);

In this example, your view model is created and passed a service provider object as per its constructor.

You then create a property which returns an instance from IOC.

public MyViewModel
{
    get { return SimpleIOC.Default.GetInstance<MyViewModel>; }
}

The clever part is that the view model locator is then created in app.xaml or equivalent as a data source.

<local:ViewModelLocator x:key="Vml" />

You can now bind to its 'MyViewModel' property to get your viewmodel with an injected service.

Hope that helps. Apologies for any code inaccuracies, coded from memory on an iPad.

查看更多
劳资没心,怎么记你
4楼-- · 2020-01-23 10:58

What I'm posting here is an improvement to sondergard's Answer, because what I'm going to tell doesn't fit into a Comment :)

In Fact I am introducing a neat solution, which avoids the need of a ServiceLocator and a wrapper for the StandardKernel-Instance, which in sondergard's Solution is called IocContainer. Why? As mentioned, those are anti-patterns.

Making the StandardKernel available everywhere

The Key to Ninject's magic is the StandardKernel-Instance which is needed to use the .Get<T>()-Method.

Alternatively to sondergard's IocContainer you can create the StandardKernel inside the App-Class.

Just remove StartUpUri from your App.xaml

<Application x:Class="Namespace.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
             ... 
</Application>

This is the App's CodeBehind inside App.xaml.cs

public partial class App
{
    private IKernel _iocKernel;

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        _iocKernel = new StandardKernel();
        _iocKernel.Load(new YourModule());

        Current.MainWindow = _iocKernel.Get<MainWindow>();
        Current.MainWindow.Show();
    }
}

From now on, Ninject is alive and ready to fight :)

Injecting your DataContext

As Ninject is alive, you can perform all kinds of injections, e.g Property Setter Injection or the most common one Constructor Injection.

This is how you inject your ViewModel into your Window's DataContext

public partial class MainWindow : Window
{
    public MainWindow(MainWindowViewModel vm)
    {
        DataContext = vm;
        InitializeComponent();
    }
}

Of course you can also Inject an IViewModel if you do the right bindings, but that is not a part of this answer.

Accessing the Kernel directly

If you need to call Methods on the Kernel directly (e.g. .Get<T>()-Method), you can let the Kernel inject itself.

    private void DoStuffWithKernel(IKernel kernel)
    {
        kernel.Get<Something>();
        kernel.Whatever();
    }

If you would need a local instance of the Kernel you could inject it as Property.

    [Inject]
    public IKernel Kernel { private get; set; }

Allthough this can be pretty useful, I would not recommend you to do so. Just note that objects injected this way, will not be available inside the Constructor, because it's injected later.

According to this link you should use the factory-Extension instead of injecting the IKernel (DI Container).

The recommended approach to employing a DI container in a software system is that the Composition Root of the application be the single place where the container is touched directly.

How the Ninject.Extensions.Factory is to be used can also be red here.

查看更多
唯我独甜
5楼-- · 2020-01-23 11:01

In your question you set the value of the DataContext property of the view in XAML. This requires that your view-model has a default constructor. However, as you have noted, this does not work well with dependency injection where you want to inject dependencies in the constructor.

So you cannot set the DataContext property in XAML. Instead you have other alternatives.

If you application is based on a simple hierarchical view-model you can construct the entire view-model hierarchy when the application starts (you will have to remove the StartupUri property from the App.xaml file):

public partial class App {

  protected override void OnStartup(StartupEventArgs e) {
    base.OnStartup(e);
    var container = CreateContainer();
    var viewModel = container.Resolve<RootViewModel>();
    var window = new MainWindow { DataContext = viewModel };
    window.Show();
  }

}

This is based around an object graph of view-models rooted at the RootViewModel but you can inject some view-model factories into parent view-models allowing them to create new child view-models so the object graph does not have to be fixed. This also hopefully answers your question suppose I need an instance of SomeViewModel from my cs code, how should I do it?

class ParentViewModel {

  public ParentViewModel(ChildViewModelFactory childViewModelFactory) {
    _childViewModelFactory = childViewModelFactory;
  }

  public void AddChild() {
    Children.Add(_childViewModelFactory.Create());
  }

  ObservableCollection<ChildViewModel> Children { get; private set; }

 }

class ChildViewModelFactory {

  public ChildViewModelFactory(/* ChildViewModel dependencies */) {
    // Store dependencies.
  }

  public ChildViewModel Create() {
    return new ChildViewModel(/* Use stored dependencies */);
  }

}

If your application is more dynamic in nature and perhaps is based around navigation you will have to hook into the code that performs the navigation. Each time you navigate to a new view you need to create a view-model (from the DI container), the view itself and set the DataContext of the view to the view-model. You can do this view first where you pick a view-model based on a view or you can do it view-model first where the view-model determines which view to use. A MVVM framework provides this key functionality with some way for you to hook your DI container into the creation of view-models but you can also implement it yourself. I am a bit vague here because depending on your needs this functionality may become quite complex. This is one of the core functions you get from a MVVM framework but rolling your own in a simple application will give you a good understanding what MVVM frameworks provide under the hood.

By not being able to declare the DataContext in XAML you lose some design-time support. If your view-model contains some data it will appear during design-time which can be very useful. Fortunately, you can use design-time attributes also in WPF. One way to do this is to add the following attributes to the <Window> element or <UserControl> in XAML:

xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=local:MyViewModel, IsDesignTimeCreatable=True}"

The view-model type should have two constructors, the default for design-time data and another for dependency injection:

class MyViewModel : INotifyPropertyChanged {

  public MyViewModel() {
    // Create some design-time data.
  }

  public MyViewModel(/* Dependencies */) {
    // Store dependencies.
  }

}

By doing this you can use dependency injection and retain good design-time support.

查看更多
何必那么认真
6楼-- · 2020-01-23 11:03

Canonic DryIoc case

Answering an old post, but doing this with DryIoc and doing what I think is a good use of DI and interfaces (minimal use of concrete classes).

  1. The starting point of a WPF app is App.xaml, and there we tell what is the inital view to use; we do that with code behind instead of the default xaml:
  2. remove StartupUri="MainWindow.xaml" in App.xaml
  3. in codebehind (App.xaml.cs) add this override OnStartup:

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        DryContainer.Resolve<MainWindow>().Show();
    }
    

that's the startup point; that's also the only place where resolve should be called.

  1. the configuration root (according to Mark Seeman's book Dependency injection in .NET; the only place where concrete classes should be mentionned) will be in the same codebehind, in the constructor:

    public Container DryContainer { get; private set; }
    
    public App()
    {
        DryContainer = new Container(rules => rules.WithoutThrowOnRegisteringDisposableTransient());
        DryContainer.Register<IDatabaseManager, DatabaseManager>();
        DryContainer.Register<IJConfigReader, JConfigReader>();
        DryContainer.Register<IMainWindowViewModel, MainWindowViewModel>(
            Made.Of(() => new MainWindowViewModel(Arg.Of<IDatabaseManager>(), Arg.Of<IJConfigReader>())));
        DryContainer.Register<MainWindow>();
    }
    

Remarks and few more details

  • I used concrete class only with the the view MainWindow;
  • I had to specify which contructor to use (we need to do that with DryIoc) for the ViewModel, because the default constructor needs to exist for the XAML designer, and the constructor with injection is the actual one used for the application.

The ViewModel constructor with DI:

public MainWindowViewModel(IDatabaseManager dbmgr, IJConfigReader jconfigReader)
{
    _dbMgr = dbmgr;
    _jconfigReader = jconfigReader;
}

ViewModel default constructor for design:

public MainWindowViewModel()
{
}

The codebehind of the view:

public partial class MainWindow
{
    public MainWindow(IMainWindowViewModel vm)
    {
        InitializeComponent();
        ViewModel = vm;
    }

    public IViewModel ViewModel
    {
        get { return (IViewModel)DataContext; }
        set { DataContext = value; }
    }
}

and what is needed in the view (MainWindow.xaml) to get a design instance with ViewModel:

d:DataContext="{d:DesignInstance local:MainWindowViewModel, IsDesignTimeCreatable=True}"

Conclusion

We hence got a very clean and minimal implementation of a WPF application with a DryIoc container and DI while keeping design instances of views and viewmodels possible.

查看更多
姐就是有狂的资本
7楼-- · 2020-01-23 11:08

Use the Managed Extensibility Framework.

[Export(typeof(IViewModel)]
public class SomeViewModel : IViewModel
{
    private IStorage _storage;

    [ImportingConstructor]
    public SomeViewModel(IStorage storage){
        _storage = storage;
    }

    public bool ProperlyInitialized { get { return _storage != null; } }
}

[Export(typeof(IStorage)]
public class Storage : IStorage
{
    public bool SaveFile(string content){
        // Saves the file using StreamWriter
    }
}

//Somewhere in your application bootstrapping...
public GetViewModel() {
     //Search all assemblies in the same directory where our dll/exe is
     string currentPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
     var catalog = new DirectoryCatalog(currentPath);
     var container = new CompositionContainer(catalog);
     var viewModel = container.GetExport<IViewModel>();
     //Assert that MEF did as advertised
     Debug.Assert(viewModel is SomViewModel); 
     Debug.Assert(viewModel.ProperlyInitialized);
}

In general, what you would do is have a static class and use the Factory Pattern to provide you with a global container (cached, natch).

As for how to inject the view models, you inject them the same way you inject everything else. Create an importing constructor (or put a import statement on a property/field) in the code-behind of the XAML file, and tell it to import the view model. Then bind your Window's DataContext to that property. Your root objects you actually pull out of the container yourself are usually composed Window objects. Just add interfaces to the window classes, and export them, then grab from the catalog as above (in App.xaml.cs... that's the WPF bootstrap file).

查看更多
登录 后发表回答