【问题标题】:Should I use additional container in my MVVM project?我应该在我的 MVVM 项目中使用额外的容器吗?
【发布时间】:2016-02-02 22:00:07
【问题描述】:

示例项目的来源: https://github.com/AntwanReno/navi

我在 WPF MVVM 中做了一个项目。它有三个子项目:WPF 应用程序、ViewModels (PCL) 和 Domain (PCL)。 WPF 只是一个 Window 与一个 Frame 和两个 Pages。我将展示代码,但建议在 repo 中克隆/分叉我准备好的示例。

这是 WPF 客户端的代码:

App.xaml.cs

namespace NaviWPFApp
{
    using System.Windows;
    using NaviWPFApp.Views;
    using NaviWPFApp.Views.Pages;   

    public partial class App : Application
    {
        public static NavigationService Navigation; 

        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);  

            MainWindow mainWindow = new MainWindow();
            mainWindow.Show();  

            Navigation = new NavigationService(mainWindow.MyFrame);
            Navigation.Navigate<FirstPage>();
        }
    }
}

App.xaml 只是:

<Application x:Class="NaviWPFApp.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:NaviWPFApp"
             x:Name="Application">
    <Application.Resources>
        <local:ViewModelLocator x:Key="ViewModelLocator"/>
    </Application.Resources>
</Application>

我有一个带框架的主窗口和两个非常相似的页面(没有代码隐藏):

<Window x:Class="NaviWPFApp.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="NaviWPFApp" Height="300" Width="300">
    <Grid>
        <Frame x:Name="MyFrame" Margin="10" />
    </Grid>
</Window>   

<Page x:Class="NaviWPFApp.Views.FirstPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      mc:Ignorable="d" 
      d:DesignHeight="300" d:DesignWidth="300" Title="FirstPage" 
      DataContext="{Binding FirstPageViewModel, Source={StaticResource ViewModelLocator}}">
    <Grid>
        <Button Command="{Binding GoToSecondPageCommand}"  Height="30" Content="Go to second page" />
    </Grid>
</Page> 


<Page x:Class="NaviWPFApp.Views.SecondPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      mc:Ignorable="d" 
      d:DesignHeight="300" d:DesignWidth="300"
      Title="SecondPage"
      DataContext="{Binding Path=SecondPageViewModel, Source={StaticResource ViewModelLocator}}">
    <StackPanel Margin="0, 100, 0, 0">
        <Button Command="{Binding CountSomethingCommand}" Content="Count something" Height="30" />
        <Button Command="{Binding BackToFirstPageCommand}" Content="Go back to page 1" Height="30" />
    </StackPanel>
</Page>

我的客户还有两个额外的类 ViewModelLocator 和 NavigationService - 用于页面之间的导航:

namespace NaviWPFApp
{
    using NaviWPFApp.ViewModels.Pages;  

    public class ViewModelLocator
    {
        public FirstPageViewModel FirstPageViewModel => new FirstPageViewModel(App.Navigation);
        public SecondPageViewModel SecondPageViewModel => new SecondPageViewModel(App.Navigation);
    }
}   

namespace NaviWPFApp
{
    using System;
    using System.Linq;
    using System.Reflection;
    using System.Windows.Controls;
    using NaviWPFApp.ViewModels.Common; 

    public class NavigationService : INavigationService
    {
        readonly Frame frame;   

        public NavigationService(Frame frame)
        {
            this.frame = frame;
        }   

        public void GoBack()
        {
            frame.GoBack();
        }   

        public void GoForward()
        {
            frame.GoForward();
        }   

        public bool Navigate(string page)
        {
            var type = Assembly.GetExecutingAssembly().GetTypes().SingleOrDefault(a => a.Name.Equals(page));
            if (type == null) return false; 

            var src = Activator.CreateInstance(type);
            return frame.Navigate(src);
        }

        public bool Navigate<T>(object parameter = null)
        {
            var type = typeof(T);
            return Navigate(type, parameter);
        }

        public bool Navigate(Type source, object parameter = null)
        {
            var src = Activator.CreateInstance(source);
            return frame.Navigate(src, parameter);
        }
    }
}

这是我的 ViewModels(便携式) 项目:

UI中每个Page只有两个ViewModel类,INavigationService(NavigationService的实现和UI客户端我什么都不想知道),MyObservableObject和MyCommand。

MyObservableObjectMyCommandINotifyPropertyChangedICommand接口的典型实现。

所以这是一个界面和两个视图模型:

public interface INavigationService
{
    void GoForward();
    void GoBack();
    bool Navigate(string page);
}


namespace NaviWPFApp.ViewModels
{
    public class FirstPageViewModel : MyObservableObject
    {
        private readonly INavigationService navigationService;

        public FirstPageViewModel(INavigationService navigationService)
        {
            this.navigationService = navigationService;
        }

        public MyCommand GoToSecondPageCommand
        {
            get { return new MyCommand(x => navigationService.Navigate("SecondPage")); }
        }
    }
}   

namespace NaviWPFApp.ViewModels
{
    public class SecondPageViewModel : MyObservableObject
    {
        private readonly INavigationService navigationService;
        private readonly BusinessLogic businessLogic;   

        public SecondPageViewModel(INavigationService navigationService, BusinessLogic businessLogic = null)
        {
            this.navigationService = navigationService;
            this.businessLogic = businessLogic;
        }   

        public MyCommand BackToFirstPageCommand
        {
            get { return new MyCommand(x => navigationService.Navigate("FirstPage")); }
        }   

        public MyCommand CountSomethingCommand
        {
            get { return new MyCommand(x => businessLogic?.CountSomething()); }
        }
    }
}

还有我的业务逻辑,就是这样:

public class BusinessLogic
{
    private int counter = 0;

    public bool CountSomething()
    {
        return ++counter > 10;
    }
}

依赖很简单:Domain什么都不知道,除了它自己的操作,ViewModel知道Domain,而对View和View一无所知。 . 好吧,这是我的问题——它知道 ViewModel,但 View 应该知道 Domain 吗?我将解释我的担忧,但这就是我的意思:

第一个关注点: 如您所见,导航全部在 ViewModel 中完成,业务逻辑仅在第二页视图模型中使用。 SecondPage 视图不需要了解逻辑。

第二个问题:因为我一直在尝试使用依赖注入,所以我想在程序开始时创建我的域对象(我只需要一个)。所以在protected override void OnStartup(StartupEventArgs e),所以在视图中。而且我不知道如何将它传递给在ViewModelLocator 中创建的第二个视图模型。

所以我的问题是:如何转换此代码,使其更加面向 ViewModel?我只想将我的域对象注入 ViewModel(它所属的地方),而不是 View。

感谢您的建议!

【问题讨论】:

  • tl;博士;对不起,伙计。你的问题是什么?看起来您已经在使用视图模型了。所以我看不出你在问什么。
  • 我不能回答你的问题,但我可以推荐使用caliburn.micro,它是一个轻量级的 MVVM 框架。使用它可以自动解决一些设计问题。它带来了自己的 IoC 容器和许多其他有用的功能。

标签: c# .net wpf xaml mvvm


【解决方案1】:

你不能避免这种依赖,因为App.OnStartup 是一个组合根,这意味着App.OnStartup 知道一切。 但是,您可以避免在您的应用程序中使用这个全局属性:public static NavigationService Navigation;。您可以简单地将其注入到您需要它的对象中。

第一个问题:如您所见,导航都在 ViewModel 和业务逻辑仅用于第二个页面视图模型。 SecondPage 视图不需要知道逻辑。

SecondPage 不必了解业务对象。应用程序必须知道。所以你可以将你的对象注入到定位器中,而定位器可以在时机成熟时将此对象注入到特定的 ViewModel 中。

第二个问题:因为我试图坚持依赖注入, 我想在 程序的开始。所以在受保护的覆盖无效 OnStartup(StartupEventArgs e),所以在 VIEW 中。我不知道,如何 将其传递给在 ViewModelLocator 中创建的第二个视图模型。

依赖注入。

我会这样做:

public class ViewModelLocator
{
    private NavigationService navigationService;
    private BusinessLogic businessLogic;
    public void InjectNavigationService(NavigationService navigation)
    {
        navigationService = navigation;
    } 
    public void InjectBusinessLogic(BusinessLogic logic)
    {
        businessLogic = logic;
    } 

    public FirstPageViewModel FirstPageViewModel => new FirstPageViewModel(navigationService);
    public SecondPageViewModel SecondPageViewModel => new SecondPageViewModel(navigationService, businessLogic);
}

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        // Create/resolve all your objects in Comoposition Root:
        var businessLogic = new BusinessLogic();

        // Here you will have locator created already, but mainWindow has not been created yet
        // Retrive your locator
        ViewModelLocator locator = Resources.Values.OfType<ViewModelLocator>().FirstOrDefault();
        if (locator == null)
            throw new NoNullAllowedException("ViewModelLocator cannot be null.");

        MainWindow mainWindow = new MainWindow();
        var navigation = new NavigationService(mainWindow.MyFrame);

        // Inject your logic and navigation into locator
        locator.InjectBusinessLogic(businessLogic);
        locator.InjectNavigationService(navigation);

        // Set up first page
        navigation.Navigate<FirstPage>();

        // and show the window
        mainWindow.Show();
    }
}

【讨论】:

    【解决方案2】:

    简短的回答是,您无法避免它,尤其是在您使用依赖注入/IoC 容器时(建议将您的代码解耦并提高可测试性)。

    您无法避免它的原因是(在使用 IoC 时)您需要通过构造函数(或通过属性/方法不是最佳的)注入您的依赖项。大多数 IoC 容器要求这些是公开的。

    由于大多数以这种方式注入的类型不在您的 ViewModel PCL 中,而是在您的模型(域)中并且公开可见,因此即使您没有手动初始化 ViewModel,也需要对程序集的引用。

    您的困惑来自“视图”一词。在这种情况下,您的 WPF 项目具有多个角色。

    1. 它用所有视图(XAML 文件)表示您的视图层
    2. 您的应用程序代码也在同一个项目/程序集中。应用程序代码类似于NavigationServce 实现(INavigationService 接口属于 ViewModel 程序集)

    虽然您的视图不需要对域类的任何引用,但您的应用程序却需要(组合根、DI/IoC 容器)。

    【讨论】:

      猜你喜欢
      • 2021-10-27
      • 2013-08-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-05-10
      • 2011-02-12
      • 2016-06-19
      • 2010-10-18
      相关资源
      最近更新 更多