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


102

我正在启动一个新的桌面应用程序,我想使用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以注入依赖关系?

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

我感到自己完全迷失了这一点,对于任何如何处理它的最佳方式的示例或指导,我将不胜感激。

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


使用.net core 3.0预览版,您可以使用一些Microsoft nuget软件包来实现。
贝利·米勒

Answers:


87

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

所以基本上它是这样的:

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

class UserControlViewModel
{
    public UserControlViewModel(IStorage storage)
    {

    }
}

ViewModelLocator为视图模型创建一个带有get属性的对象,该属性会从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>

绑定DataContextUserControl在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);
        }
    }
}

此解决方案确实使用了静态ServiceLocatorIocKernel),通常将其视为反模式,因为它隐藏了类的依赖关系。但是,避免对UI类进行某种形式的手动服务查找非常困难,因为它们必须具有无参数的构造函数,而且您无论如何也无法控制实例化,因此无法注入VM。至少通过这种方式,您可以隔离地测试VM,这是所有业务逻辑所在的位置。

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

编辑:Lucky Likey通过让Ninject实例化UI类提供了摆脱静态服务定位器的答案。答案的细节可以在这里看到


13
我是依赖注入的新手,但从本质上讲,您的解决方案是将Service Locator反模式与Ninject结合在一起,因为您使用的是静态ViewModel Locator。有人可能会说注入是在Xaml文件中完成的,因此不太可能进行测试。我没有更好的解决方案,可能会使用您的解决方案-但我认为在答案中也将对此有所帮助。
user3141326

曼,您的解决方案很棒,只有以下一行代码可以解决“问题”:DataContext="{Binding [...]}"。这导致VS-Designer执行ViewModel的构造方法中的所有程序代码。在我的情况下,Window正在执行,并以模块方式阻止与VS的任何交互。也许应该修改ViewModelLocator以便不在设计时定位“真实的” ViewModel。-另一个解决方案是“禁用项目代码”,这也将防止显示其他所有内容。也许您已经找到了一个解决方案。在这种情况下,请您展示一下。
LuckyLikey

@LuckyLikey您可以尝试使用d:DataContext =“ {d:DesignInstance vm:UserControlViewModel,IsDesignTimeCreatable = True}”,但我不确定这会有所不同。但是,VM构造函数为什么/如何启动模式窗口?什么样的窗户?
sondergard

@son其实我不知道为什么和如何,但是当我从解决方案资源管理器中打开窗口设计器时,随着新标签页的打开,设计器将显示该窗口,并且该窗口看起来就像调试模式一样,托管在VS“ Micorosoft Visual Studio XAML设计器”之外的新流程中。如果该进程已关闭,则VS-Designer也会失败,并出现前面提到的异常。我将尝试您的解决方法。我会在检测到新信息时通知您:)
LuckyLikey

1
@sondergard我已对您的答案进行了改进,避免了ServiceLocator反模式。随时检查。
LuckyLikey

52

在您的问题中,您设置了DataContextXAML中视图属性的值。这要求您的视图模型具有默认构造函数。但是,正如您已经指出的那样,这与要在构造函数中注入依赖项的依赖项注入不能很好地配合。

因此,您无法DataContext在XAML中设置属性。相反,您还有其他选择。

如果您的应用程序基于简单的分层视图模型,则可以在应用程序启动时构造整个视图模型层次结构(您必须StartupUriApp.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框架提供的功能。

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

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

class MyViewModel : INotifyPropertyChanged {

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

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

}

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


12
这正是我在寻找的东西。我无数次地看到说“只使用[ yadde-ya ]框架”的答案,这使我感到沮丧。一切都很好,但是我想确切地知道如何首先自己介绍一下,然后才知道实际上对我而言哪种框架有用。感谢您清楚地说明。
kmote

28

我在这里发布的内容是对Sondergard's Answer的改进,因为我要说的内容不适合Comment :)

在Fact中,我将介绍一个简洁的解决方案,它避免了ServiceLocatorStandardKernel-Instance 的包装,在sondergard的Solution中将其称为IocContainer。为什么?如前所述,这些是反模式。

使得StandardKernel随处可见

Ninject的神奇法宝是StandardKernel-Instance,它是使用-Method所必需的.Get<T>()

除了Sondergard,IocContainer还可以在-Class 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中应用程序的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注入或最常见的一个Constructor注入

这是将ViewModel注入Window的方式DataContext

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

当然,IViewModel如果您进行正确的绑定,您也可以注入,但这不是此答案的一部分。

直接访问内核

如果您需要直接在内核上调用方法(例如.Get<T>()-Method),则可以让内核自行注入。

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

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

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

尽管这可能非常有用,但我不建议您这样做。请注意,以这种方式注入的对象将在构造函数内部不可用,因为稍后将其注入。

根据此链接,您应该使用出厂扩展名而不是注入IKernel(DI容器)。

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

Ninject.Extensions.Factory的使用方式在这里也可以是红色的。


好的方法。从未探索过Ninject到这个水平,但我可以看到我错过了:)
sondergard 17-4-27的

@son thx。在回答的最后,您说如果有人有更好的方法,请分享。您可以为此添加链接吗?
LuckyLikey

如果有人对如何使用Ninject.Extensions.Factory它感兴趣,请在评论中在此处说明,我将添加更多信息。
LuckyLikey

1
@LuckyLikey:如何通过没有无参数构造函数的XAML将ViewModel添加到窗口数据上下文中?使用Sondergard的ServiceLocator解决方案,这种情况将成为可能。
Thomas Geulen

因此,请告诉我如何检索附加属性中需要的服务?它们始终是静态的,包括后备DependencyProperty字段及其Get和Set方法。
springy76 '18年

12

我采用一种“视图优先”的方法,其中将视图模型传递给视图的构造函数(在其背后的代码中),该构造函数被分配给数据上下文,例如

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

        DataContext = viewModel;
    }
}

这将替代您基于XAML的方法。

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

DI框架的选择几乎无关紧要,因为它们基本上都在做相同的事情,即,您要注册一个接口(或类型)以及要让该框架找到依赖于该接口的实例化的具体类型。作为记录,我使用温莎城堡。

Prism导航需要一些时间来适应,但是一旦您掌握了它就可以了,这非常好,它允许您使用不同的视图来编写应用程序。例如,您可以在主窗口上创建一个“棱镜”“区域”,然后使用“棱镜”导航在该区域内从一个视图切换到另一个视图,例如,当用户选择菜单项或其他操作时。

或者,看看MVVM框架之一,例如MVVM Light。我没有这些经验,所以无法评论它们的用途。


1
如何将构造函数参数传递给子视图?我已经尝试过这种方法,但是在父视图中遇到异常,告诉我子视图没有默认的无参数构造函数
Doctor Jones

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”属性,以使用注入的服务获取视图模型。

希望能有所帮助。对于从iPad上的内存进行编码的任何代码错误,我们深表歉意。


您不应在应用程序的引导程序中使用GetInstanceresolve。那就是DI的重点!
Soleil-MathieuPrévot19年

我同意您可以在启动期间设置属性值,但是建议使用惰性实例反对DI是错误的。
kidshaw

@kishaw我没有。
Soleil-MathieuPrévot'19年

3

佳能DryIoc机箱

回答一个旧的帖子,但是这样做DryIoc并做我认为是对DI和接口的很好使用(最少使用具体类)。

  1. WPF应用程序的起始点是App.xaml,然后在这里告诉我们要使用的初始视图。我们用后面的代码代替默认的xaml:
  2. StartupUri="MainWindow.xaml"在App.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; }
    }
}

以及在视图(MainWindow.xaml)中需要什么才能使用ViewModel获得设计实例:

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

结论

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


2

使用托管可扩展性框架

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

通常,您将要做的是拥有一个静态类,并使用Factory Pattern为您提供一个全局容器(缓存的,natch的)。

至于如何注入视图模型,则以与注入其他所有对象相同的方式注入它们。在XAML文件的代码背后创建一个导入构造函数(或在属性/字段上放置导入语句),并告诉它导入视图模型。然后将您Window的绑定DataContext到该属性。您实际上实际从容器中拉出的根对象通常是组合Window对象。只需将接口添加到窗口类中,然后导出它们,然后从上面的目录中进行抓取(在App.xaml.cs中是WPF引导文件)。


您遗漏了DI的重要一点,那就是避免使用创建任何实例new
Soleil-MathieuPrévot19年

0

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

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

使用Castle Windsor作为IOC容器。

关于公约的一切

Caliburn.Micro的主要功能之一是通过执行一系列约定来消除对样板代码的需求。有些人喜欢约定,有些讨厌。这就是为什么CM的约定完全可自定义,甚至在不需要时甚至可以完全关闭的原因。如果您要使用约定,并且由于默认情况下处于启用状态,那么最好知道这些约定是什么以及它们如何工作。这就是本文的主题。视图分辨率(ViewModel优先)

基本

使用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”变量。为了获得视图,我们假设您在虚拟机的命名中使用的是“ ViewModel”文本,因此我们只需删除“模型”一词,便将其更改为“视图”。这具有更改类型名称和名称空间的效果。因此,ViewModels.CustomerViewModel将成为Views.CustomerView。或者,如果您要按功能组织应用程序:CustomerManagement.CustomerViewModel变为CustomerManagement.CustomerView。希望这很简单。一旦有了名称,便可以搜索具有该名称的类型。我们通过AssemblySource.Instance.2搜索可公开给CM的所有程序集。如果找到类型,我们将创建一个实例(如果已注册,则从IoC容器中获取一个实例),然后将其返回给调用方。如果找不到类型,

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


2
您的整个帖子都是意见。
约翰·彼得斯

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

}

你不应该有任何容器GetInstanceresolve外面app.xaml.cs,你失去DI点。另外,在视图代码的背后提及xaml视图有点令人费解。只需使用纯C#调用视图,然后对容器执行此操作。
Soleil-MathieuPrévot19年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.