Implementar Patron MVVM WPF

Author Top
Pablo

En esta entrada de  blog se mostrara un ejemplo básico de cómo implementar el patrón MVVM, no pretendo mostrar la mejor forma de usar el patrón ya que existen muchas variaciones y perspectivas acerca del mismo; esto queda al gusto del lector

Esta implementación explica cómo implementar el patrón en el lenguaje C#, sin embargo también se provee de las clases bases para realizarlo en VisualBasic.

Básicamente el patrón MVVM busca dividir los programas en 3 partes para un mejor mantenimiento: el modelo, la vista y la vista-modelo, a continuación listamos los pasos la realizarlo.

1. Implementar Modelo.

El Modelo debe de contener solo la lógica de negocio, esto quiere decir que está formado por las clases o entidades del sistema, como vamos a utilizar WPF debemos crear las propiedades como DependencyProperties en los modelos, esto para realizar nuestros Bindings en las vistas.

Normalmente una DependencyProperty se declara como se muestra en este tutorial de Microsoft, además se pueden agregar métodos OnPropertyChanged específicos para cada propiedad, sin embargo generalmente solo se necesita que se notifique a la vista cualquier cambio en las propiedades, es por esto que se puede realizar una clase padre que pase por parámetro el nombre de la propiedad y realice la notificación como se muestra a continuación, con esta implementación los modelos heredan de esta clase y llaman el método OnPropertyChanged en cada set.

 C#

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WpfApplicationMVVM.MVVM.Model{
    public class NotifyBase : INotifyPropertyChanged{

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName){
            if (PropertyChanged != null) {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
		    }//Fin de if.
        }//Fin de OnPropertyChanged.
    }//Fin de clase.
}//Fin de namespace.

 VB

Imports System.ComponentModel

Namespace MVVM.Model

    Public Class NotifyBase
        Implements INotifyPropertyChanged

        Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged

        Protected Overridable Sub OnPropertyChanged(ByVal propertyName As String)
            RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
        End Sub

    End Class

End Namespace

Un ejemplo de su implementación seria la siguiente clase:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WpfApplicationMVVM.MVVM.Model;

namespace WpfApplicationMVVM.Model{
    class Client: NotifyBase{

#region Atributos
        private int id;
        private string name;
        private string lastname;
#endregion 
        
#region Propiedades
        public int Id { 
            get{
                return id; 
            }//Fin de get.
            set{
                id = value;
                OnPropertyChanged("Id");
                OnPropertyChanged("DisplayName");
            }//Fin de set.
        }//Fin de propiedad Id.

        public string Name { 
            get{
                return name; 
            }//Fin de get.
            set{
                name = value;
                OnPropertyChanged("Name");
                OnPropertyChanged("DisplayName");
            }//Fin de set.
        }//Fin de propiedad Name.

        public string LastName { 
            get{
                return lastname; 
            }//Fin de get.
            set{
                lastname = value;
                OnPropertyChanged("LastName");
                OnPropertyChanged("DisplayName");
            }//Fin de set.
        }//Fin de propiedad LastName.

        public string DisplayName {
            get {
                return Id + "-" + Name + " " + LastName;
            }//Fin de get.
        }//Fin de la propiedad readonly DisplayName.
#endregion
        
    }//Fin de clase.
}//Fin de namespace.

2. Implementar Vista-Modelo.

La Vista-Modelo es la parte del patrón que comunica la vista con el modelo, cumple una función similar al controlador en el patrón MVC con la diferencia que MVVM pretende o busca dejar limpio de código completamente la Vista manejando los eventos( Commands ), Bindings, y además de alojar los listados de los modelos.

Los Command se utilizan para asignar acciones desde otras clases a los botones haciendo uso de propiedades y demás para producir la funcionalidad deseada, en otras palabras los eventos, estos hace uso de la interfaz ICommand y se utilizan como se muestra en este ejemplo, sin embargo existe una implementación más genérica para poder usarse fácilmente en los proyectos a continuación se coloca esta implementación conocida como RelayCommand, esta es mostrada en el Link de WPF del primer párrafo, aunque nosotros le llamaremos CommandBase, es prácticamente la misma con pequeños cambios.

 C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace WpfApplicationMVVM.MVVM.ViewModel{
    class CommandBase : ICommand{
        private Action<object> execAction;
        private Func<object, bool> canExecFunc;
	
        public CommandBase(Action<object> execAction){
            this.execAction = execAction;
            this.canExecFunc = null;
	    }//Fin de constructor.

	    public CommandBase(Action<object> execAction, Func<object, bool> canExecFunc){
		    this.execAction = execAction;
		    this.canExecFunc = canExecFunc;
	    }//Fin de constructor.

	    public bool CanExecute(object parameter){
            if (canExecFunc != null) {
			    return canExecFunc.Invoke(parameter);
		    }//Fin de if.
            else {
			    return true;
		    }//Fin de else.
	    }//Fin de CanExecute.

        public event EventHandler CanExecuteChanged{
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        } 

	    public void Execute(object parameter){
		    if (execAction != null) {
			    execAction.Invoke(parameter);
		    }//Fin de if.
	    }//Fin de Execute.
    }//Fin de clase.
}//Fin de namespace.

 VB

Namespace MVVM.ViewModel

    Public Class CommandBase
        Implements ICommand

        Private execAction As Action(Of Object)
        Private canExecFunc As Func(Of Object, Boolean)

        Public Sub New(execAction As Action(Of Object))
            Me.execAction = execAction
        End Sub

        Public Sub New(execAction As Action(Of Object), canExecFunc As Func(Of Object, Boolean))
            Me.execAction = execAction
            Me.canExecFunc = canExecFunc
        End Sub

        Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute
            If canExecFunc IsNot Nothing Then
                Return canExecFunc.Invoke(parameter)
            Else
                Return True
            End If
        End Function

        Public Event CanExecuteChanged As System.EventHandler Implements ICommand.CanExecuteChanged

        Public Sub Execute(parameter As Object) Implements ICommand.Execute
            If execAction IsNot Nothing Then
                execAction.Invoke(parameter)
            End If
        End Sub

    End Class

End Namespace

Esta implementación genérica nos permite crear funciones y utilizarlas en la Vista-Modelo sin hacerlas muy personalizadas, lógicamente habrá momentos en que se necesitará una implementación más especifica del ICommand y esta base puede que no nos sea útil pero generalmente es de gran ayuda, su uso se muestra en el siguiente ejemplo.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using WpfApplicationMVVM.Model;
using WpfApplicationMVVM.MVVM.ViewModel;

namespace WpfApplicationMVVM.ViewModel{
    class ClientViewModel : ObservableCollection<Client>, INotifyPropertyChanged{

#region Atributos
        private int selectedIndex;

        private int id;
        private string name;
        private string lastName;
        private ICommand addClientCommand;
        private ICommand clearCommand;
#endregion

#region Propiedades
        public int SelectedIndexOfCollection {
            get { 
                return selectedIndex; 
            }//Fin de get.
            set{
                selectedIndex = value;
                OnPropertyChanged("SelectedIndexOfCollection");

                //Activa el evento OnPropertyChanged de los atributos para refrescar las propiedades segun el indice seleccionado.
                OnPropertyChanged("Id");
                OnPropertyChanged("Name");
                OnPropertyChanged("LastName");
            }//Fin de set.
        }//Fin de propiedad Name.

        public int Id {
            get {
                if (this.SelectedIndexOfCollection > -1){
                    return this.Items[this.SelectedIndexOfCollection].Id; 
                }//Fin de if.
                else {
                    return id;
                }//Fin de else.
            }//Fin de get.
            set{
                if (this.SelectedIndexOfCollection > -1){
                    this.Items[this.SelectedIndexOfCollection].Id = value;
                }//Fin de if.
                else {
                    id = value;
                }//Fin de else.
                OnPropertyChanged("Id");
            }//Fin de set.
        }//Fin de propiedad Id.

        public string Name {
            get { 
                if (this.SelectedIndexOfCollection > -1){
                    return this.Items[this.SelectedIndexOfCollection].Name; 
                }//Fin de if.
                else {
                    return name;
                }//Fin de else. 
            }//Fin de get.
            set{
                if (this.SelectedIndexOfCollection > -1){
                    this.Items[this.SelectedIndexOfCollection].Name = value;
                }//Fin de if.
                else {
                    name = value;
                }//Fin de else.
                OnPropertyChanged("Name");
            }//Fin de set.
        }//Fin de propiedad Name.

        public string LastName {
            get { 
                if (this.SelectedIndexOfCollection > -1){
                    return this.Items[this.SelectedIndexOfCollection].LastName; 
                }//Fin de if.
                else {
                    return lastName;
                }//Fin de else.
            }//Fin de get.
            set{
                if (this.SelectedIndexOfCollection > -1){
                    this.Items[this.SelectedIndexOfCollection].LastName = value;
                }//Fin de if.
                else {
                    lastName = value;
                }//Fin de else.
                OnPropertyChanged("LastName");
            }//Fin de set.
        }//Fin de propiedad LastName.

        public ICommand AddClientCommand {
            get { 
                return addClientCommand; 
            }//Fin de get.
            set{
                addClientCommand = value;
            }//Fin de set.
        }//Fin de propiedad LastName.

        public ICommand ClearCommand {
            get { 
                return clearCommand; 
            }//Fin de get.
            set{
                clearCommand = value;
            }//Fin de set.
        }//Fin de propiedad LastName.
#endregion

#region Constructores
        public ClientViewModel() {
            Client vlClient1 = new Client();
            vlClient1.Id = 1;
            vlClient1.Name = "Pablo";
            vlClient1.LastName = "Gonzalez";
            this.Add(vlClient1);

            Client vlClient2 = new Client();
            vlClient2.Id = 2;
            vlClient2.Name = "Roberto";
            vlClient2.LastName = "Herrera";
            this.Add(vlClient2);

            Client vlClient3 = new Client();
            vlClient3.Id = 3;
            vlClient3.Name = "Anibal";
            vlClient3.LastName = "Salazar";
            this.Add(vlClient3);

            AddClientCommand = new CommandBase(param => this.AddClient());
            ClearCommand = new CommandBase(new Action<Object>(ClearClient));
        }//Fin de constructor.
#endregion

#region Interface
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName){
            if (PropertyChanged != null) {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
		    }//Fin de if.
        }//Fin de OnPropertyChanged.
#endregion

#region Metodos/Funcciones
        private void AddClient(){
            Client vlClient = new Client();
            vlClient.Id = Id;
            vlClient.Name = Name;
            vlClient.LastName = LastName;
            this.Add(vlClient);
        }//Fin de AddClient.

        private void ClearClient(object obj){
            SelectedIndexOfCollection = -1;
            Id = 0;
            Name = "";
            LastName = "";
        }//Fin de AddClient.
#endregion
    }//Fin de clase.
}//Fin de namespace.

Nota: Cuando se utiliza C# hay 2 forma de crear el Command

AddClientCommand = new CommandBase(param => this.AddClient())

  También puede realizarse de la siguiente manera:

ClearCommand = new CommandBase(new Action<Object>(ClearClient)) 

Con VisualBasic se asigna así:

UpdateCommand = New CommandBase(New Action(Of Object)(AddressOf UpdateItem))

Notaran que en la Vista-Modelo es una lista de CLIENT donde se maneja un índice de elemento que se encuentra seleccionado, y si no existe ese elemento maneja las propiedades normalmente para permitir crear nuevos clientes, al mismo tiempo aquí repetimos la implementación del INotifyPropertyChanged por el motivo de que no se puede heredar de 2 clases, entonces se hereda de la lista ObservableCollection<Client> y se implementa la interfaz.

Se remarca la importancia de la buena práctica de programación de nombrar las interfaces con el prefijo ¨I¨ para diferenciar la herencia de la implementación fácilmente, si alguno desea tener de atributo la lista y así no implementar la interfaz no hay problema igual funciona Wink , vemos también como se llama el evento OnPropertyChanged en cada propiedad, y en el índice se llaman todas para notificar que se cambiaran las propiedades por las del cliente en el índice seleccionado.

3. Implementar la Vista.

La Vista es la interfaz de usuario, aquí existe un poco de controversia, ya que los puristas del Patrón MVVM creen que la Vista no debe poseer ni una línea de código, otros creen que puede contener pero no mucho, otros manejan algunos Bindings en código , entre otras formas de realizarlo, en el ejemplo mostraremos una Vista limpia de código donde solo existe XAML y el método que crea los componentes InitializeComponent() ya que este es obligatorio

<Window x:Class="WpfApplicationMVVM.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:viewmodel="clr-namespace:WpfApplicationMVVM.ViewModel"
        Title="MainWindow" Height="350" Width="525">
    
    <Window.Resources>
        <viewmodel:ClientViewModel x:Key="clientViewModel" />
    </Window.Resources>

    <Grid  DataContext="{StaticResource clientViewModel}">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition MinHeight="200"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <TextBlock x:Name="txbIdLabel" VerticalAlignment="Center" Margin="20 10 20 0" Grid.Column="0" Grid.Row="0" Text="Id" FontSize="15"/>
        <TextBox x:Name="txtId" Text="{Binding Id}" Grid.Column="1" Grid.Row="0" Height="20" Margin="20 10 20 0" VerticalAlignment="Center" />
        
        <TextBlock x:Name="txbNameLabel" VerticalAlignment="Center" Margin="20 10 0 0" Grid.Column="0" Grid.Row="1" Text="Name" FontSize="15"/>
        <TextBox x:Name="txtName" Text="{Binding Name}" Grid.Column="1" Grid.Row="1" Height="20" Margin="20 10 20 0" VerticalAlignment="Center" />
        
        <TextBlock x:Name="txbLastNameLabel" VerticalAlignment="Center" Margin="20 10 0 0" Grid.Column="0" Grid.Row="2" Text="LastName" FontSize="15"/>
        <TextBox x:Name="txtLastName" Text="{Binding LastName}" Grid.Column="1" Grid.Row="2" Height="20" Margin="20 10 20 0" VerticalAlignment="Center" />
        
        <StackPanel x:Name="stcpButtons" Orientation="Horizontal" Grid.Column="0" Grid.Row="3" Grid.ColumnSpan="2" Margin="20 10 20 10 ">
            <Button Content="Clear" Height="40" Width="60" Command="{Binding ClearCommand}"/>
            <Button Content="Add" Height="40" Width="60" Margin="10 0 0 0" Command="{Binding AddClientCommand}"/>
        </StackPanel>

        <ListView x:Name="lvClients" SelectedIndex="{Binding SelectedIndexOfCollection, Mode=TwoWay}" MinHeight="50" ItemsSource="{StaticResource ResourceKey=clientViewModel}" DisplayMemberPath="DisplayName"  Grid.Column="0" Grid.Row="4" Grid.ColumnSpan="2" Margin="20 10 20 50"/>
    </Grid>
</Window>

Vemos como solamente se hace una instancia de las Vista-Modelo <viewmodel:ClientViewModel x:Key="clientViewModel"/> y se realizan los Bindings correspondientes a la misma, así como la asignación de Command de los botones.

El resultado de la aplicación ejemplo seria el siguiente:

Espero que les guste la entrada, creo que les servirá de mucho a los nuevos en el mundo de WPF muchas gracias por leer el post Cool