MVVM - Cấu trúc phân cấp & Điều hướng

Khi xây dựng ứng dụng MVVM, bạn thường phân tách các màn hình thông tin phức tạp thành một tập hợp các chế độ xem mẹ và con, trong đó các chế độ xem con được chứa trong các chế độ xem chính trong các bảng điều khiển hoặc vùng chứa và tạo thành một hệ thống phân cấp sử dụng.

  • Sau khi phân tách các Chế độ xem phức tạp, điều đó không có nghĩa là mỗi và mọi phần nội dung con mà bạn tách thành tệp XAML của riêng nó nhất thiết phải là chế độ xem MVVM.

  • Phần nội dung chỉ cung cấp cấu trúc để hiển thị nội dung nào đó lên màn hình và không hỗ trợ bất kỳ đầu vào hoặc thao tác nào của người dùng đối với nội dung đó.

  • Nó có thể không cần một ViewModel riêng biệt, nhưng nó có thể chỉ là một đoạn XAML hiển thị dựa trên các thuộc tính được hiển thị bởi ViewModel cha mẹ.

  • Cuối cùng, nếu bạn có hệ thống phân cấp Chế độ xem và ViewModel, thì ViewModel mẹ có thể trở thành trung tâm giao tiếp để mỗi ViewModel con có thể tách rời khỏi ViewModels con khác và khỏi ViewModel mẹ nhiều nhất có thể.

Hãy xem một ví dụ trong đó chúng ta sẽ xác định một hệ thống phân cấp đơn giản giữa các khung nhìn khác nhau. Tạo một dự án Ứng dụng WPF mớiMVVMHierarchiesDemo

Step 1 - Thêm ba thư mục (Model, ViewModel và Views) vào dự án của bạn.

Step 2 - Thêm các lớp Khách hàng và Đơn hàng trong thư mục Model, CustomerListView và OrderView trong thư mục Views, và CustomerListViewModel và OrderViewModel trong thư mục ViewModel như trong hình sau.

Step 3- Thêm các khối văn bản trong cả CustomerListView và OrderView. Đây là tệp 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>

Sau đây là tệp 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>

Bây giờ chúng tôi cần một cái gì đó để lưu trữ các chế độ xem này và một nơi tốt cho việc đó trong MainWindow của chúng tôi vì nó là một ứng dụng đơn giản. Chúng tôi cần một điều khiển vùng chứa mà chúng tôi có thể đặt các chế độ xem của mình và chuyển đổi chúng theo cách điều hướng. Vì mục đích này, chúng tôi cần thêm ContentControl trong tệp MainWindow.xaml của mình và chúng tôi sẽ sử dụng thuộc tính nội dung của nó và liên kết thuộc tính đó với tham chiếu ViewModel.

Bây giờ xác định các mẫu dữ liệu cho mỗi dạng xem trong từ điển tài nguyên. Sau đây là tệp MainWindow.xaml. Lưu ý cách mỗi mẫu dữ liệu ánh xạ một kiểu dữ liệu (kiểu ViewModel) đến một Dạng xem tương ứng.

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

Bất cứ khi nào mô hình chế độ xem hiện tại được đặt thành một phiên bản của CustomerListViewModel, nó sẽ hiển thị một CustomerListView với ViewModel được kết nối. Đó là một ViewModel đặt hàng, nó sẽ hiển thị OrderView, v.v.

Bây giờ chúng ta cần một ViewModel có thuộc tính CurrentViewModel và một số logic và lệnh để có thể chuyển tham chiếu hiện tại của ViewModel bên trong thuộc tính.

Hãy tạo một ViewModel cho MainWindow này được gọi là MainWindowViewModel. Chúng ta chỉ có thể tạo một phiên bản ViewModel của mình từ XAML và sử dụng nó để đặt thuộc tính DataContext của cửa sổ. Đối với điều này, chúng ta cần tạo một lớp cơ sở để đóng gói việc triển khai INotifyPropertyChanged cho các ViewModels của chúng ta.

Ý tưởng chính đằng sau lớp này là đóng gói việc triển khai INotifyPropertyChanged và cung cấp các phương thức trợ giúp cho lớp dẫn xuất để chúng có thể dễ dàng kích hoạt các thông báo thích hợp. Sau đây là việc triển khai lớp 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 { }; 
   } 
}

Bây giờ đã đến lúc thực sự bắt đầu thực hiện một số chuyển đổi chế độ xem bằng thuộc tính CurrentViewModel của chúng tôi. Chúng tôi chỉ cần một số cách để thúc đẩy thiết lập của thuộc tính này. Và chúng tôi sẽ làm nó để người dùng cuối có thể ra lệnh truy cập danh sách khách hàng hoặc xem đơn đặt hàng. Đầu tiên hãy thêm một lớp mới vào dự án của bạn, lớp này sẽ triển khai giao diện ICommand. Sau đây là việc thực hiện giao diện 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 
   } 
}

Bây giờ chúng ta cần thiết lập một số điều hướng cấp cao nhất đến những thứ này tới ViewModels và logic cho việc chuyển đổi đó sẽ thuộc về MainWindowViewModel. Đối với điều này, chúng tôi sẽ sử dụng một phương thức được gọi là điều hướng lấy đích đến là chuỗi và trả về thuộc tính CurrentViewModel.

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

Để điều hướng các Chế độ xem khác nhau này, chúng ta cần thêm hai nút trong tệp MainWindow.xaml của mình. Sau đây là quá trình triển khai tệp XAML hoàn chỉnh.

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

Sau đây là cách triển khai MainWindowViewModel hoàn chỉnh.

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

Lấy ra tất cả các ViewModels của bạn từ lớp BindableBase. Khi đoạn mã trên được biên dịch và thực thi, bạn sẽ thấy kết quả sau.

Như bạn có thể thấy, chúng tôi chỉ thêm hai nút và một CurrentViewModel trên MainWindow của chúng tôi. Nếu bạn nhấp vào bất kỳ nút nào thì nó sẽ điều hướng đến Chế độ xem cụ thể đó. Hãy nhấp vào nút Khách hàng và bạn sẽ thấy rằng CustomerListView được hiển thị.

Chúng tôi khuyên bạn nên thực hiện ví dụ trên theo cách từng bước để hiểu rõ hơn.