MVVM - Jerarquías y navegación

Al crear aplicaciones MVVM, normalmente descompone pantallas complejas de información en un conjunto de vistas principales y secundarias, donde las vistas secundarias se encuentran dentro de las vistas principales en paneles o controles de contenedor, y forman una jerarquía de uso.

  • Después de descomponer las vistas complejas, no significa que todos y cada uno de los elementos del contenido secundario que separe en su propio archivo XAML necesariamente deben ser una vista MVVM.

  • El fragmento de contenido solo proporciona la estructura para representar algo en la pantalla y no admite ninguna entrada o manipulación por parte del usuario para ese contenido.

  • Puede que no necesite un ViewModel separado, pero podría ser solo un fragmento XAML que se procesa según las propiedades expuestas por el ViewModel principal.

  • Finalmente, si tiene una jerarquía de Vistas y ViewModels, el ViewModel principal puede convertirse en un centro de comunicaciones para que cada ViewModel secundario pueda permanecer desacoplado de los otros ViewModels secundarios y de su principal tanto como sea posible.

Echemos un vistazo a un ejemplo en el que definiremos una jerarquía simple entre diferentes vistas. Crear un nuevo proyecto de aplicación WPFMVVMHierarchiesDemo

Step 1 - Agregue las tres carpetas (Modelo, ViewModel y Vistas) a su proyecto.

Step 2 - Agregue las clases Customer y Order en la carpeta Model, CustomerListView y OrderView en la carpeta Views, y CustomerListViewModel y OrderViewModel en la carpeta ViewModel como se muestra en la siguiente imagen.

Step 3- Agregue bloques de texto tanto en CustomerListView como en OrderView. Aquí está el archivo 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>

A continuación se muestra el archivo 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>

Ahora necesitamos algo para alojar estas vistas y un buen lugar para eso en nuestra ventana principal porque es una aplicación simple. Necesitamos un control de contenedor en el que podamos colocar nuestras vistas y cambiarlas en forma de navegación. Para este propósito, necesitamos agregar ContentControl en nuestro archivo MainWindow.xaml y usaremos su propiedad de contenido y la vincularemos a una referencia de ViewModel.

Ahora defina las plantillas de datos para cada vista en un diccionario de recursos. A continuación se muestra el archivo MainWindow.xaml. Observe cómo cada plantilla de datos asigna un tipo de datos (el tipo ViewModel) a una Vista correspondiente.

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

Cada vez que el modelo de vista actual se establece en una instancia de un CustomerListViewModel, representará un CustomerListView con el ViewModel conectado. Es una orden ViewModel, renderizará OrderView y así sucesivamente.

Ahora necesitamos un ViewModel que tenga una propiedad CurrentViewModel y algo de lógica y comandos para poder cambiar la referencia actual de ViewModel dentro de la propiedad.

Creemos un ViewModel para esta MainWindow llamado MainWindowViewModel. Podemos simplemente crear una instancia de nuestro ViewModel desde XAML y usarla para establecer la propiedad DataContext de la ventana. Para esto, necesitamos crear una clase base para encapsular la implementación de INotifyPropertyChanged para nuestros ViewModels.

La idea principal detrás de esta clase es encapsular la implementación INotifyPropertyChanged y proporcionar métodos auxiliares a la clase derivada para que puedan activar fácilmente las notificaciones apropiadas. A continuación se muestra la implementación de la clase 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 { }; 
   } 
}

Ahora es el momento de empezar a cambiar de vista utilizando nuestra propiedad CurrentViewModel. Solo necesitamos alguna forma de controlar la configuración de esta propiedad. Y lo haremos de modo que el usuario final pueda ordenar ir a la lista de clientes oa la vista de pedidos. Primero agregue una nueva clase en su proyecto que implementará la interfaz ICommand. A continuación se muestra la implementación de la interfaz 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 
   } 
}

Ahora necesitamos configurar una navegación de nivel superior a estos en ViewModels y la lógica para ese cambio debe pertenecer dentro de MainWindowViewModel. Para esto, usaremos un método llamado navegar que toma un destino de cadena y devuelve la propiedad CurrentViewModel.

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

Para navegar por estas diferentes Vistas, necesitamos agregar dos botones en nuestro archivo MainWindow.xaml. A continuación se muestra la implementación completa del archivo 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>

A continuación se muestra la implementación completa 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; 
         } 
      } 
   } 
}

Derive todos sus ViewModels de la clase BindableBase. Cuando se compile y ejecute el código anterior, verá el siguiente resultado.

Como puede ver, hemos agregado solo dos botones y un CurrentViewModel en nuestra MainWindow. Si hace clic en cualquier botón, navegará a esa Vista en particular. Hagamos clic en el botón Clientes y verá que se muestra CustomerListView.

Le recomendamos que ejecute el ejemplo anterior paso a paso para una mejor comprensión.