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

Fed*_*kin 90 c# wpf dependencies dependency-injection mvvm

我正在启动一个新的桌面应用程序,我想使用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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

public class SomeViewModel
{
    private IStorage _storage;

    public SomeViewModel(IStorage storage){
        _storage = storage;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

<Window 
    ... xmlns definitions ...
>
   <Window.DataContext>
        <local:SomeViewModel />
   </Window.DataContext>
</Window>
Run Code Online (Sandbox Code Playgroud)

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

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

我觉得我完全迷失了,我会感谢任何有关如何处理它的最好方法的例子或指导.

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

提前致谢.

son*_*ard 78

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

所以基本上它是这样的:

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

class UserControlViewModel
{
    public UserControlViewModel(IStorage storage)
    {

    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

<Application ...>
    <Application.Resources>
        <local:ViewModelLocator x:Key="ViewModelLocator"/>
    </Application.Resources>
</Application>
Run Code Online (Sandbox Code Playgroud)

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

<UserControl ...
             DataContext="{Binding UserControlViewModel, Source={StaticResource ViewModelLocator}}">
    <Grid>
    </Grid>
</UserControl>
Run Code Online (Sandbox Code Playgroud)

创建一个继承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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

        base.OnStartup(e);
    }
}
Run Code Online (Sandbox Code Playgroud)

我使用静态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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

  • 我是依赖注入的新手,但是你的解决方案是将服务定位器反模式与Ninject结合起来,因为你使用的是静态ViewModel定位器.有人可能认为注射是在Xaml文件中完成的,不太可能进行测试.我没有更好的解决方案,可能会使用你的解决方案 - 但我认为在答案中也提到这一点会很有帮助. (12认同)
  • @sondergard 我已经发布了对您答案的改进,避免了 ServiceLocator 反模式。随意检查一下。 (2认同)

Mar*_*age 48

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

因此,您无法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();
  }

}
Run Code Online (Sandbox Code Playgroud)

这是基于视图模型的对象图,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 */);
  }

}
Run Code Online (Sandbox Code Playgroud)

如果您的应用程序本质上更具动态性,并且可能基于导航,则必须挂钩执行导航的代码.每次导航到新视图时,都需要创建视图模型(来自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}"
Run Code Online (Sandbox Code Playgroud)

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

class MyViewModel : INotifyPropertyChanged {

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

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

}
Run Code Online (Sandbox Code Playgroud)

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

  • 这正是我所寻找的.让我感到沮丧的是,我读了多少次"只使用[*yadde-ya*]框架"的答案.这一切都很好,但我想确切知道如何自己滚动_first_然后我可以知道什么样的框架可能实际上对我有用.谢谢你这么清楚地拼写出来. (9认同)
  • @Shenron我的观点是,如果视图模型没有默认构造函数,则无法在 XAML“通用 WPF 方式”中设置数据上下文。我知道你不喜欢我的措辞,但我仍然认为这是一个有效的观点。问题是如何处理这个问题。这里有多个答案,您链接的答案通过使用自定义标记扩展提供了替代解决方案。你仍然必须找到问题的解决方案,这正是 Stack Overflow 的宗旨:提供问题的解决方案。 (3认同)

Luc*_*key 24

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

事实上,我正在引入一个简洁的解决方案,它避免了需要ServiceLocatorStandardKernel-Instance 的包装器,它在sondergard的Solution中被调用IocContainer.为什么?如上所述,这些都是反模式.

使得StandardKernel随处可见

Ninject的神奇之处在于使用StandardKernel-Method所需的-Instance .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>
Run Code Online (Sandbox Code Playgroud)

这是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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

注射你的 DataContext

当Ninject存活时,您可以执行各种注射,例如Property Setter Injection或最常见的一个Constructor Injection.

这就是你将ViewModel注入你Window的方式的方法DataContext

public partial class MainWindow : Window
{
    public MainWindow(MainWindowViewModel vm)
    {
        DataContext = vm;
        InitializeComponent();
    }
}
Run Code Online (Sandbox Code Playgroud)

当然,IViewModel如果你做了正确的绑定,你也可以注入一个,但这不是这个答案的一部分.

直接访问内核

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

    private void DoStuffWithKernel(IKernel kernel)
    {
        kernel.Get<Something>();
        kernel.Whatever();
    }
Run Code Online (Sandbox Code Playgroud)

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

    [Inject]
    public IKernel Kernel { private get; set; }
Run Code Online (Sandbox Code Playgroud)

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

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

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

如何使用Ninject.Extensions.Factory 在这里也可以是红色的.


And*_*ens 12

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

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

        DataContext = viewModel;
    }
}
Run Code Online (Sandbox Code Playgroud)

这取代了基于XAML的方法.

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

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

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

或者看一下MVVM Light之类的MVVM框架.我没有这些经验,所以不能评论他们喜欢用什么.

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

kid*_*haw 10

安装MVVM Light.

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

使用简单的IOC,您可以针对类型注册实现...

SimpleIOC.Default.Register<MyViewModel>(()=> new MyViewModel(new ServiceProvider()), true);
Run Code Online (Sandbox Code Playgroud)

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

然后,您创建一个从IOC返回实例的属性.

public MyViewModel
{
    get { return SimpleIOC.Default.GetInstance<MyViewModel>; }
}
Run Code Online (Sandbox Code Playgroud)

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

<local:ViewModelLocator x:key="Vml" />
Run Code Online (Sandbox Code Playgroud)

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

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


Sol*_*eil 7

佳能 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();
    }
    
    Run Code Online (Sandbox Code Playgroud)

这就是启动点;这也是唯一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>();
    }
    
    Run Code Online (Sandbox Code Playgroud)

备注和更多细节

  • 我只在视图中使用了具体类MainWindow
  • 我必须为 ViewModel 指定要使用的构造函数(我们需要使用 DryIoc 来实现),因为 XAML 设计器需要存在默认构造函数,而具有注入功能的构造函数是实际用于应用程序的构造函数。

带有 DI 的 ViewModel 构造函数:

public MainWindowViewModel(IDatabaseManager dbmgr, IJConfigReader jconfigReader)
{
    _dbMgr = dbmgr;
    _jconfigReader = jconfigReader;
}
Run Code Online (Sandbox Code Playgroud)

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

public MainWindowViewModel()
{
}
Run Code Online (Sandbox Code Playgroud)

视图的代码隐藏:

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

    public IViewModel ViewModel
    {
        get { return (IViewModel)DataContext; }
        set { DataContext = value; }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

d:DataContext="{d:DesignInstance local:MainWindowViewModel, IsDesignTimeCreatable=True}"
Run Code Online (Sandbox Code Playgroud)

结论

因此,我们使用 DryIoc 容器和 DI 获得了一个非常干净和最小的 WPF 应用程序实现,同时保持视图和视图模型的设计实例成为可能。