2016-01-27 35 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