【问题标题】:How to seperate commands out of big viewmodel如何从大视图模型中分离命令
【发布时间】:2016-03-16 18:29:06
【问题描述】:

我的视图模型包含很多命令,它使我的视图模型非常大。我想将我的命令与视图模型分开。目前,我的解决方案是为每个命令创建一个类,如下所示,

 public class TestCommand : CommandBase
{
    private MainViewModel vm;

    public TestCommand(MainViewModel vm)
    {
        this.vm = vm;
    }

    public override bool CanExecute(object parameter)
    {
        return true;
    }

    public override void ExecuteCommand(object parameter)
    {
        vm.logger.log(...);
        ...
    }
}

由于我需要在 ViewModel 中使用一些方法或属性,所以我必须将 viewmodel 作为参数传递给命令。对于这个解决方案,有两个缺点: 1. 项目中有很多命令文件,如果一个视图的命令平均数为15,10个视图项目中有150个命令文件; 2. 将 ViewModel 作为参数传递给命令需要将一些应该是私有的属性或方法更改为公共的;将视图模型传递给命令也很奇怪。

有没有其他的分离命令的解决方案?

【问题讨论】:

  • 如果您只关心可见性,您可以将它们抽象为部分类。但是,如果您有很多命令,则可能暗示您的 ViewModel 尝试做太多事情。请记住,上述解决方案可能会破坏您的视图模型封装,因为上述命令只能访问公共属性和方法
  • 你能提供一些更具体的命令吗?如果您尝试在 ViewModel 中放置太多关注点,或者您只需要更好的抽象,以获得更好的概览

标签: c# wpf mvvm architecture


【解决方案1】:

TL;DR:

ViewModel 是主要在命令中表达的表示逻辑,因此命令占用大量 ViewModel 代码的情况并不少见。不要试图用INotifyPropertyChanged 使 ViewModel 成为普通数据持有者(就像使用 ViewModel 时在 ASP.NET MVC 中常见的那样)。

加长版

由于缺乏更多详细信息,因此很难为您提供具体提示,但这里有一些通用指南。您可以使用有关您正在使用的命令类型的更多详细信息来更新您的问题,我会尝试更新问题。

  1. 呈现逻辑

    ViewModel 的主要关注点是展示。 ViewModel 中没有业务逻辑的位置。

    必须将业务逻辑提取到您的业务/域模型(如果您遵循丰富的域模型)或服务(在贫乏的域模型中)。在富域模型中,您的服务层通常非常薄,并且主要用于协调多个模型之间的操作。

    因此,如果您的 ViewModel/命令正在执行与演示无关的任何类型的逻辑(如果单击按钮 A,则禁用按钮 B、C 和 D 或隐藏 GroupBoxA 或“如果数据丢失则禁用按钮 A(CanExecute of @ 987654323@)) 可能做得太多了。

  2. 关注点分离

    您的 ViewModel 可能会尝试做的事情超出其预期。您示例中的记录器就是这样的提示。日志记录不是 ViewModel 关注的问题。

    ViewModel 是关于表示和表示逻辑的,而日志记录是一个应用程序问题(因为它不属于域/业务逻辑)。

    通常一个 ViewModel 可以拆分为两个或多个 ViewModel(即管理客户列表并允许编辑所选客户的 ViewModel,通常可以拆分为 2 或 3 个 ViewModel:CustomersViewModel(显示列表), CustomerDetailViewModelCustomerViewModel(客户详细信息)和 CustomerEditViewModel(编辑相关客户)

    日志和缓存等问题应该使用装饰器模式来完成。这要求您的服务和/或存储库正确使用和实现接口,然后您可以创建用于缓存或日志记录的装饰器,而不是注入服务的原始实例,而是实现装饰器。

    依赖注入 (DI) 和控制反转 (IoC) 容器确实可以帮助您解决这个问题。不得不手动连接它(又名可怜的人 DI)是一件很痛苦的事情。具体示例超出了此答案的范围。

  3. 命令中的业务逻辑

    命令不应包含业务逻辑。当您的命令包含太多代码(通常超过 5-20 行代码)时,这是一个很好的线索,您的命令可能执行的太多。

    命令实际上应该只连接对服务的多个调用并将数据分配给属性和/或引发事件/消息(与表示层相关)。不要与不应在命令中引发的域事件混淆)。它们类似于 MVC 中的“动作”(例如,在 ASP.NET MVC 中使用的框架)。

    命令通常应该是这样的

    var customer = new Customer { Name = this.CustomerName, Mail = this.CustomerMail };
    try {
        this.customerService.AddCustomer(customer);
        // Add it to Observable<Customer> list so the UI gets updated
        this.Customers.Add(customer);
        // the service should have populated the Id field of Customer when persisting it
        // so we notify all other ViewModels that a new customer has been added
        this.messageBus.Publish(new CustomerCreated() { CustomerId = customer.Id } );
    } catch (SomeSpecificException e) { // Handle the Exception } 
    

    this.Customers = this.customerRepository.GetAll();
    // Or this for async commands
    this.Customers = await this.customerRepository.GetAllAsync();
    
  4. 封装

    许多命令与 ViewModel 本身紧密耦合,需要访问 ViewModel 或 Model 的内部状态(Model 不应直接暴露给 View,这会将 Model 与 View 以及任何更改该模型会破坏您的视图和绑定)。

    如果不破坏封装,将这些 ICommands 移出 ViewModel 可能会很困难。

当然你也可以在一个类中实现多个命令

public class MyViewModelCommandHandler
{
    private readonly IMyRepository myRepository;

    public MyViewModelCommandHandler(/* pass dependencies here*/)
    {
        // assign and guard dependencies

        MyCommand = new RelayCommand(MyCommand, CanExecuteMyCommand);
        MyOtherCommand = new RelayCommand(MyOtherCommand, CanExecuteMyOtherCommand);
    }

    public ICommand MyCommand { get; protected set; } 
    public ICommand MyOtherCommand { get; protected set; } 

    private void MyCommand() 
    {
        // do something
    }

    private void CanExecuteMyCommand() 
    {
        // validate
    }

    private void MyOtherCommand() 
    {
        // do something else
    }

    private void CanExecuteMyOtherCommand() 
    {
        // validate
    }
}

在您的 ViewModel 中简单地分配这些命令

public class MyViewModel : ViewModelBase 
{
    public MyViewModel()
    {
        var commandHandler = new MyCommandHandler(this);
        OneCommand = commandHandler.MyCommand;
        OtherCommand = commandHandler.MyOtherCommand;
    }

    public ICommand OneCommand { get; private set; } 
    public ICommand OtherCommand { get; private set; } 
}

您还可以使用 IoC 容器将 MyCommandHandler 注入您的视图中,这需要稍微改造您的命令处理程序类,以按需创建 ICommand。然后你可以像使用它一样

public class MyViewModel : ViewModelBase 
{
    public MyViewModel(MyCommandHandler commandHandler)
    {
        OneCommand = commandHandler.CreateMyCommand(this);
        OtherCommand = commandHandler.CreateMyOtherCommand(this);
    }

    public ICommand OneCommand { get; private set; } 
    public ICommand OtherCommand { get; private set; } 
}

但这只会改变你的问题,但它不会解决第 1 点到第 5 点。所以我建议先尝试上面列表中的建议,如果您的命令仍然包含“太多代码行”,请尝试其他解决方案。

我不太喜欢它,因为它创建了不必要的抽象而收效甚微。

ViewModel 主要由表示逻辑组成并不少见,因为这是它们的目的,而表示逻辑通常在命令内部。除此之外,您只有属性和构造函数。除了检查值是否更改,然后分配和一个或多个 OnPropertyChanged 调用之外,属性不应该有任何其他内容。

因此,50-80% 的 ViewModel 是来自命令的代码。

【讨论】:

    【解决方案2】:

    检查您的视图模型是否可以划分为逻辑块并为每个块创建子视图模型。额外的好处是,当您想在其他地方以不同的方式显示相同的信息时,通常可以重复使用这些较小的视图模型。

    此外,我更喜欢有一个通用的 RelayCommand 定义,并且只在我的视图模型中创建命令而不指定不同的方法,这样我就可以将 Execute 和 CanExecute 作为 lambda 表达式保持在一起。

    如果无法创建不同的视图模型,您还可以将类的代码拆分为多个文件(部分类)以提高可维护性。

    【讨论】:

    • +1 建议创建子视图模型。但是创建部分类不是好的做法。请参考:stackoverflow.com/a/2477848/3500959
    • 我非常同意你的解决方案,这是分离大视图模型的常用方法。但是我们的架构提供了当前的解决方案。当我实现它时,我发现它破坏了封装。
    • 在下面查看我的答案。您还可以将所有服务注入到您的命令/命令处理程序中,这样您就不会再在命令中依赖它们了。您仍将受限于 ViewModels 公共属性(应包括所有可绑定属性,因为您无法绑定非公共属性)。这可能会迫使您将某些属性设置器公开(即ObservableCollection&lt;T&gt; 属性通常只有公共 getter 和私有或受保护设置器。这限制了破坏封装的可能性。尽管没有 IoC 容器,但它是 PITA
    【解决方案3】:

    您的问题的答案是Single Responsibility Principle。您的视图模型做得太多了。将功能与您的 vm 分开并将其放入不同的类中,并将这些类作为对您的命令的引用发送。你的情况

    public class TestCommand : CommandBase
    {
        private Logger logger;
    
        public TestCommand(Logger logger)
        {
            this.logger = logger;
        }
    
        public override bool CanExecute(object parameter)
        {
            return true;
        }
    
        public override void ExecuteCommand(object parameter)
        {
            logger.log(...);
        }
    }
    

    这里我将 Logger 对象发送到 Command 而不是视图模型。只要将它们保存在逻辑文件夹中,项目中也有很多命令文件只是一个好习惯。

    注意:在现实世界中,我们不仅仅记录命令执行。基本上我们做一些功能并记录相同的内容。我在这里使用记录器的唯一原因只是因为 OP 的快速理解。理想情况下,我们应该发送一个具有必须在命令执行时完成的功能的类。

    【讨论】:

    • 但是如果我需要其他属性,不仅仅是记录器。所有私有字段都应该是公共的。
    • 为了达到您的要求,您可以使用 RelayCommands,也可以使用 subviewmodels。老实说,我不知道这些是最好的解决方案。我总是使用 RelayCommands。我建议您使用上述代码和您的要求将此特定要求作为一个新问题提出。 (在您的情况下,它是 SelectedItem)。有人可能会给你一个更好的解决方案。
    【解决方案4】:

    使用 ICommand 作为消息模式

    此解决方案侧重于 关注点分离单一职责原则

    它允许您跳过 MVVM 中的 RelayCommand 模式。

    如果您使用 XAML,则可以引用具有单个命令类的命名空间。像这样:

     xmlns:cmd="clr-namespace:MyProject"
    

    然后可以定义全局或局部样式,如下所示。这使得所有按钮只使用一个命令,将按钮的文本作为参数传递。大多数按钮使用文本作为上下文,但标签也可以使用。

            <Style BasedOn="{StaticResource XDButton}" TargetType="{x:Type Button}">
                <Setter Property="Command" Value="{StaticResource ResourceKey=cmd}"/>
                <Setter Property="CommandParameter" Value="{Binding Content, RelativeSource={RelativeSource Self}}"/>
            </Style>
    

    您可以像这样为整个项目创建一个命令,注意“路由”是基于按钮文本的。 '优先命名约定优于配置'

       public class Commands : ICommand
        {
            private bool canExecute = true;
    
            public bool CanExecute(object parameter)
            {
                return canExecute;
            }
    
            public event EventHandler CanExecuteChanged;
    
            public void Execute(object parameter)
            {
                NotifyCanExecute(false);
                var information = parameter.ToString();
                try
                {
                    if (information == "Show Passed") Events.ShowAllPassedTests(this, new EventArgs());
                    if (information == "Show Failed") Events.ShowAllFailedTests(this, new EventArgs());
                    if (information == "Sort By elapsed Time") Events.SortByElapsedTime(this, new EventArgs());
                    if (information == "Sort By Run Data") Events.SortByRunData(this, new EventArgs());
                    if (information == "Sort By Title") Events.SortByTitle(this, new EventArgs());
                    if (information == "Generate HTML Report") Events.GenerateHTMLReport(this, new EventArgs());
                }
                catch (NullReferenceException nre) {
                    Trace.WriteLine("Test Runner Commands 320- An attempt to fire an event failed due to no subscribers");
                }
                NotifyCanExecute(true);
            }
    
            private void NotifyCanExecute(bool p)
            {
                canExecute = p;
                if (CanExecuteChanged != null) CanExecuteChanged(this, new EventArgs());
            }
        }
    

    像这样创建单个事件聚合类:

    public  class Events  
    {
        public static EventHandler ShowAllPassedTests;
        public static EventHandler ShowAllFailedTests;
        public static EventHandler ClearAllFilters;
        public static EventHandler SortByElapsedTime;
        public static EventHandler SortByRunData;
        public static EventHandler SortByTitle;
        public static EventHandler GenerateHTMLReport;
        public static EventHandler<CheckBox> ColumnViewChanged;
    }
    

    您可以像这样创建一个带有按钮的解耦 Navigator 用户控件。单击按钮时,它只调用传递 Button 上下文的 Command 类。

     <StackPanel Orientation="Vertical">
            <StackPanel.Resources>
                <Style BasedOn="{StaticResource XDButton}" TargetType="{x:Type Button}">
                    <Setter Property="Command" Value="{StaticResource ResourceKey=cmd}"/>
                    <Setter Property="CommandParameter" Value="{Binding Content, RelativeSource={RelativeSource Self}}"/>
                </Style>
            </StackPanel.Resources>
            <Button x:Name="XBTNShowPassed"  >Show Passed</Button>
            <Button x:Name="XBTNShowFailed"  >Show Failed</Button>
            <Button x:Name="XBTNShowAll"  >Show All</Button>
            <Button x:Name="XBTNSortByElapsedTime"  >Sort by Elapsed Time</Button>
            <Button x:Name="XBTNSortByRunData"  >Sort By Run Data</Button>
            <Button x:Name="XBTNSortByTitle"  >Sort By Title</Button>
            <Button x:Name="XBTNGenerateHTMLReport"  >Generate HTML Report</Button>
        </StackPanel>
    

    最后接收的 ViewModel 或其他类看起来像这样:

                Events.ColumnViewChanged += OnColumnViewChanged;
                Events.SortByTitle += OnSortByTitle;
                Events.SortByRunData += OnSortByRunData;
                Events.SortByElapsedTime += OnSortByElapsedTime;
                Events.GenerateHTMLReport += OnGenerateHTMLReport;
                Events.ShowAllFailedTests += OnShowAllFailedTests;
                Events.ShowAllPassedTests += OnShowAllPassedTests;
    
            }
    
            private void OnShowAllPassedTests(object sender, EventArgs e)
            {
                FilterCVS(tr => tr.DidTestPass);
            }
    
            private void OnShowAllFailedTests(object sender, EventArgs e)
            {
                FilterCVS(tr => tr.DidTestFail);
            }
    

    别忘了实现 Dispose

    当代码连接到 EventHandler 时,它就没有资格进行垃圾收集了。要解决此问题,请实现 Dispose 模式并断开事件处理程序...例如

    Events.OnColumnViewChanged -= OnColumnViewChanged;
    

    【讨论】:

    • 让您的应用程序泄漏内存和/或必须在每个视图模型中实现一次性模式以及在不需要它们时立即处理它们的系统的好方法。否则,您的事件也将在甚至不再引用的视图模型中执行(即由于导航问题,但尚未被 GC 收集)...
    • 只有在 ViewModel 被重复处理和创建时才需要关注内存泄漏。对于每个视图只创建一个视图模型的项目,这不是问题。但是是的,如果这是一个问题,那么 dispose 方法可以取消注册事件处理程序。或者可以简单地转到 onCompleted 自动处理订阅的观察者模式。
    • 在除了最简单的 MVVM 应用程序之外的任何应用程序中,您都有某种导航系统可以在视图之间来回切换,并且其中大多数会处理视图(在 View first 方法中,这会删除参考视图模型),因为没有理由将它们保存在内存中。不好的做法和反模式不会帮助人们写出好的代码
    猜你喜欢
    • 1970-01-01
    • 2011-04-09
    • 2012-06-24
    • 2012-10-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-11
    • 1970-01-01
    相关资源
    最近更新 更多