如何处理 WPF/MVVM 应用程式中的依赖注入

我开始一个新的桌面应用程序,我想建立它使用 MVVM 和 WPF。

I am also intending to use 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;
}
}

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

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 would I do that?

例如,如果我有下面这个 xaml:

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

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

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

我觉得我完全迷失在这里,我会感激任何例子或指导是最好的方式来处理它。

我熟悉结构图,但我不是专家。此外,如果有一个更好的/更容易的/开箱即用的框架,请让我知道。

114013 次浏览

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

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


DataContext = viewModel;
}
}

这将取代基于 XAML 的方法。

我使用 Prism 框架来处理导航——当一些代码请求显示一个特定的视图(通过“导航”到它) ,Prism 将解析该视图(内部使用应用程序的 DI 框架) ; DI 框架将依次解析该视图所具有的任何依赖关系(我的例子中的视图模型) ,然后解析 它的依赖关系,等等。

DI 框架的选择几乎是无关紧要的,因为它们基本上都做同样的事情,也就是说,当框架发现对该接口的依赖关系时,您将注册一个接口(或类型)以及您希望框架实例化的具体类型。声明一下,我用的是温莎城堡。

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

或者看看其中一个 MVVM 框架,比如 MVVM Light。我没有这方面的经验,所以不能评论他们喜欢使用什么。

安装 MVVM 灯。

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

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

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

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

然后创建一个从 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" />

您现在可以绑定到它的“ MyViewModel”属性,以获得注入服务的 viewmodel。

希望能有所帮助,抱歉我的代码不准确,是 iPad 内存里的代码。

Use the 托管扩展性框架.

[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 模式为您提供一个全局容器(缓存,自然)。

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

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

见: Https://caliburnmicro.codeplex.com/wikipage?title=all%20about%20conventions

使用 Castle Windsor作为 IOC 容器。

关于聚会

Caliburn 的主要特色之一。微是显而易见的,它的能力,以消除锅炉板代码的需要,按照一系列的约定行事。有些人喜欢会议,有些人讨厌它们。这就是为什么 CM 的约定是完全可定制的,甚至可以完全关闭,如果不需要的话。如果您打算使用约定,并且默认情况下它们是 ON,那么最好知道这些约定是什么以及它们是如何工作的。这就是本文的主题。 视图解析(ViewModel-First)

Basics

使用 CM 时可能遇到的第一个约定与视图解析有关。此约定影响应用程序的任何 ViewModel-First 区域。在 ViewModel-First,我们需要将现有的 ViewModel 渲染到屏幕上。为此,CM 使用一个简单的命名模式来查找应该绑定到 ViewModel 并显示的 UserControl1。这是什么模式?我们来看看查看定位器。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);
};

让我们先忽略“上下文”变量。为了得到这个视图,我们假设您在命名 VM 时使用了文本“ ViewModel”,所以我们只需要通过删除单词“ Model”将其更改为“ View”。这会改变类型名称和命名空间。所以,视图模型。CustomerViewModel 将成为视图。CustomerView.或者如果您按照以下特性组织应用程序: CustomerManagement。CustomerViewModel 成为 CustomerManagement。CustomerView.希望你能直截了当。一旦我们有了名称,我们就可以搜索具有该名称的类型。我们通过 AssemblySource 搜索您公开给 CM 的任何程序集。实例2如果我们找到了这个类型,我们就创建一个实例(或者从 IoC 容器中获取一个实例,如果它被注册了的话)并将它返回给调用者。如果我们没有找到类型,我们将生成一个带有适当的“未找到”消息的视图。

现在,回到“上下文”值。这就是 CM 如何在同一视图模型上支持多个视图的。如果提供了上下文(通常是字符串或枚举) ,我们将根据该值对名称进行进一步的转换。这种转换有效地假设您为不同的视图拥有一个文件夹(名称空间) ,方法是从末尾删除单词“ View”并附加上下文。因此,在“掌握”我们的视图模型的上下文中。CustomerViewModel 将成为视图。顾客。师父。

在您的问题中,您用 XAML 设置了视图的 DataContext属性的值。这就要求你的视图模型有一个缺省构造函数。但是,正如你已经注意到的,当你想要在构造函数中注入依赖项时,这个依赖注入并不能很好地工作。

因此,你不能在 XAML 中设置 DataContext属性,也不能设置依赖注入。相反,你有其他的选择。

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

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的视图模型对象图,但是你可以将一些视图模型工厂注入到父视图模型中,允许它们创建新的子视图模型,这样对象图就不需要修复了。这也有望回答你的问题 假设我需要从我的 ABC2代码中获得一个 SomeViewModel的实例,我应该怎么做呢?

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 中也可以使用 设计时属性设计时属性。一种方法是在 XAML 中向 <Window>元素或 <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.
}


}

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

我一直在使用注射器,发现它是一种乐趣与工作。一切都是用代码设置的,语法相当简单,并且有很好的文档(关于 SO 有很多答案)。

基本上是这样的:

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

class UserControlViewModel
{
public UserControlViewModel(IStorage storage)
{


}
}

为视图模型创建一个带有 get 属性的 ViewModelLocator,该属性将从 Njet 加载视图模型:

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

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

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

UserControlDataContext绑定到 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
}
}

在应用程序启动时用必要的 Nregister 模块初始化 IoC 内核(目前是上面的模块) :

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


base.OnStartup(e);
}
}

I have used a static IocKernel class to hold the application wide instance of the IoC kernel, so I can easily access it when needed:

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,这是所有业务逻辑所在的位置。

If anyone has a better way, please do share.

编辑: Lucky Likey 提供了一个消除静态服务定位器的解决方案,它允许 Nregister 实例化 UI 类。答案的细节可以在这里看到

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


}

我在这里发布的是对 Sondergard 答案的改进,因为我将要讲述的内容与评论不符:)

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

到处都可以买到 StandardKernel

使用 .Get<T>()-Method 所需要的 StandardKernel-实例是 Njet 魔法的关键。

除了 Sondergard 的 IocContainer之外,你还可以在 App-Class 中创建 StandardKernel

只需从 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>

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

从现在开始,注射还活着并且准备好战斗:)

注射 DataContext

由于注射是活的,你可以进行各种注射,例如 属性设置程序注入或最常见的 构造函数注入

这就是如何将 ViewModel 注入到 WindowDataContext中的方法

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

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

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

虽然这可能是非常有用的,我不会建议你这样做。只要注意以这种方式注入的对象,将不会在构造函数内部可用,因为它是以后注入的。

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

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

如何使用注入。扩展。工厂也可以是红色的 给你

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

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 设计缺省构造函数:

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

结论

因此,我们得到了一个非常干净和最小的 WPF 应用程序实现,其中包含 DryIoc 容器和 DI,同时保持了视图和视图模型的设计实例。

另一个简单的解决方案是创建一个 murkup 扩展,它根据视图模型的类型来解析视图模型:

public class DISource : MarkupExtension {
public static Func<Type, object, string, object> Resolver { get; set; }


public Type Type { get; set; }
public object Key { get; set; }
public string Name { get; set; }


public override object ProvideValue(IServiceProvider serviceProvider) => Resolver?.Invoke(Type, Key, Name);
}

您可以通过以下方式将此扩展调整到任何 DI 容器:

protected override void OnStartup(StartupEventArgs e) {
base.OnStartup(e);
DISource.Resolver = Resolve;
}
object Resolve(Type type, object key, string name) {
if(type == null)
return null;
if(key != null)
return Container.ResolveKeyed(key, type);
if(name != null)
return Container.ResolveNamed(name, type);
return Container.Resolve(type);
}

Use it in XAML as simple as that:

DataContext="{local:DISource Type=viewModels:MainViewModel}"

通过这种方式,您可以轻松地将 DataContext 分配给视图,并使用 DI 容器自动将所有必需的参数直接注入到视图模型中。使用这种技术,您不必将 DI 容器或其他参数传递给 View 构造函数。

DISource 并不依赖于容器类型,所以你可以将它与任何依赖注入框架一起使用。这足够设置离间源了。属性设置为知道如何使用 DI 容器的方法。

我在 在 WPF 的 MVVM 应用程式中加入依赖注入中更详细地描述了这种技术