我有一个简单的WPF应用程序,它利用Unity框架进行依赖注入(inject)。当前,我正在尝试简化MVVM模式实现中的 View 之间导航方法。但是,整个堆栈溢出中的许多示例都没有考虑依赖注入(inject)的警告。
我有两种完全不同的看法。
第一,Main
充当将内容加载到其中的主窗口(非常典型;消除了不必要的内容):
<Window x:Class="Application.UI.Main">
<Grid Background="White">
<ContentControl Content="{Binding aProperty}"/>
</Grid>
</Window>
构造函数通过构造函数注入(inject)接收一个ViewModel(再次非常简单):
public partial class Main
{
private MainViewModel _mainViewModel;
public Main (MainViewModel mainViewModel)
{
InitializeComponent();
this.DataContext = _mainViewModel = mainViewModel;
}
}
然后,我有一个
UserControl
,Home
,我想在其上“导航”主窗口(即设置ContentControl
。它的构造函数还通过构造函数注入(inject)接收一个ViewModel,与Main
一样。同样简单:public Home(HomeViewModel homeViewModel)
{
InitializeComponent();
// Set Data Context:
this.DataContext = homeViewModel;
}
这里的主要问题是,我想启用基于构造函数的注入(inject),同时保持尽可能纯的MVVM实现。
我在MVVM的“查看优先”阵营中,您可以在其中找到一个不错的discussion in these comments。
我已经看到了基于导航的服务的一些暗示。但是,不确定是否可以保持MVVM所追求的关注点分离。
DataTemplates
要求不带参数的View构造函数,而我已阅读了对DataTemplates的批评,该批评认为ViewModels不应参与View的实例化。This solution(在我看来)是完全错误的,因为ViewModel意识到了自己的View并依赖于ViewModel实例化服务,这使得真正的依赖注入(inject)可以解析ViewModel和View依赖,但几乎是不可能的。这个问题在MSDN article中使用
RelayCommand
时非常明显。维护对
Main
View 的全局,单例引用的导航服务是否最有意义? Main
View 公开一个方法是否可以接受,例如:public void SetContent(UserControl userControl) { //... }
那该服务可以访问吗?
最佳答案
这是我实现另一作者提供的解决方案背后的动机。我不提供代码,因为链接文章提供了很棒的代码示例。这些当然是我的观点,但也代表了我对该主题的研究的结合
无需容器的解决方案
Rachel Lim撰写了一篇很棒的文章Navigation with MVVM,描述了如何充分利用WPF的DataTemplate
来解决MVVM导航所带来的挑战。 Lim的方法提供了“最佳”解决方案,因为它大大减少了对任何Framework依赖项的需求。但是,确实没有解决问题的“好方法”。
通常,对拉结的方法最大的反对意见是, View 模型将负责(或“定义”)它与 View 的关系。对于Lim的解决方案,这是次要的反对,其原因有两个(不作任何进一步证明其他不良体系结构决策的理由,如下所述):
1.)DataTemplate
关系不是由XAML文件强制执行的,也就是说, View 模型本身永远不会直接知道其 View ,反之亦然,因此,即使我们的View构造函数也得到了进一步简化,例如Home
类构造函数-现在无需引用 View 模型:
public Home()
{
InitializeComponent();
}
2.)由于没有其他地方表达这种关系,因此特定 View 和 View 模型之间的关联很容易更改。
一个应用程序应该能够在没有指定的View的情况下充分运行(建模域)。这种理想源自于努力使应用程序的支持体系结构最佳地分离,并促进SOLID编程原理(尤其是依赖注入(inject))的进一步应用。
XAML文件(不是第三方依赖关系容器)成为解决View和View Model之间关系的关键点(因此,它与OP直接矛盾)。
依赖注入(inject)
应将应用程序设计为完全不了解其容器,甚至更好的是,有关服务依赖关系的任何特定于实现的信息。这允许我们做的是将通过某种契约(Contract)(接口(interface))强制执行的服务“分配”(注入(inject))到组成我们应用程序功能的各个类。
这给我们留出了两个“良好设计”的标准:
第二点是最重要的,通常是被打破的“规则”。该“打破规则”是原始帖子背后的灵感。
在许多应用程序中,对导航问题的响应是注入(inject)一个包装的依赖项注入(inject)容器,然后将其用于从实现类中进行调用以解决依赖关系。该类(class)现在了解容器,更糟糕的是,它对执行操作所需的详细信息有了更多,更具体的了解(有些人可能认为更难维护)。
对View,View Model或Model方面的依赖关系解决方案容器的任何了解都是一种反模式(您可以在其他地方阅读更多有关该声明的理由)。
一个依赖于依赖注入(inject)的编写良好的应用程序可以在没有依赖注入(inject)框架的情况下运行,即您可以从手写 bootstrap 手动解决依赖关系(尽管这需要大量的精心工作)。
Lim的解决方案使我们能够从实现中“不需要”对容器的引用。
我们应该留给的是类似如下的构造函数:
// View:
public Home() { //... }
// View Model
public HomeViewModel (SomeModelClass someModel, MaybeAService someService)
如果一个目标是模块化和可重用性,那么上面就可以很好地实现这一目标。我们可以通过确保那些传入的依赖关系是通过接口(interface)的契约(Contract)履行,来继续进行抽象。