将依赖注入添加到MVVM应用程序

4

尝试使用MVVM模式将WPF应用程序回填以使用依赖注入。我对DI并不太熟悉,之前只使用过一次,但我认为我理解了其中涉及的原则。

我需要确保所有绑定都在一个地方注册——应用程序根节点。在WPF中,这是OnStartup方法。因此,我使用Ninject并将其放入我的应用程序中,尝试自动绑定我的存储库类到初始视图:

private void OnStartup(object sender, StartupEventArgs e)
{
    IKernel kernel = new StandardKernel();
    kernel.Bind<IRepository>().To<Repository>();

    Views.MainView view = new Views.MainView();
    view.DataContext = kernel.Get<ViewModels.MainViewModel>();

    view.Show();
}

从现在开始,我将使用数据模板资源来设置上下文:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:views="clr-namespace:My.Views"
                    xmlns:models="clr-namespace:My.ViewModels" >
    <DataTemplate DataType="{x:Type models:MyViewModel}" >
        <views:MyView />
    </DataTemplate>
    <!-- etc -->
</ResourceDictionary>

并且它有效。太棒了!然而,在MainViewModel中,我按下一个按钮,并将不同类型的ViewModel加载到窗口中:

NavigationHelper.NewWindow(this, new QuoteViewModel(quote, new Repository()));

这行代码正是我第一次接触 DI 的原因 - 我无法测试它,因为我无法 mock 出这个依赖项。在这种情况下添加 DI 对我没有帮助,因为我只能在 OnStartUp 中使用我的 IoC 容器,所以我不能使用 kernel.Get 来获取我的 QuoteViewModel,对吗?

在 SO 上搜寻后,我发现有很多人推荐我使用服务定位器来解决这个问题。这对我来说很新颖,但也有很多人告诉我,将其用于 DI 是一种反模式,应该避免使用。到底谁是对的呢?

或许更重要的是,有没有什么巧妙的方法来解决这个问题?我看到了一些其他的例子,需要使用大量不同的包才能使它工作。现在,MVVM 和 DI 似乎就是不能完美地协作。


1
你一定要看看关于在WPF/MVVM中进行依赖注入的这个问题/答案,还有这个问题/答案 - Steven
2个回答

3

你已经很接近了,只需要完成两个步骤:

子视图模型的工厂

你需要一个能够创建子视图模型的工厂。为此工厂引入一个接口,这样你就可以在测试时替换它。

public interface IVmFactory {
    IQuoteViewModel CreateQuoteViewModel();
}

您可以自己实现这个接口,或者让NInject为您完成
请确保在DI容器中注册此工厂,以便当容器收到请求实例化具有该工厂依赖项的类时能够解析它。
ViewModels.MainViewModel中实现正确的依赖注入。
现在,您可以使用标准构造函数注入将IVmFactory注入到视图模型中:
public class MainViewModel {
    public MainViewModel(IVmFactory vmFactory) {
        _vmFactory = vmFactory;
    }

    // ...
}

依赖注入和MVVM

一旦你解决了WPF的默认构造函数问题(当你尝试让WPF实例化VM时),依赖注入和MVVM能够很好地协同工作。

依赖注入显然不是反模式,但服务定位器是。不幸的是,服务定位器通常与MVVM一起推荐,因为它使您能够快速入门,而无需创建工厂并正确注入它们。权衡起来,是选择快速入门还是具有清晰可测试的设计——由您决定。


-1

我同意服务定位器是一种反模式,我个人通过为StandardKernel(注入器)创建一个包装类来解决这个问题,该类实现了一个接口(IInjector),然后通过字段注入将自身注入到需要它的类中:

[Inject] public IInjector Injector {get; set;}

void MyClassMethod()
{
    var instance = this.Injector.Get<ISomeInterface>();
    // etc
}

这样可以消除服务定位反模式,并且抽象掉 DI 框架的实现细节。


这仍然是一个伪装成服务定位器的东西。通过将IInjector作为依赖项,一个类基本上在传达“我依赖于整个世界”。这会影响可读性和可测试性,并违反了接口隔离原则。 - theDmi
真的,但这只适用于所有实例都接收相同注入器的特定情况。 - Mark Feldman
1
不,这总是情况:在类型上,您可以通过注入器解析任何类型。您可以编写Injector.Get<string>(),并且它将编译。这很糟糕,因为它对开发人员没有帮助,接口可以解析任何东西(类型系统方面)。您实现了抽象服务定位器变体 - theDmi
如果你看不到一个类真正依赖的内容,你只能看到它使用了注入器。 - theDmi
好的观点,我想我需要重新学习一下设计模式。 - Mark Feldman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接