如何在WPF / MVVM应用程序中处理依赖项注入

时间:2014-08-18 14:56:53

标签: c# wpf mvvm dependency-injection dependencies

我正在启动一个新的桌面应用程序,我想使用MVVM和WPF构建它。

我也打算使用TDD。

问题在于我不知道如何使用IoC容器将依赖项注入我的生产代码。

假设我有以下类和接口:

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

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

然后我有另一个以IStorage为依赖的类,假设此类是ViewModel或业务类......

public class SomeViewModel
{
    private IStorage _storage;

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

有了这个,我可以轻松编写单元测试,以确保它们正常工作,使用模拟等。

问题在于在实际应用中使用它。我知道我必须有一个链接IStorage接口的默认实现的IoC容器,但我该怎么办?

例如,如果我有以下xaml会怎么样:

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

在这种情况下,如何正确地告诉'WPF注入依赖项?

另外,假设我需要SomeViewModel代码中cs的实例,我应该怎么做?

我觉得我完全迷失了,我会很感激任何一个例子或指导如何处理它。

我熟悉StructureMap,但我不是专家。此外,如果有更好/更容易/开箱即用的框架,请告诉我。

提前致谢。

9 个答案:

答案 0 :(得分:73)

我一直在使用Ninject,并发现与之合作很愉快。一切都是在代码中设置的,语法相当简单,并且有很好的文档(以及大量的答案)。

所以基本上它是这样的:

创建视图模型,并将IStorage接口作为构造函数参数:

class UserControlViewModel
{
    public UserControlViewModel(IStorage storage)
    {

    }
}

使用视图模型的get属性创建一个ViewModelLocator,它从Ninject加载视图模型:

class ViewModelLocator
{
    public UserControlViewModel UserControlViewModel
    {
        get { return IocKernel.Get<UserControlViewModel>();} // Loading UserControlViewModel will automatically load the binding for IStorage
    }
}

使ViewModelLocator成为App.xaml中的应用程序范围的资源:

<Application ...>
    <Application.Resources>
        <local:ViewModelLocator x:Key="ViewModelLocator"/>
    </Application.Resources>
</Application>

将UserControl的DataContext绑定到ViewModelLocator中的相应属性。

<UserControl ...
             DataContext="{Binding UserControlViewModel, Source={StaticResource ViewModelLocator}}">
    <Grid>
    </Grid>
</UserControl>

创建一个继承NinjectModule的类,它将设置必要的绑定(IStorage和viewmodel):

class IocConfiguration : NinjectModule
{
    public override void Load()
    {
        Bind<IStorage>().To<Storage>().InSingletonScope(); // Reuse same storage every time

        Bind<UserControlViewModel>().ToSelf().InTransientScope(); // Create new instance every time
    }
}

在应用程序启动时使用必要的Ninject模块初始化IoC内核(现在就是上面的模块):

public partial class App : Application
{       
    protected override void OnStartup(StartupEventArgs e)
    {
        IocKernel.Initialize(new IocConfiguration());

        base.OnStartup(e);
    }
}

我使用静态IocKernel类来保存IoC内核的应用程序范围实例,因此我可以在需要时轻松访问它:

public static class IocKernel
{
    private static StandardKernel _kernel;

    public static T Get<T>()
    {
        return _kernel.Get<T>();
    }

    public static void Initialize(params INinjectModule[] modules)
    {
        if (_kernel == null)
        {
            _kernel = new StandardKernel(modules);
        }
    }
}

此解决方案确实使用了静态ServiceLocator(IocKernel),它通常被视为反模式,因为它隐藏了类的依赖项。但是,要避免对UI类进行某种手动服务查找是非常困难的,因为它们必须具有无参数构造函数,并且无论如何都无法控制实例化,因此无法注入VM。至少这种方式允许您单独测试VM,这是所有业务逻辑的所在。

如果有人有更好的方法,请分享。

编辑: 幸运的Likey通过让Ninject实例化UI类提供了摆脱静态服务定位器的答案。可以看到答案的详细信息here

答案 1 :(得分:45)

在您的问题中,您在XAML中设置视图的DataContext属性的值。这要求您的视图模型具有默认构造函数。但是,正如您所指出的,对于要在构造函数中注入依赖项的依赖项注入,这不能很好地工作。

所以您无法在XAML中设置DataContext属性。相反,你有其他选择。

如果应用程序基于简单的分层视图模型,则可以在应用程序启动时构建整个视图模型层次结构(您必须从StartupUri文件中删除App.xaml属性) :

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();
  }

}

这基于以RootViewModel为根的视图模型的对象图,但是您可以将一些视图模型工厂注入到父视图模型中,从而允许它们创建新的子视图模型,因此对象图可以不必修复。这也有希望回答你的问题假设我需要SomeViewModel代码cs的实例,我应该怎么做?

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 */);
  }

}

如果您的应用程序本质上更具动态性,并且可能基于导航,则必须挂钩执行导航的代码。每次导航到新视图时,都需要创建视图模型(来自DI容器),视图本身并将视图的DataContext设置为视图模型。您可以执行此操作首先查看,根据视图选择视图模型,或者您可以执行视图模型,其中视图模型确定要使用的视图。 MVVM框架提供了这一关键功能,您可以通过某种方式将DI容器挂钩到视图模型的创建中,但您也可以自己实现它。我在这里有点模糊,因为根据您的需要,这个功能可能变得非常复杂。这是您从MVVM框架获得的核心功能之一,但在简单的应用程序中滚动自己的功能将使您更好地了解MVVM框架提供的内容。

由于无法在XAML中声明DataContext,您将失去一些设计时支持。如果您的视图模型包含一些数据,它将在设计时出现,这可能非常有用。幸运的是,您也可以在WPF中使用design-time attributes。一种方法是将以下属性添加到<Window>元素或XAML中的<UserControl>

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}"

视图模型类型应该有两个构造函数,设计时数据的默认值和依赖注入的另一个:

class MyViewModel : INotifyPropertyChanged {

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

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

}

通过这样做,您可以使用依赖注入并保留良好的设计时支持。

答案 2 :(得分:21)

我在这里发布的内容是对sondergard的答案的改进,因为我要说的不适合评论:)

实际上,我正在介绍一个简洁的解决方案,它避免了 ServiceLocator StandardKernel - Instance的包装器的需要,在sondergard的解决方案中称为IocContainer 。为什么?如上所述,这些都是反模式。

使StandardKernel随处可用

Ninject神奇的关键是StandardKernel - 使用.Get<T>() - 方法所需的实例。

除了sondergard的IocContainer,你可以在StandardKernel - 类中创建App

只需从App.xaml中删除StartUpUri

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

这是App.xaml.cs中App的CodeBehind

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();
    }
}

从现在开始,Ninject还活着,准备战斗:)。

注入DataContext

当Ninject存活时,您可以执行各种注射,例如 Property Setter Injection 或最常见的构造函数注射

这是您将ViewModel注入Window的{​​{1}}

的方式
DataContext

当然,如果你做了正确的绑定,你也可以注入一个public partial class MainWindow : Window { public MainWindow(MainWindowViewModel vm) { DataContext = vm; InitializeComponent(); } } ,但这不是这个答案的一部分。

直接访问内核

如果需要直接在内核上调用方法(例如IViewModel - 方法), 你可以让内核注入自己。

.Get<T>()

如果您需要内核的本地实例,可以将其作为Property注入。

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

虽然这可能非常有用,但我不建议您这样做。请注意,以这种方式注入的对象在构造函数中不可用,因为它稍后会被注入。

根据此link,你应该使用factory-Extension而不是注入 [Inject] public IKernel Kernel { private get; set; } (DI容器)。

  

在软件系统中使用DI容器的推荐方法是,应用程序的组合根是直接触摸容器的单个位置。

如何使用Ninject.Extensions.Factory也可以是红色here

答案 3 :(得分:12)

我首先去看看#34;方法,我将视图模型传递给视图的构造函数(在其代码隐藏中),它被分配给数据上下文,例如

public class SomeView
{
    public SomeView(SomeViewModel viewModel)
    {
        InitializeComponent();

        DataContext = viewModel;
    }
}

这取代了基于XAML的方法。

我使用Prism框架来处理导航 - 当某些代码请求显示特定视图时(通过&#34;导航&#34;到它),Prism将解析该视图(在内部,使用应用程序&#39; s DI框架); DI框架将依次解析视图所具有的任何依赖关系(在我的示例中为视图模型),然后解析依赖关系,依此类推。

DI框架的选择几乎无关紧要,因为它们基本上都是一样的,即你注册一个接口(或类型)以及你希望框架在找到对该接口的依赖时实例化的具体类型。为了记录,我使用Castle Windsor。

Prism导航需要一些习惯,但是一旦你了解它就非常好,允许你使用不同的视图组合你的应用程序。例如。你可能会创建一个Prism&#34; region&#34;在您的主窗口上,然后使用棱镜导航,您将在该区域内从一个视图切换到另一个视图,例如当用户选择菜单项或其他任何内容时。

或者看看其中一个MVVM框架,例如MVVM Light。我没有这些经验,因此无法评论他们想要使用的内容。

答案 4 :(得分:10)

安装MVVM Light。

部分安装是创建视图模型定位器。这是一个将视图模型公开为属性的类。然后,可以从IOC引擎返回这些属性的getter实例。幸运的是,MVVM light还包括SimpleIOC框架,但如果您愿意,可以在其他框架中连接。

使用简单的IOC,您可以针对类型...

注册实现
SimpleIOC.Default.Register<MyViewModel>(()=> new MyViewModel(new ServiceProvider()), true);

在此示例中,将创建视图模型,并根据其构造函数传递服务提供者对象。

然后创建一个从IOC返回实例的属性。

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

聪明的部分是视图模型定位器随后在app.xaml中创建或等效创建为数据源。

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

您现在可以绑定到其“MyViewModel”属性,以使用注入的服务获取您的viewmodel。

希望有所帮助。对从iPad上的内存编码的任何代码不准确表示歉意。

答案 5 :(得分:2)

使用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);
}

通常,您要做的是使用静态类并使用工厂模式为您提供全局容器(缓存,自然)。

关于如何注入视图模型,您可以像注入其他所有内容一样注入它们。在XAML文件的代码隐藏中创建导入构造函数(或在属性/字段上放置import语句),并告诉它导入视图模型。然后将Window DataContext绑定到该媒体资源。您实际从容器中拉出的根对象通常由Window个对象组成。只需将接口添加到窗口类,然后导出它们,然后从上面的目录中获取(在App.xaml.cs中......这是WPF引导程序文件)。

答案 6 :(得分:1)

佳能DryIoc盒

回答一则旧文章,但使用DryIoc进行此操作,并做我认为可以很好地利用DI和接口(最少使用具体类)的事情。

  1. WPF应用程序的起点是App.xaml,在这里我们告诉我们要使用的初始视图;我们用后面的代码代替默认的xaml:
  2. 在App.xaml中删除StartupUri="MainWindow.xaml"
  3. 在代码后面(App.xaml.cs)中添加以下override OnStartup

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

这是启动点;这也是应该调用resolve的唯一地方。

  1. 配置根目录(根据Mark Seeman的书《 .NET中的依赖注入;应该在其中提到具体类的唯一地方》)将在构造函数中位于相同的代码后面:

    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>();
    }
    

备注和更多细节

  • 我仅在视图MainWindow中使用了具体的课程;
  • 我必须为ViewModel指定使用哪个构造函数(我们需要对DryIoc进行此操作),因为XAML设计器需要存在默认构造函数,并且带有注入功能的构造函数是实际用于该应用程序的构造函数。

带有DI的ViewModel构造函数:

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

ViewModel用于设计的默认构造函数:

public MainWindowViewModel()
{
}

该视图的代码:

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

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

以及使用ViewModel获取设计实例所需的视图(MainWindow.xaml):

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

结论

因此,我们使用DryIoc容器和DI对WPF应用程序进行了非常干净,最少的实现,同时使视图和视图模型的设计实例成为可能。

答案 7 :(得分:-1)

我建议使用ViewModel - First方法 https://github.com/Caliburn-Micro/Caliburn.Micro

请参阅: https://caliburnmicro.codeplex.com/wikipage?title=All%20About%20Conventions

使用Castle Windsor作为IOC容器。

所有关于约定

Caliburn.Micro的一个主要特点是能够通过一系列约定来消除对锅炉板代码的需求。有些人喜欢传统,有些人讨厌他们。这就是为什么CM的约定是完全可定制的,甚至可以在不需要时完全关闭。如果您打算使用约定,并且由于默认情况下它们处于启用状态,那么最好知道这些约定是什么以及它们如何工作。这是本文的主题。 查看分辨率(ViewModel-First)

基本

使用CM时可能遇到的第一个约定与视图分辨率有关。此约定会影响应用程序的任何ViewModel-First区域。在ViewModel-First中,我们需要一个现有的ViewModel渲染到屏幕上。为此,CM使用一个简单的命名模式来查找它应绑定到ViewModel并显示的UserControl1。那么,那种模式是什么?让我们看一下ViewLocator.LocateForModelType来找出:

public static Func<Type, DependencyObject, object, UIElement> LocateForModelType = (modelType, displayLocation, context) =>{
    var viewTypeName = modelType.FullName.Replace("Model", string.Empty);
    if(context != null)
    {
        viewTypeName = viewTypeName.Remove(viewTypeName.Length - 4, 4);
        viewTypeName = viewTypeName + "." + context;
    }

    var viewType = (from assmebly in AssemblySource.Instance
                    from type in assmebly.GetExportedTypes()
                    where type.FullName == viewTypeName
                    select type).FirstOrDefault();

    return viewType == null
        ? new TextBlock { Text = string.Format("{0} not found.", viewTypeName) }
        : GetOrCreateViewType(viewType);
};

首先让我们忽略“context”变量。为了得到视图,我们假设您在VM的命名中使用了文本“ViewModel”,因此我们只需通过删除单词“Model”将其更改为“查看”我们找到它的任何位置。这具有更改类型名称和名称空间的效果。因此ViewModels.CustomerViewModel将成为Views.CustomerView。或者,如果您按功能组织应用程序:CustomerManagement.CustomerViewModel将成为CustomerManagement.CustomerView。希望,这很直接。获得名称后,我们会搜索具有该名称的类型。我们通过AssemblySource.Instance.2搜索您已经暴露给CM的任何程序集。如果我们找到该类型,我们创建一个实例(如果已经注册,则从IoC容器中获取一个实例)并将其返回给调用者。如果我们找不到类型,我们会生成一个带有相应“未找到”消息的视图。

现在,回到那个“上下文”的价值。这就是CM在同一ViewModel上支持多个视图的方式。如果提供了上下文(通常是字符串或枚举),我们会根据该值对名称进行进一步转换。通过从末尾删除单词“View”并附加上下文,此转换有效地假定您具有不同视图的文件夹(命名空间)。因此,给定“Master”的上下文,我们的ViewModels.CustomerViewModel将成为Views.Customer.Master。

答案 8 :(得分:-1)

从app.xaml中删除启动uri。

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

现在您可以使用您的IoC类来构建实例。

MainWindowView.xaml.cs

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

        //Do other configuration            

        DataContext = mainWindowViewModel;

        InitializeComponent();
    }

}