2016-01-27 20 views
1

我的viewmodel包含很多命令,它使我的viewmodel非常大。我想把我的命令从视图模型中分离出来。目前,我soluton是创建一个类像下面的每个命令,如何从大视图模型中分离命令

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(...); 
     ... 
    } 
} 

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

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

+0

如果您唯一关心的是可视性,您可以将它们抽象为部分类。但是,如果你需要很多命令,这可能暗示你的ViewModel尝试做太多。请记住,上面的解决方案可能会破坏你的viewmodels封装,因为上述命令只能访问公共属性和方法 – Tseng

+0

你能提供一些更具体的哪些类型的命令吗?如果您尝试在ViewModel中投入太多关注点,或者您需要更好的抽象 – Tseng

回答

5

TL; DR:

的ViewModels是其主要表现在命令中表示逻辑,所以它的不寻常的是,命令需要大量的ViewModel代码。不要试图将ViewModel作为普通的数据持有者(如使用ViewModel时常见的ASP.NET MVC)与INotifyPropertyChanged一起使用。

龙版

上缺乏更多的细节很难给你具体的建议,但这里有一些一般准则。您可以更新您的问题,提供更多关于您正在使用的命令的详细信息,并尝试更新问题。

  1. 表示逻辑

    的ViewModels的主要关注点是呈现。 ViewModels中没有业务逻辑的地方。

    业务逻辑必须提取到您的业务/域模型(如果您遵循丰富的域模型)或服务(在贫血域模型中)。在丰富的域模型中,您的服务层通常非常薄,并且主要用于协调多个模型之间的操作。 (如果点击按钮A,禁用按钮B,C和D或隐藏GroupBoxA或“如果数据丢失,则禁用按钮A”(CanExecuteICommand))如果您的ViewModel /命令执行的任何类型的逻辑与演示无关)它可能做得太多

  2. 关注分离:。

    它可能是您的视图模型试图做多,它的目的是做你的榜样的记录是这样的提示记录不是ViewModel关心的问题。

    视图模型是关于介绍和表示逻辑和日志记录是一个应用程序的关注(因为它不属于域/业务逻辑)。

    通常一个ViewModel可以被分成两个或更多的ViewModels(即A视图模型管理的顾客的列表,并且允许编辑所选择的顾客,通常可以分成2或3的ViewModels:CustomersViewModel(显示列表),CustomerDetailViewModelCustomerViewModel(详细给客户)和CustomerEditViewModel(编辑有问题的客户)

    关注像记录和缓存应即使用Decorator模式来完成。这需要你的服务和/或存储库正确使用和实现接口,那么你就可以创建缓存或记录装饰而非注入你的服务的原始实例,需要实现装饰。

    依赖注入(DI)和控制反转(IOC)容器真正帮助您与此有关。不得不手动将它们连接起来(又名穷人DI)是令人痛苦的。具体的例子超出了这个答案的范围。

  3. 业务逻辑命令

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

    命令实际上应该只连接多个服务调用,并将数据分配给属性和/或上升事件/消息(与表示层相关)。不要与域事件混淆,不应将其与内部引发命令)。它们与MVC中的“Actions”类似(例如,像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或Model的模型(模型不应该直接暴露给V这将模型耦合到视图,并且模型中的任何更改会破坏视图和绑定)。

    移动这些ICommands出的ViewModels可能难以在不破坏封装。

当然,你也可以实现在一个类

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; } 
} 

您也可以将您的MyCommandHandler到您的视图中使用注入IoC容器,这需要重塑你的命令处理程序类了一下,根据需要创建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。虽然。所以我建议先尝试一下上面的列表中的建议,如果你的命令仍然包含“太多的代码行”,请尝试另一种解决方案。

我不喜欢它太多,因为它会造成不必要的抽象,因为收益不大。

ViewModels主要由表现逻辑组成,因为这是他们的目的,表现逻辑通常是内部命令。除此之外,你只需要有属性和构造函数。除了检查值是否更改之外,属性不应具有任何其他值,然后分配一个或多个OnPropertyChanged调用。

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

+1

非常好的帖子。 –

4

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

另外我更喜欢有一个通用的RelayCommand定义,只是在我的viewmodel中创建命令而不指定不同的方法,因此我可以将Execute和CanExecute一起保存为lambda表达式。

如果不可能创建不同的视图模型,您也可以将您的类的代码拆分为多个文件(部分类)以增加可维护性。

+2

+1以建议创建子视图模型,则可以更好地了解概述。但是创建部分类并不是好习惯。请参考:http://stackoverflow.com/a/2477848/3500959 – Sivasubramanian

+0

我真的同意你的解决方案,这是分离大视图模型的常用方法。但是我们的架构提供了当前的解决方当我执行它时,我发现它打破了封锁。 – Allen4Tech

+0

请参阅下面的答案。您也可以将所有的服务注入到命令/命令处理程序中,因此您不会再依赖于它们。您仍然只能使用ViewModels的公共属性(应包含所有可绑定属性,因为您无法绑定非公共属性)。这可能会迫使你公开使用certian属性设置器(例如'ObservableCollection '属性通常只有公共getter和私有或受保护的setter。这限制了破坏封装的可能性。它是PITA,尽管没有IoC容器 – Tseng

3

对您的问题的回答是Single Responsibility Principle。你的viewmodel做得太多了。将功能从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对象到命令而不是视图模型。在项目中使用很多命令文件也是一种很好的做法,只要将它们保存在一个逻辑文件夹中即可。

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

+0

但是如果我需要其他属性,不只是记录器,所有的私有字段都应该是公开的 – Allen4Tech

+0

例如,我想在命令中使用SelectedItem – Allen4Tech

+0

要达到您的要求,您可以使用RelayCommands,或者您可以使用子视图模型,老实说,我不知道这些都是最好的解决方案,我总是使用RelayCommands,我建议你用上面的代码和你的要求来提出这个特殊的要求(在你的情况下它是SelectedItem)。有人可能会给你一个更好的解决方案 – Sivasubramanian

-2

使用的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; 
} 

您可以创建一个有按钮这样的分离型导航仪的用户控制。当按钮被点击时,它只是调用通过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> 

最后接收视图模型或其他类看起来是这样的:

  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

当代码挂接到事件处理程序变得没有资格垃圾采集。为了解决这个问题,实现Dispose模式和断开事件处理器...如

Events.OnColumnViewChanged -= OnColumnViewChanged; 
+0

好让你的应用程序泄漏内存和/或不得不在每个应用程序中实现一次性模式的方式以及每一个视图模型,以及一个系统,将尽快处置这些,只要他们不需要。否则,您的事件也将在视图模型中执行,甚至不再被引用(即由于导航问题,但尚未被GC收集)... – Tseng

+0

只有在ViewModel被丢弃和创建时才会担心内存泄漏反复。对于每个视图只能创建一个视图模型的项目,这不是问题。但是,如果这是一个问题,那么dispose方法可以注销事件处理程序。或者可以简单地移动到onCompleted自动配置订阅的Observer模式。 –

+0

在任何最简单的MVVM应用程序中,都有一些导航系统可以在不同视图之间来回切换,并且大多数视图处理视图(并且在视图第一种方法中删除了对视图模型的引用),因为没有理由让他们记忆。不好的做法和反模式不会帮助人们编写好的代码 – Tseng