MVVM - Hierarchie i nawigacja

Podczas budowania aplikacji MVVM zazwyczaj rozkładasz złożone ekrany informacji na zestaw widoków nadrzędnych i podrzędnych, w których widoki podrzędne są zawarte w widokach nadrzędnych w panelach lub kontrolkach kontenerów i same tworzą hierarchię użycia.

  • Po zdekomponowaniu złożonych widoków nie oznacza to, że każdy element zawartości podrzędnej, który jest oddzielany do własnego pliku XAML, musi być widokiem MVVM.

  • Fragment zawartości po prostu zapewnia strukturę do renderowania czegoś na ekranie i nie obsługuje żadnych danych wejściowych ani manipulacji przez użytkownika dla tej zawartości.

  • Może nie potrzebować oddzielnego ViewModel, ale może to być po prostu fragment XAML, który jest renderowany na podstawie właściwości uwidocznionych przez ViewModel nadrzędnych.

  • Na koniec, jeśli masz hierarchię widoków i ViewModels, nadrzędny ViewModel może stać się centrum komunikacji, tak aby każdy podrzędny ViewModel mógł pozostać oddzielony od innych podrzędnych modeli ViewModel i ich nadrzędnych w miarę możliwości.

Spójrzmy na przykład, w którym zdefiniujemy prostą hierarchię między różnymi widokami. Utwórz nowy projekt aplikacji WPFMVVMHierarchiesDemo

Step 1 - Dodaj trzy foldery (Model, ViewModel i Views) do projektu.

Step 2 - Dodaj klasy Customer i Order w folderze Model, CustomerListView i OrderView w folderze Views oraz CustomerListViewModel i OrderViewModel w folderze ViewModel, jak pokazano na poniższej ilustracji.

Step 3- Dodaj bloki tekstowe w CustomerListView i OrderView. Oto plik 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>

Poniżej znajduje się plik 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>

Teraz potrzebujemy czegoś do hostowania tych widoków i dobrego miejsca na to w naszym MainWindow, ponieważ jest to prosta aplikacja. Potrzebujemy kontrolki kontenera, abyśmy mogli umieszczać nasze widoki i przełączać je w sposób nawigacyjny. W tym celu musimy dodać ContentControl w naszym pliku MainWindow.xaml i będziemy używać jego właściwości content i powiązać ją z referencją ViewModel.

Teraz zdefiniuj szablony danych dla każdego widoku w słowniku zasobów. Poniżej znajduje się plik MainWindow.xaml. Zwróć uwagę, jak każdy szablon danych mapuje typ danych (typ ViewModel) do odpowiedniego widoku.

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

Za każdym razem, gdy bieżący model widoku jest ustawiony na wystąpienie CustomerListViewModel, wyrenderuje CustomerListView z podłączonym ViewModel. Jest to ViewModel zamówienia, wyrenderuje OrderView i tak dalej.

Teraz potrzebujemy ViewModel, który ma właściwość CurrentViewModel oraz pewną logikę i polecenia, aby móc przełączać bieżące odwołanie do ViewModel wewnątrz właściwości.

Utwórzmy ViewModel dla tego MainWindow o nazwie MainWindowViewModel. Możemy po prostu utworzyć wystąpienie naszego ViewModel z XAML i użyć go do ustawienia właściwości DataContext okna. W tym celu musimy utworzyć klasę bazową, aby hermetyzować implementację INotifyPropertyChanged dla naszych ViewModels.

Główną ideą tej klasy jest hermetyzacja implementacji INotifyPropertyChanged i udostępnienie metod pomocniczych do klasy pochodnej, tak aby mogły łatwo wyzwalać odpowiednie powiadomienia. Poniżej przedstawiono implementację klasy 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 { }; 
   } 
}

Teraz nadszedł czas, aby faktycznie rozpocząć przełączanie widoków za pomocą naszej właściwości CurrentViewModel. Potrzebujemy tylko sposobu, aby sterować ustawieniem tej właściwości. I zrobimy to tak, aby użytkownik końcowy mógł wydać polecenie przejścia do listy klientów lub widoku zamówienia. Najpierw dodaj nową klasę do projektu, która zaimplementuje interfejs ICommand. Poniżej przedstawiono implementację interfejsu 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 
   } 
}

Musimy teraz skonfigurować nawigację najwyższego poziomu do tych modeli ViewModels, a logika dla tego przełączania powinna znajdować się w MainWindowViewModel. W tym celu użyjemy metody wywoływanej on navigate, która przyjmuje miejsce docelowe w postaci ciągu znaków i zwraca właściwość CurrentViewModel.

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

Aby poruszać się po tych różnych widokach, musimy dodać dwa przyciski w naszym pliku MainWindow.xaml. Poniżej znajduje się pełna implementacja pliku 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>

Poniżej znajduje się pełna implementacja 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; 
         } 
      } 
   } 
}

Wyprowadź wszystkie ViewModels z klasy BindableBase. Gdy powyższy kod zostanie skompilowany i wykonany, zobaczysz następujące dane wyjściowe.

Jak widać, dodaliśmy tylko dwa przyciski i CurrentViewModel w naszym MainWindow. Kliknięcie dowolnego przycisku spowoduje przejście do tego konkretnego widoku. Kliknijmy przycisk Klienci, a zobaczysz, że wyświetla się CustomerListView.

Zalecamy wykonanie powyższego przykładu krok po kroku w celu lepszego zrozumienia.