MVVM - Comunicación View / ViewModel

En este capítulo, aprenderemos cómo agregar interactividad a las aplicaciones MVVM y cómo llamar a la lógica de forma limpia. También verá que todo esto se hace manteniendo el acoplamiento suelto y la buena estructura que es el corazón del patrón MVVM. Para comprender todo esto, primero aprendamos sobre los comandos.

Comunicación View / ViewModel a través de comandos

El patrón de comando ha sido bien documentado y utiliza con frecuencia el patrón de diseño durante un par de décadas. En este patrón hay dos actores principales, el invocador y el receptor.

Invocador

  • El invocador es un fragmento de código que puede ejecutar alguna lógica imperativa.

  • Normalmente, es un elemento de la interfaz de usuario con el que el usuario interactúa, en el contexto de un marco de interfaz de usuario.

  • Podría ser simplemente otro fragmento de código lógico en otro lugar de la aplicación.

Receptor

  • El receptor es la lógica que debe ejecutarse cuando el invocador dispara.

  • En el contexto de MVVM, el receptor suele ser un método en su ViewModel que debe llamarse.

Entre estos dos, tiene una capa de obstrucción, lo que implica que el invocador y el receptor no tienen que conocerse explícitamente. Esto generalmente se representa como una abstracción de interfaz expuesta al invocador y una implementación concreta de esa interfaz es capaz de llamar al receptor.

Echemos un vistazo a un ejemplo simple en el que aprenderá los comandos y cómo usarlos para comunicarse entre View y ViewModel. En este capítulo, continuaremos con el mismo ejemplo del capítulo anterior.

En el archivo StudentView.xaml, tenemos un ListBox que conecta los datos del estudiante desde un ViewModel. Ahora agreguemos un botón para eliminar un alumno del ListBox.

Lo importante es que trabajar con comandos en el botón es muy fácil porque tienen una propiedad de comando para conectarse a un ICommand.

Entonces, podemos exponer una propiedad en nuestro ViewModel que tiene un ICommand y se une a él desde la propiedad de comando del botón como se muestra en el siguiente código.

<Button Content = "Delete" 
   Command = "{Binding DeleteCommand}" 
   HorizontalAlignment = "Left" 
   VerticalAlignment = "Top" 
   Width = "75" />

Agreguemos 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 MVVMDemo { 

   public class MyICommand : ICommand { 
      Action _TargetExecuteMethod; 
      Func<bool> _TargetCanExecuteMethod;
		
      public MyICommand(Action executeMethod) {
         _TargetExecuteMethod = executeMethod; 
      }
		
      public MyICommand(Action executeMethod, Func<bool> canExecuteMethod){ 
         _TargetExecuteMethod = executeMethod;
         _TargetCanExecuteMethod = canExecuteMethod; 
      }
		
      public void RaiseCanExecuteChanged() { 
         CanExecuteChanged(this, EventArgs.Empty); 
      }
		
      bool ICommand.CanExecute(object parameter) { 
		
         if (_TargetCanExecuteMethod != null) { 
            return _TargetCanExecuteMethod(); 
         } 
			
         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(); 
         } 
      } 
   } 
}

Como puede ver, esta es una implementación de delegación simple de ICommand donde tenemos dos delegados, uno para executeMethod y otro para canExecuteMethod, que se pueden pasar en la construcción.

En la implementación anterior, hay dos constructores sobrecargados, uno solo para executeMethod y otro para executeMethod y puedo canExecuteMethod.

Agreguemos una propiedad de tipo MyICommand en la clase StudentView Model. Ahora necesitamos construir una instancia en StudentViewModel. Usaremos el constructor sobrecargado de MyICommand que toma dos parámetros.

public MyICommand DeleteCommand { get; set;} 

public StudentViewModel() { 
   LoadStudents(); 
   DeleteCommand = new MyICommand(OnDelete, CanDelete); 
}

Ahora agregue la implementación de los métodos OnDelete y CanDelete.

private void OnDelete() { 
   Students.Remove(SelectedStudent); 
}

private bool CanDelete() { 
   return SelectedStudent != null; 
}

También necesitamos agregar un nuevo SelectedStudent para que el usuario pueda eliminar el elemento seleccionado de ListBox.

private Student _selectedStudent;
 
public Student SelectedStudent { 
   get { 
      return _selectedStudent; 
   } 
	
   set { 
      _selectedStudent = value;
      DeleteCommand.RaiseCanExecuteChanged(); 
   } 
}

A continuación se muestra la implementación completa de la clase ViewModel.

using MVVMDemo.Model; 

using System.Collections.ObjectModel; 
using System.Windows.Input; 
using System;

namespace MVVMDemo.ViewModel { 

   public class StudentViewModel { 
	
      public MyICommand DeleteCommand { get; set;} 
		
      public StudentViewModel() { 
         LoadStudents(); 
         DeleteCommand = new MyICommand(OnDelete, CanDelete); 
      }
		
      public ObservableCollection<Student> Students { 
         get; 
         set; 
      }
		
      public void LoadStudents() { 
         ObservableCollection<Student> students = new ObservableCollection<Student>();
			
         students.Add(new Student { FirstName = "Mark", LastName = "Allain" }); 
         students.Add(new Student { FirstName = "Allen", LastName = "Brown" }); 
         students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" }); 
			
         Students = students; 
      }
		
      private Student _selectedStudent; 
		
      public Student SelectedStudent { 
         get {
            return _selectedStudent; 
         } 
			
         set { 
            _selectedStudent = value;
            DeleteCommand.RaiseCanExecuteChanged(); 
         } 
      }
		
      private void OnDelete() { 
         Students.Remove(SelectedStudent); 
      }
		
      private bool CanDelete() { 
         return SelectedStudent != null; 
      }
   } 
}

En StudentView.xaml, necesitamos agregar la propiedad SelectedItem en un ListBox que se vinculará a la propiedad SelectStudent.

<ListBox ItemsSource = "{Binding Students}" SelectedItem = "{Binding SelectedStudent}"/>

A continuación se muestra el archivo xaml completo.

<UserControl x:Class = "MVVMDemo.Views.StudentView" 
   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:MVVMDemo.Views" 
   xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel" 
   xmlns:data = "clr-namespace:MVVMDemo.Model" 
   xmlns:vml = "clr-namespace:MVVMDemo.VML" 
   vml:ViewModelLocator.AutoHookedUpViewModel = "True" 
   mc:Ignorable = "d"
   d:DesignHeight = "300" d:DesignWidth = "300">
	
   <UserControl.Resources> 
      <DataTemplate DataType = "{x:Type data:Student}"> 
		
         <StackPanel Orientation = "Horizontal"> 
			
            <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
               Width = "100" Margin = "3 5 3 5"/> 
					
            <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
               Width = "100" Margin = "0 5 3 5"/> 
					
            <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
               Margin = "0 5 3 5"/> 
					
         </StackPanel> 
			
      </DataTemplate> 
   </UserControl.Resources>
	
   <Grid> 
      <StackPanel Orientation = "Horizontal"> 
         <ListBox ItemsSource = "{Binding Students}" 
            SelectedItem = "{Binding SelectedStudent}"/> 
				
         <Button Content = "Delete" 
            Command = "{Binding DeleteCommand}"
            HorizontalAlignment = "Left" 
            VerticalAlignment = "Top" 
            Width = "75" /> 
      </StackPanel> 
   </Grid>
	
</UserControl>

Cuando se compile y ejecute el código anterior, verá la siguiente ventana.

Puede ver que el botón Eliminar está desactivado. Se habilitará cuando seleccione cualquier elemento.

Cuando selecciona cualquier elemento y presiona eliminar. Verá que la lista de elementos seleccionados se elimina y el botón Eliminar nuevamente se desactiva.

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