MVVM – 層次結構與導航



構建 MVVM 應用程式時,通常會將複雜的資訊螢幕分解成一組父檢視和子檢視,其中子檢視包含在面板或容器控制元件中的父檢視內,並形成自身使用的層次結構。

  • 分解複雜的檢視後,並不意味著您分離到其自身 XAML 檔案中的每個子內容部分都需要成為 MVVM 檢視。

  • 內容塊僅提供渲染內容到螢幕的結構,並且不支援使用者對該內容的任何輸入或操作。

  • 它可能不需要單獨的 ViewModel,而可能只是一個基於父 ViewModel 公開屬性進行渲染的 XAML 塊。

  • 最後,如果您具有檢視和 ViewModel 的層次結構,則父 ViewModel 可以成為通訊中心,以便每個子 ViewModel 儘可能地與其他子 ViewModel 及其父級分離。

MVVM Hierarchies and Navigation

讓我們來看一個定義不同檢視之間簡單層次結構的示例。建立一個新的 WPF 應用程式專案 MVVMHierarchiesDemo

WPF Application

步驟 1 − 將三個資料夾(Model、ViewModel 和 Views)新增到您的專案中。

WPF Application Step1

步驟 2 − 在 Model 資料夾中新增 Customer 和 Order 類,在 Views 資料夾中新增 CustomerListView 和 OrderView,在 ViewModel 資料夾中新增 CustomerListViewModel 和 OrderViewModel,如下面的影像所示。

WPF Application Step2

步驟 3 − 在 CustomerListView 和 OrderView 中新增文字塊。以下是 CustomerListView.xaml 檔案。

<UserControl x:Class="MVVMHierarchiesDemo.Views.CustomerListView" 
   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" 
   xmlns:local = "clr-namespace:MVVMHierarchiesDemo.Views" 
   mc:Ignorable = "d" 
   d:DesignHeight = "300" d:DesignWidth = "300">
	
   <Grid> 
      <TextBlock Text = "Customer List View"/> 
   </Grid> 
	
</UserControl>

以下是 OrderView.xaml 檔案。

<UserControl x:Class = "MVVMHierarchiesDemo.Views.OrderView" 
   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" 
   xmlns:local = "clr-namespace:MVVMHierarchiesDemo.Views" mc:Ignorable = "d" 
   d:DesignHeight = "300" d:DesignWidth = "300">
	
   <Grid> 
      <TextBlock Text = "Order View"/> 
   </Grid> 
	
</UserControl>

現在我們需要一些東西來託管這些檢視,在我們的 MainWindow 中這是一個好地方,因為它是一個簡單的應用程式。我們需要一個容器控制元件,我們可以放置我們的檢視並在導航方式中切換它們。為此,我們需要在我們的 MainWindow.xaml 檔案中新增 ContentControl,我們將使用它的 content 屬性並將其繫結到 ViewModel 引用。

現在在資源字典中為每個檢視定義資料模板。以下是 MainWindow.xaml 檔案。請注意每個資料模板如何將資料型別(ViewModel 型別)對映到相應的 View。

<Window x:Class = "MVVMHierarchiesDemo.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" 
   xmlns:local = "clr-namespace:MVVMHierarchiesDemo" 
   xmlns:views = "clr-namespace:MVVMHierarchiesDemo.Views" 
   xmlns:viewModels = "clr-namespace:MVVMHierarchiesDemo.ViewModel" 
   mc:Ignorable = "d" 
   Title = "MainWindow" Height = "350" Width = "525"> 
   
   <Window.DataContext> 
      <local:MainWindowViewModel/> 
   </Window.DataContext>
	
   <Window.Resources> 
      <DataTemplate DataType = "{x:Type viewModels:CustomerListViewModel}">
         <views:CustomerListView/> 
      </DataTemplate>
		
      <DataTemplate DataType = "{x:Type viewModels:OrderViewModel}"> 
         <views:OrderView/> 
      </DataTemplate> 
   </Window.Resources>
	
   <Grid> 
      <ContentControl Content = "{Binding CurrentView}"/> 
   </Grid> 
	
</Window>

每當當前檢視模型設定為 CustomerListViewModel 的例項時,它將呈現一個 CustomerListView,其中 ViewModel 已連線。它是一個訂單 ViewModel,它將呈現 OrderView 等等。

我們現在需要一個 ViewModel,它具有 CurrentViewModel 屬性以及一些邏輯和命令,以便能夠切換屬性內部的 ViewModel 的當前引用。

讓我們為此 MainWindow 建立一個名為 MainWindowViewModel 的 ViewModel。我們可以從 XAML 建立 ViewModel 的例項並使用它來設定視窗的 DataContext 屬性。為此,我們需要建立一個基類來封裝我們 ViewModel 的 INotifyPropertyChanged 實現。

此類的主要思想是封裝 INotifyPropertyChanged 實現並向派生類提供幫助器方法,以便它們可以輕鬆觸發適當的通知。以下是 BindableBase 類的實現。

using System; 
using System.Collections.Generic; 
using System.ComponentModel; 
using System.Linq; 
using System.Runtime.CompilerServices; 
using System.Text; 
using System.Threading.Tasks;

namespace MVVMHierarchiesDemo { 

   class BindableBase : INotifyPropertyChanged { 
	
      protected virtual void SetProperty<T>(ref T member, T val,
         [CallerMemberName] string propertyName = null) { 
            if (object.Equals(member, val)) return;
				
            member = val;
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); 
      }
			
      protected virtual void OnPropertyChanged(string propertyName) { 
         PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); 
      } 
		
      public event PropertyChangedEventHandler PropertyChanged = delegate { }; 
   } 
}

現在是時候使用我們的 CurrentViewModel 屬性開始進行一些檢視切換了。我們只需要某種方式來驅動此屬性的設定。我們將使其成為終端使用者可以命令轉到客戶列表或訂單檢視。首先在您的專案中新增一個新類,該類將實現 ICommand 介面。以下是 ICommand 介面的實現。

using System; 
using System.Windows.Input;

namespace MVVMHierarchiesDemo { 

   public class MyICommand<T> : ICommand { 
	
      Action<T> _TargetExecuteMethod; 
      Func<T, bool> _TargetCanExecuteMethod;
		
      public MyICommand(Action<T> executeMethod) {
         _TargetExecuteMethod = executeMethod; 
      }
		
      public MyICommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod) {
         _TargetExecuteMethod = executeMethod;
         _TargetCanExecuteMethod = canExecuteMethod; 
      }

      public void RaiseCanExecuteChanged() {
         CanExecuteChanged(this, EventArgs.Empty); 
      } 
		
      #region ICommand Members

      bool ICommand.CanExecute(object parameter) { 
		
         if (_TargetCanExecuteMethod != null) { 
            T tparm = (T)parameter; 
            return _TargetCanExecuteMethod(tparm); 
         } 
			
         if (_TargetExecuteMethod != null) { 
            return true; 
         } 
			
         return false; 
      }
		
      // Beware - should use weak references if command instance lifetime is
         longer than lifetime of UI objects that get hooked up to command 
			
      // Prism commands solve this in their implementation 

      public event EventHandler CanExecuteChanged = delegate { };
	
      void ICommand.Execute(object parameter) { 
         if (_TargetExecuteMethod != null) {
            _TargetExecuteMethod((T)parameter); 
         } 
      } 
		
      #endregion 
   } 
}

現在我們需要為這些 ViewModel 設定一些頂級導航,並且切換的邏輯應該屬於 MainWindowViewModel 內部。為此,我們將使用一個名為 on navigate 的方法,該方法接受一個字串目標並返回 CurrentViewModel 屬性。

private void OnNav(string destination) {
 
   switch (destination) { 
      case "orders": 
         CurrentViewModel = orderViewModelModel; 
      break; 
      case "customers": 
      default: 
         CurrentViewModel = custListViewModel; 
      break; 
   } 
}

為了導航這些不同的檢視,我們需要在我們的 MainWindow.xaml 檔案中新增兩個按鈕。以下是完整的 XAML 檔案實現。

<Window x:Class = "MVVMHierarchiesDemo.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" 
   xmlns:local = "clr-namespace:MVVMHierarchiesDemo" 
   xmlns:views = "clr-namespace:MVVMHierarchiesDemo.Views" 
   xmlns:viewModels = "clr-namespace:MVVMHierarchiesDemo.ViewModel" 
   mc:Ignorable = "d" 
   Title = "MainWindow" Height = "350" Width = "525">

   <Window.DataContext> 
      <local:MainWindowViewModel/> 
   </Window.DataContext>
	
   <Window.Resources> 
      <DataTemplate DataType = "{x:Type viewModels:CustomerListViewModel}">
         <views:CustomerListView/> 
      </DataTemplate> 
		
      <DataTemplate DataType = "{x:Type viewModels:OrderViewModel}">
         <views:OrderView/> 
      </DataTemplate> 
   </Window.Resources>
	
   <Grid>
      <Grid.RowDefinitions> 
         <RowDefinition Height = "Auto" /> 
         <RowDefinition Height = "*" /> 
      </Grid.RowDefinitions> 
	
      <Grid x:Name = "NavBar"> 
         <Grid.ColumnDefinitions> 
            <ColumnDefinition Width = "*" /> 
            <ColumnDefinition Width = "*" /> 
            <ColumnDefinition Width = "*" /> 
         </Grid.ColumnDefinitions> 
	
         <Button Content = "Customers" 
            Command = "{Binding NavCommand}"
            CommandParameter = "customers" 
            Grid.Column = "0" />
				
         <Button Content = "Order" 
            Command = "{Binding NavCommand}" 
            CommandParameter = "orders" 
            Grid.Column = "2" />
      </Grid> 
	
      <Grid x:Name = "MainContent" Grid.Row = "1"> 
         <ContentControl Content = "{Binding CurrentViewModel}" /> 
      </Grid> 
		
   </Grid> 
	
</Window>

以下是完整的 MainWindowViewModel 實現。

using MVVMHierarchiesDemo.ViewModel; 
using MVVMHierarchiesDemo.Views; 

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Threading.Tasks;

namespace MVVMHierarchiesDemo {
 
   class MainWindowViewModel : BindableBase {
	
      public MainWindowViewModel() { 
         NavCommand = new MyICommand<string>(OnNav); 
      } 
		
      private CustomerListViewModel custListViewModel = new CustomerListViewModel(); 
		
      private OrderViewModel orderViewModelModel = new OrderViewModel();
		
      private BindableBase _CurrentViewModel; 
		
      public BindableBase CurrentViewModel { 
         get {return _CurrentViewModel;} 
         set {SetProperty(ref _CurrentViewModel, value);} 
      }
		
      public MyICommand<string> NavCommand { get; private set; }

      private void OnNav(string destination) {
		
         switch (destination) { 
            case "orders": 
               CurrentViewModel = orderViewModelModel; 
               break; 
            case "customers": 
            default: 
               CurrentViewModel = custListViewModel; 
               break; 
         } 
      } 
   } 
}

從 BindableBase 類派生所有 ViewModel。當上述程式碼編譯並執行時,您將看到以下輸出。

Hierarchies and Navigation MainWindow1

如您所見,我們只在 MainWindow 上添加了兩個按鈕和一個 CurrentViewModel。如果單擊任何按鈕,它將導航到該特定檢視。讓我們單擊“客戶”按鈕,您將看到 CustomerListView 顯示出來。

Hierarchies and Navigation MainWindow2

我們建議您逐步執行上述示例以更好地理解。

廣告