MVVM - Hiérarchies et navigation

Lors de la création d'applications MVVM, vous décomposez généralement des écrans d'informations complexes en un ensemble de vues parent et enfant, où les vues enfants sont contenues dans les vues parent dans des panneaux ou des contrôles de conteneur et forment elles-mêmes une hiérarchie d'utilisation.

  • Après avoir décomposé les vues complexes, cela ne signifie pas que chaque élément de contenu enfant que vous séparez dans son propre fichier XAML doit nécessairement être une vue MVVM.

  • Le morceau de contenu fournit simplement la structure pour rendre quelque chose à l'écran et ne prend en charge aucune entrée ou manipulation par l'utilisateur pour ce contenu.

  • Il peut ne pas avoir besoin d'un ViewModel distinct, mais il peut simplement s'agir d'un bloc XAML qui effectue le rendu en fonction des propriétés exposées par le ViewModel parent.

  • Enfin, si vous avez une hiérarchie de vues et de ViewModels, le ViewModel parent peut devenir un hub pour les communications afin que chaque ViewModel enfant puisse rester découplé des autres ViewModels enfants et de leur parent autant que possible.

Jetons un œil à un exemple dans lequel nous définirons une hiérarchie simple entre différentes vues. Créer un nouveau projet d'application WPFMVVMHierarchiesDemo

Step 1 - Ajoutez les trois dossiers (Model, ViewModel et Views) dans votre projet.

Step 2 - Ajoutez des classes Customer et Order dans le dossier Model, CustomerListView et OrderView dans le dossier Views, et CustomerListViewModel et OrderViewModel dans le dossier ViewModel, comme illustré dans l'image suivante.

Step 3- Ajoutez des blocs de texte à la fois dans CustomerListView et OrderView. Voici le fichier 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>

Voici le fichier 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>

Maintenant, nous avons besoin de quelque chose pour héberger ces vues, et un bon endroit pour cela dans notre MainWindow car c'est une application simple. Nous avons besoin d'un contrôle de conteneur que nous pouvons placer nos vues et les changer en mode navigation. Pour cela, nous devons ajouter ContentControl dans notre fichier MainWindow.xaml et nous utiliserons sa propriété content et la lierons à une référence ViewModel.

Définissez maintenant les modèles de données pour chaque vue dans un dictionnaire de ressources. Voici le fichier MainWindow.xaml. Notez comment chaque modèle de données mappe un type de données (le type ViewModel) à une vue correspondante.

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

Chaque fois que le modèle de vue actuel est défini sur une instance d'un CustomerListViewModel, il restituera un CustomerListView avec le ViewModel connecté. C'est un ViewModel de commande, il rendra OrderView et ainsi de suite.

Nous avons maintenant besoin d'un ViewModel qui a une propriété CurrentViewModel et une logique et une commande pour pouvoir changer la référence actuelle de ViewModel à l'intérieur de la propriété.

Créons un ViewModel pour cette MainWindow appelé MainWindowViewModel. Nous pouvons simplement créer une instance de notre ViewModel à partir de XAML et l'utiliser pour définir la propriété DataContext de la fenêtre. Pour cela, nous devons créer une classe de base pour encapsuler l'implémentation de INotifyPropertyChanged pour nos ViewModels.

L'idée principale derrière cette classe est d'encapsuler l'implémentation INotifyPropertyChanged et de fournir des méthodes d'assistance à la classe dérivée afin qu'elle puisse facilement déclencher les notifications appropriées. Voici l'implémentation de la classe 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 { }; 
   } 
}

Il est maintenant temps de commencer à changer de vue à l'aide de notre propriété CurrentViewModel. Nous avons juste besoin d'un moyen de piloter le réglage de cette propriété. Et nous allons faire en sorte que l'utilisateur final puisse commander d'accéder à la liste des clients ou à la vue des commandes. Ajoutez d'abord une nouvelle classe dans votre projet qui implémentera l'interface ICommand. Voici l'implémentation de l'interface 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 
   } 
}

Nous devons maintenant configurer une navigation de niveau supérieur vers ceux-ci vers ViewModels et la logique pour cette commutation doit appartenir à MainWindowViewModel. Pour cela, nous allons utiliser une méthode appelée sur la navigation qui prend une chaîne de destination et renvoie la propriété CurrentViewModel.

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

Pour la navigation de ces différentes vues, nous devons ajouter deux boutons dans notre fichier MainWindow.xaml. Voici l'implémentation complète du fichier 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>

Voici l'implémentation complète de 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; 
         } 
      } 
   } 
}

Dérivez tous vos ViewModels de la classe BindableBase. Lorsque le code ci-dessus est compilé et exécuté, vous verrez la sortie suivante.

Comme vous pouvez le voir, nous n'avons ajouté que deux boutons et un CurrentViewModel sur notre MainWindow. Si vous cliquez sur n'importe quel bouton, il accédera à cette vue particulière. Cliquons sur le bouton Clients et vous verrez que le CustomerListView est affiché.

Nous vous recommandons d'exécuter l'exemple ci-dessus étape par étape pour une meilleure compréhension.