WPF应用程序是否可以使用依赖注入?

38
我希望在我的WPF应用程序中开始使用依赖注入,主要是为了更好的单元测试性。我的应用程序大部分都是按照M-V-VM模式构建的。我正在考虑使用Autofac作为我的IoC容器,但我认为这对于本讨论并不太重要。
将服务注入到启动窗口似乎很简单,因为我可以在App.xaml.cs中创建容器并从中解析。
我遇到的问题是如何将ViewModels和Services DI到User Controls中?用户控件是通过XAML标记实例化的,因此没有机会对它们进行Resolve()
我能想到的最好的方法是将容器放在一个Singleton中,并让用户控件从全局容器中解析其ViewModels。这感觉像是一种半途而废的解决方案,因为它仍然需要我的组件依赖于ServiceLocator。
完整的IoC在WPF中可行吗?
[编辑] - 已经建议使用Prism,但即使评估Prism也似乎是一项巨大的投资。我希望有一个更小的解决方案。
[编辑] 这里是我停下来的代码片段
//setup IoC container (in app.xaml.cs)
var builder = new ContainerBuilder();
builder.Register<NewsSource>().As<INewsSource>();
builder.Register<AViewModel>().FactoryScoped();
var container = builder.Build();

// in user control ctor -
// this doesn't work, where do I get the container from
VM = container.Resolve<AViewModel>();

// in app.xaml.cs
// this compiles, but I can't use this uc, 
//as the one I want in created via xaml in the primary window
SomeUserControl uc = new SomeUserControl();
uc.VM = container.Resolve<AViewModel>();

Glenn Block通过播客和博客文章对Prism进行了一些介绍 - 我认为在评估它方面并没有太多的投资。 - Jedidja
Scott,你认为那个“大”投资是什么?你是在猜测呢还是已经看过了?Prism 的设计使得你可以只使用你需要的部分,没有太大的承诺。我很乐意与你线下聊聊这个问题。 - Glenn Block
8个回答

17
实际上非常容易做到。正如jedidja提到的,Prism中有这方面的示例。您可以使ViewModel注入View或者让View注入ViewModel。在Prism StockTraderRI中,我们将View注入到ViewModel中。实质上,View(和View接口)拥有一个Model属性。该属性在代码后台中实现为将DataContext设置为该值,例如:this.DataContext = value;。在ViewModel的构造函数中,会进行View的注入。然后它会设置View.Model = this;,将自身作为DataContext传递。
您也可以轻松地反过来,并将ViewModel注入到View中。我个人更喜欢这种方法,因为这意味着ViewModel不再具有与视图的任何后向引用。这意味着在对ViewModel进行单元测试时,甚至没有需要Mock的视图。此外,它使代码更加清晰,在View的构造函数中,它只需将DataContext设置为已注入的ViewModel即可。
我在与Jeremy Miller一起在Kaizenconf上举行的Separated Presentation Patterns讲座的视频录像中多谈了一些关于这方面的内容。其中的第一部分可以在此处找到:https://vimeo.com/2189854

7
很有帮助 - 我也更喜欢第二种方法,你知道哪里可以找到实现这个的代码示例吗? - Scott Weinstein

10

我认为你找到了问题所在。控件需要被注入到它们的父级中,而不是通过XAML声明式创建。

为了使DI起作用,一个DI容器应该创建接受依赖关系的类。这意味着在设计时,父级将没有任何子控件的实例,并且看起来像一个设计器中的外壳。这可能是推荐的方法。

另一种“替代”方法是从控件的构造函数或类似的地方调用全局静态容器。还有一个常见的模式,声明了两个构造函数,其中一个带有参数列表用于构造函数注入,另一个没有参数并委托给:

// For WPF
public Foo() : this(Global.Container.Resolve&lt;IBar&gt;()) {}

// For the rest of the world
public Foo(IBar bar) { .. }

我几乎会称之为反模式,但有些框架没有其他选择。

我对WPF的了解还不到一半,所以我预计会有很多人对此进行负面评价 :) 但希望这可以帮助您。Autofac团队(从主页链接)可能是另一个提出这个问题的地方。Prism或MEF示例应用程序(包括一些WPF示例)应该可以给您一个想法。


4
我们遇到了类似的问题。我们期待一种解决方案,能够在Expression Blend 2.0(强类型)下提供设计时支持。此外,我们期待一种解决方案,可以在Expression Blend下提供一些模拟和自动生成的数据样本。
当然,我们也希望所有这些都能使用IOC模式工作。
Paul Stovell有一篇有趣的文章可以开始阅读: http://www.paulstovell.com/blog/wpf-dependency-injection-in-xaml 因此,我尝试了一些方法来增加绑定和模拟对象的有价值的设计时支持,目前我遇到的大部分问题与在View(代码)和ModelView(Xaml)之间建立强类型连接有关。我尝试了一些场景:
解决方案1:使用泛型创建视图
public class MyDotNetcomponent<T> : SomeDotNetcomponent 
{
    // Inversion of Control Loader…
    // Next step add the Inversion of control manager plus
    // some MockObject feature to work under design time
    public T View {Get;}
}

这个解决方案行不通,因为Blend不支持设计面板内部的泛型,但Xaml确实有一些,在运行时可以工作但在设计时不行;
解决方案2:ObjectDataProvider
<ObjectDataProvider ObjectType="{x:Type CP:IFooView}" />
<!-- Work in Blend -->
<!—- IOC Issue: we need to use a concrete type and/or static Method there no way to achive a load on demande feature in a easy way -->

解决方案三:继承 ObjectDataProvider
<CWD:ServiceObjectDataProvider ObjectType="{x:Type CP:IFooView}" />
<!-- Cannot inherit from ObjectDataProvider to achive the right behavior everything is private-->

解决方案4:从头创建一个模拟的ObjectDataProvider来完成工作。
<CWD:ServiceObjectDataProvider ObjectType="{x:Type CP:IFooView }" />
<!-- Not working in Blend, quite obvious-->

解决方案5:创建标记扩展(Paul Stovell)
<CWM:ServiceMarkup MetaView="{x:Type CP:IFooView}"/>
<!-- Not working in Blend -->

仅想澄清一点。当我说“在混合中无法工作”时,我的意思是绑定对话框不可用,设计师需要自己手写XAML。

我们接下来的步骤可能是花时间评估为Expression Blend创建插件的能力。


这个视频提供了一个漂亮的方法来使用M-V-VM并获得设计师/Blend集成 - 跳到最后的10-20分钟获取这些细节。 http://www.lab49.com/files/videos/Jason%20Dolinger%20MVVM.wmv - Scott Weinstein

3
你应该看一下Caliburn - 它是一个简单的WPF/Silverlight MVC框架,支持完整的DI。它看起来非常酷,让你可以使用任何IoC容器。在文档wiki上有几个示例。

Caliburn是一个遗留框架。所有的开发都集中在Caliburn.Micro上。| Caliburn.Micro-不再处于活跃维护状态。| 公告| GitHub讨论。 - Pang
当然可以。(PS:我在12年前写下了上面的答案......) - mookid8000

3

是的,我们经常这样做。您可以将ViewModel“注入”到控件的DataContext中。

我发现使用依赖注入(DI)甚至更容易使用WPF。即使依赖对象和属性也可以无缝地与其配合使用。


1

Glen Block(见上文)提到,一种常见的方法是设计你的MVVM解决方案,使用DataContext作为在View中“解析”View Model的位置。然后,你可以使用来自Expression Blend 2008的设计扩展(请注意,你不需要使用Expression Blend设计工具来利用此功能)。例如:

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:MyViewModelMock, IsDesignTimeCreatable=True}"

在您的视图中,您可以拥有一个属性 getter,将您的 DataContext 强制转换为您所期望的类型(只是为了使它更容易在代码后台中使用)。
private IMyViewModel ViewModel { get { return (IMyViewModel) DataContext; } }

请不要忘记使用接口,这样您的视图将更容易测试,或者帮助您注入不同的运行时实现。
通常情况下,您不应该在解决方案中的各个地方从容器中解析事物。在每个构造函数中传递容器或使其全局可访问实际上被认为是一种不良做法(您应该查阅有关“服务定位器”策略构成“反模式”的讨论)。
创建一个公共的View构造函数,其中包含显式依赖项,容器(例如Prism Unity或MEF)可以解析。
如果必要,您也可以创建一个内部默认构造函数来创建您的视图模型(或者实际的视图模型)的模拟。这可防止意外地在您的“Shell”或任何其他地方外部使用此“设计构造函数”。您的测试项目也可以使用这样的构造函数,使用“AssemblyInfo”中的“InternalsVisibleToAttribute”。但是,通常情况下并不需要这样做,因为您可以使用完全依赖于构造函数注入您的模拟,而且大部分测试应该首先关注ViewModel。View中的任何代码都应该非常简单。如果您的View需要进行大量测试,则可能需要问问自己为什么这样!
Glen提到您可以将View注入到ViewModel中,或者将ViewModel嵌入到View中。我更喜欢后者,因为有完全良好的解耦技术(使用声明式绑定、命令、事件聚合、中介者模式等)。View Model是执行核心业务逻辑所需的所有重要工作的地方。如果View Model提供了所有必要的"绑定"点,它实际上不需要了解View(这在XAML中大多可以声明性地连接)。
如果我们使View Model对用户交互源不可知,那么它将更容易进行测试(最好是首先)。这也意味着您可以轻松地插入任何视图(WPF、Silverlight、ASP.NET、Console等)。实际上,为确保已实现适当的解耦,我们可以问自己是否可以在Workflow服务的上下文中使用"MVM"(Model-ViewModel)体系结构。当您停下来思考时,您的大多数单元测试可能会根据这个前提设计。

0

我认为你需要先决定是采用视图优先还是视图模型优先的方式,然后像其他回答中所提到的那样,就可以做出决策了。有几个开源框架采用相同的方式。我使用Caliburn,其中采用了视图模型优先的方式,这是一种非常好的方法。


同意。当我写这个问题时,首先考虑的是视图。 - Scott Weinstein

0

我编写了一个非常轻量级的框架,其中使用IoC(Unity)作为标记扩展,在运行时解析ViewModel。

该框架允许编写XAML而无需代码后台,但仍可让您拥有路由命令、数据绑定和事件处理程序。

无论如何,我认为在您的情况下不需要松散的XAML,但如果您查看代码(http://xtrememvvm.codeplex.com),可能会发现您可以使用其中一些代码来解决注入视图模型和服务的问题。


请不要发布仅宣传您的代码的答案。在此处回答问题(附带代码片段),并链接到您的代码(显式标记为代码参考)。 - ChrisF

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