C# 具有统一和单元测试架构设计的MVVM

C# 具有统一和单元测试架构设计的MVVM,c#,unit-testing,architecture,mvvm,unity-container,C#,Unit Testing,Architecture,Mvvm,Unity Container,我正在WPF中构建一个类似VisualStudio的应用程序,在确定组件的最佳体系结构设计组织时遇到了一些问题。我计划使用Unity作为我的依赖注入容器和VisualStudio单元测试框架,可能还有Mock库的moq 我将首先介绍我的解决方案的结构,然后介绍我的问题: 我有一个WPF项目,其中包含: 应用程序启动时我的Unity容器初始化(引导程序)(在App.xaml.cs中) 所有我的应用程序视图(XAML) 另一个名为ViewModel的项目包含: 我所有的应用程序视图模型。我的所

我正在WPF中构建一个类似VisualStudio的应用程序,在确定组件的最佳体系结构设计组织时遇到了一些问题。我计划使用Unity作为我的依赖注入容器和VisualStudio单元测试框架,可能还有Mock库的moq

我将首先介绍我的解决方案的结构,然后介绍我的问题:

我有一个WPF项目,其中包含:

  • 应用程序启动时我的Unity容器初始化(引导程序)(在App.xaml.cs中)
  • 所有我的应用程序视图(XAML)
另一个名为ViewModel的项目包含:

  • 我所有的应用程序视图模型。我的所有ViewModels都继承自ViewModelBase,该ViewModelBase公开了ILogger属性
我的初始化逻辑如下:

  • 应用程序启动
  • Unity容器创建和注册类型:MainView和MainViewModel
  • 解析我的主视图并显示它
    var window=Container.Resolve()

    window.Show()

    My MainView构造函数在其构造函数中接收MainViewModel对象:

    public MainView(MainViewModel _mvm)
    
  • My Main ViewModel的每个面板都有一个子ViewModel:

    public ToolboxViewModel ToolboxVM{get; set;}
    public SolutionExplorerViewModel SolutionExplorerVM { get; set; }
    public PropertiesViewModel PropertiesVM { get; set; }
    public MessagesViewModel MessagesVM { get; set; }
    
  • 我计划创建一个InitializePanels()方法来初始化每个面板

    现在我的问题是: 我的MainViewModel.InitializePanels()如何初始化所有这些面板?考虑到以下选项:

    选项1:手动初始化ViewModels:

    ToolboxVM = new ToolboxViewModel();
    //Same for the rest of VM...
    
    缺点:

    • 我没有使用Unity容器,因此我的依赖项(例如ILogger)不会自动解析
    选项2:通过注释我的属性使用setter注入:

    [Dependency]
    public ToolboxViewModel ToolboxVM{get; set;}
    //... Same for rest of Panel VM's
    
    缺点:

    • 我已经读到,应该避免使用Unity Setter依赖项,因为在这种情况下,它们会生成一个具有Unity的依赖项
    • 我还读到,您应该避免在单元测试中使用Unity,那么如何在我的单元测试中明确这种依赖关系呢?拥有许多依赖属性可能是一场噩梦
    选项3:使用Unity构造函数注入将所有我的面板视图模型传递给MainViewModel构造函数,以便Unity容器自动解析它们:

    public MainViewModel(ToolboxViewModel _tbvm, SolutionExploerViewModel _sevm,....)
    
    优点:

    • 在创建时,依赖关系将是明显和清晰的,这有助于构建我的ViewModel单元测试
    缺点:

    • 拥有如此多的构造函数参数可能很快就会变得难看
    选项4:在容器构建时注册所有我的VM类型。然后通过构造函数注入将UnityContainer实例传递到我的MainViewModel:

    public MainViewModel(IUnityContainer _container)
    
    这样我可以做一些事情,比如:

            Toolbox = _container.Resolve<ToolboxViewModel>();
            SolutionExplorer = _container.Resolve<SolutionExplorerViewModel>();
            Properties = _container.Resolve<PropertiesViewModel>();
            Messages = _container.Resolve<MessagesViewModel>();
    
    Toolbox=\u container.Resolve();
    SolutionExplorer=_container.Resolve();
    属性=_container.Resolve();
    Messages=_container.Resolve();
    
    缺点:

    • 如果我决定不使用Unity进行单元测试,正如许多人建议的那样,那么我将无法解析和初始化我的Panel ViewModels
    考虑到这个冗长的解释,什么是最好的方法,这样我就可以利用依赖注入容器并最终得到一个单元可测试的解决方案


    提前感谢,

    首先,您可能希望使用接口而不是具体的类,以便在单元测试时能够传递模拟对象,即
    IToolboxViewModel
    而不是
    ToolboxViewModel
    ,等等

    话虽如此,我还是推荐第三种选择——构造函数注入。这是最有意义的,因为否则您可以调用
    var mainVM=new MainViewModel()
    ,最终得到一个非功能视图模型。通过这样做,您还可以非常容易地理解视图模型的依赖关系,从而更容易编写单元测试


    我想退房,因为这与你的问题有关。

    第一件事第一。。。正如您所注意到的,在进行单元测试(复杂的VM初始化)时,您当前的设置可能会出现问题。然而,简单地遵循抽象,而不是具体化,会使这个问题立即消失。若您的视图模型将实现接口,并且依赖关系将通过接口实现,那个么任何复杂的初始化都将变得无关紧要,因为在测试中,您只需使用mock

    接下来,带注释属性的问题是在视图模型和Unity之间创建了高度耦合(这就是为什么它很可能是错误的)。理想情况下,注册应该在单个顶级点(在您的情况下是引导程序)进行处理,所以容器不会以任何方式绑定到它提供的对象。您的选项#3和#4是解决此问题最常见的解决方案,只需注意以下几点:

    • #3:通常通过将公共功能分组到中来缓解过多的构造函数依赖关系(但是4毕竟不是那么多)。通常,正确设计的代码没有这个问题。请注意,根据您的
      MainViewModel
      所做的工作,您可能只需要依赖于子视图模型列表,而不是具体的子视图模型
    • to#4:您不应该在单元测试中使用IoC容器。您只需手动创建
      MainViewModel
      (通过ctor)并手动注入模拟
    我想再谈一点。考虑当项目增长时会发生什么。将所有视图模型打包到单个项目中可能不是个好主意。每个视图模型都有自己的依赖项(通常与其他视图模型无关),所有这些都必须放在一起。这可能很快变得难以维持。相反,请考虑是否可以提取一些常见功能(例如消息传递、工具),并将它们放在不同的项目组中(同样,
    > MyApp.Users
    > MyApp.Users.ViewModels
    > MyApp.Users.Views
    > ...