Patrón MVVM | Análisis en 10 minutos

28 Apr 2025 10 min (0) Comentarios

El patrón MVVM es un patrón que tiene 20 años de vida y se inicio en en aplicaciones de escritorio ha evolucionado fuera del entorno de .NET y es muy muy popular en el Front end hoy en día. 

 

 

1 - Que es MVVM?

 

El término MVVM son las siglas del patrón Model-View-ViewModel (Modelo-Vista-ViewModel), un patrón de diseño centrado principalmente en la interfaz de usuario.

 

La idea principal de este patrón es separar de una forma clara la lógica de negocio de la propia interfaz. A diferencia de otros patrones muy comunes no ponemos la lógica del código en lo que llamaríamos code-behind de la UI sino que abstraemos esta lógica en el view model.

De esta forma, la vista no tiene nada de lógica, lo que hace las aplicaciones más fáciles de mantener y testear. 

 

Conocemos MVVM desde aproximadamente 2005 con WPF en .NET y luego se adopto por xamarin, maui etc, pero hoy en día en el front end todos los frameworks como son Vue, React o Angular son MVVM.

 

 

2 - Cómo funciona MVVM?

 

El funcionamiento principal se hace a través de un sistema de enlace de datos y el mecanismo de los comandos que nos da el propio framework. De esta forma la vista interactúa con el ViewModel sin conocerlo directamente. 

 

Si vamos a la documentación “oficial” de microsoft tenemos un diagrama como el siguiente: 

mvvm architecture

Donde vemos como la vista se comunica con el viewmodel a través de los comandos y es el viewModel quien actualiza el modelo. Posteriormente los datos se actualizan en la vista a través de los eventos de notificación. Por lo que conseguimos que la vista no conozca la lógica interna y el modelo no dependa de la vista. 

 

Esta separación de capas es clara, ya que cada una tiene un funcionamiento esperado:

  • Modelo: Es la capa de acceso a datos. Lo que quiere decir, insertar en la base de datos o servicio externo, leer, etc.
  • Vista: la interfaz de usuario, la cual no contiene nada de lógica de negocio, únicamente se encarga de la representación visual, en el caso de WPF en formato xaml, por supuesto, tenemos que enlazar la vista con las propiedades expuestas por el viewModel a través de los bindings.
  • ViewModel: una clase, bueno un conjunto de clases que actuarán entre la vista y el modelo. Lo que el ViewModel hace es contener los datos necesarios para exponerlos en propiedades que consume la vista. Además, no se queda ahí, sino que implementa los comandos para las posibles acciones que pueda invocar la vista. Por ejemplo, si la vista tiene un botón para ejecutar una acción, el ViewModel tiene un comando que será ejecutado cuando esa acción suceda. Pero Ojo, este ViewModel, no sabe si dicha acción sucede en un botón o por cualquier otra acción, solo sabe que sucede. Este Comando está tras la interfaz ICommand, lo que implica que debemos especificar un comando por cada acción. 

 

 

Pero, y la lógica de datos? 

 

Bueno, como en cada arquitectura que hemos visto, hay mil opiniones. Una que me gusta es la siguiente, si es una simple APP crud, lo más probable es que lo que insertes vaya directo al modelo, y tengas una capa con un repositorio el cual inyectas en el ViewModel. Pero si tienes más lógica, o tienes que agregar datos, etc. Mi opinión personal es que entre el modelo y el ViewModel, podemos añadir la capa de lógica de negocio como si de cualquier otro tipo de arquitectura se tratase. 

 

 

3 - Crear TODO app con MVVM en C#

 

Para este ejemplo vamos a ver como crear una aplicación sencilla, la típica TODO app, al final este tipo de aplicaciones son fáciles de hacer y muestran bien los elementos básicos de la arquitectura que queremos mostrar. 

 

De cara al usuario vamos a tener algo así: 

todo app wpf

Una caja de texto para introducir el nombre de la tarea y luego poder mostrarlas y eliminarlas. Algo simple. 

 

 

Técnicamente, a la hora de programar, podemos empezar por el diseño y luego modificarlo para poner los bindings etc, pero en nuestro caso empezaremos por el modelo. Para nuestro escenario es muy sencillo una única clase llamada SimpleTask la cual contiene dos propiedades, Description y Completed.

public class SimpleTask
{
    public string Description { get; set; }
    public bool Completed { get; set; }
}

En ciertos escenarios, quizá necesitas saber si una propiedad cambia, te permite invocar un evento de c#, para ello lo haces con el uso de la interfaz INotifyPropertyChanged y la implementación que el compilador te obliga, la cual sería algo como la siguiente:

public class SimpleTask : INotifyPropertyChanged
{
    public string Description { get; set; }
    
    private bool _completed;
    public bool Completed
    {
        get => _completed;
        set
        {
            if (Completed != value)
            {
                Completed = value;
                OnPropertyChanged(nameof(Completed));
            }
        }
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

 

Muchos puristas te pueden decir que INotifyPropertyChanged es una propiedad exclusiva de la vista, pero en mi opinión depende de lo que sea, está bien implementarlo. En nuestro caso, si estamos simulando una entidad de una base de datos lo suyo es NO implementarlo. 

 

Ahora pasamos a crear el ViewModel, el cual contiene una lista de las tareas, pero ojo, no usamos una lista cualquiera, utilizamos una ObservableCollection<T> esto es porque vamos a enlazar dicha colección a la lista de tareas de la interfaz.

public class ListSimpleTasksViewModel 
{
    public ObservableCollection<SimpleTask> SimpleTasks { get; } = new ObservableCollection<SimpleTask>(); 👈
}

Posteriormente necesitamos un campo de texto para la nueva tarea, la cual enlazaremos a un TextBox de la vista.

public class ListSimpleTasksViewModel
{
    public ObservableCollection<SimpleTask> SimpleTasks { get; } = new ObservableCollection<SimpleTask>(); 
    public string NewSimpleTask { get; set; } 👈
}
//en la vista
<TextBox Text="{Binding NewSimpleTask}" Width="300" Margin="0 0 0 5" />

 

Ahora nos queda la funcionalidad de añadir tareas, para ello lo que necesitamos es un ICommand para realizar una acción, para ello nos aseguraremos que tenemos instalado el paquete “CommunityToolkit.Mvvm”, que si bien no es totalmente necesario, nos da acceso a la clase RelayCommand y únicamente tenemos que definir en el constructor un delegado para que funcione: 

public class ListSimpleTasksViewModel 
{
    public ObservableCollection<SimpleTask> SimpleTasks { get; } = new ObservableCollection<SimpleTask>();
    
    public string NewSimpleTask { get; set; }

    public ICommand AddSimpleTask { get; } 👈

    public ListSimpleTasksViewModel()
    {
        AddSimpleTask = new RelayCommand(AddSimpleTaskAction); 👈
    }
    
    private void AddSimpleTaskAction() 👈
    {
        SimpleTasks.Add(new SimpleTask()
        {
            Description = NewSimpleTask,
            Completed = false,
        });
        NewSimpleTask = string.Empty;
    }
}

//en la vista
<Button Content="Agregar Tarea" Command="{Binding AddSimpleTask}" Width="120" />

 

Ahora tenemos que enlazar la lista que tenemos en el code behind con la vista, para ello  lo ponemos en un listbox donde mostramos cada elemento:

<ListBox ItemsSource="{Binding SimpleTasks}" Margin="0 10 0 5" Height="200">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <CheckBox IsChecked="{Binding Completed, Mode=TwoWay}" />
                <TextBlock Text="{Binding Description}" Margin="5,0,0,0" />
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Como vemos tenemos el binding del listbox con la lista observable de tareas y luego para cada uno de los elementos estamos mostrando un checkbox para indicar si lo hemos completado y la descripción. Y ahora podemos probar:

todo app mvvm

Antes de pasar a eliminar las tareas completadas (o no) hay que tener algo en cuenta, si le das a añadir el valor de la caja de texto no ha cambiado. 

Esto es porque al crear la tarea estamos asignando el valor de NewSimpleTask a vacio, pero no estamos notificando a la vista que el valor a cambiado, ya que no utilizamos INotifyCollectionChanged, por ello, debemos de implementarlo en el código:

public class ListSimpleTasksViewModel : INotifyPropertyChanged 👈
{
    public ObservableCollection<SimpleTask> SimpleTasks { get; } = new ObservableCollection<SimpleTask>();

    private string _newSimpleTask = string.Empty; 👈
    public string NewSimpleTask 👈
    {
        get => _newSimpleTask;
        set
        {
            if (_newSimpleTask != value)
            {
                _newSimpleTask = value;
                OnPropertyChanged();
            }
        }
    }
    
    ...

    public event PropertyChangedEventHandler? PropertyChanged; 👈

    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) 👈
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Si ahora pruebas, verás como el campo de texto se pone vacío al añadir la tarea. 

Ojo, en el caso de la lista, no debemos hacerlo porque es un tipo especial de WPF que ya implementa INotifyPropertyChanged.

 

Finalmente nos queda eliminar las tareas de la lista, en nuestro caso vamos a crear en el ViewModel una propiedad llamada SelectedSimpleTask que va a ser del tipo SimpleTask, y en el ListBox de la interfaz vamos a utilizar la propiedad SelectedItem para hacer el binding:

public class ListSimpleTasksViewModel : INotifyPropertyChanged
{
    public ObservableCollection<SimpleTask> SimpleTasks { get; } = new ObservableCollection<SimpleTask>();

    public SimpleTask? SelectedSimpleTask { get; set; } 👈
    
    ...  
}

<ListBox ItemsSource="{Binding SimpleTasks}"
            SelectedItem="{Binding SelectedSimpleTask}" 👈
            Margin="0 10 0 5" Height="200">
    
    <ListBox.ItemTemplate>
    ....
    </ListBox.ItemTemplate>
</ListBox>

 

De esta forma al seleccionar un elemento en la lista, el valor de la propiedad SelectedSimpleTask se actualizará. E Igual que hicimos para añadir tareas, debemos crear un comando para eliminar: 

 

public class ListSimpleTasksViewModel : INotifyPropertyChanged
{
    public ObservableCollection<SimpleTask> SimpleTasks { get; } = new ObservableCollection<SimpleTask>();

    public SimpleTask? SelectedSimpleTask { get; set; }
    
    private string _newSimpleTask = string.Empty;
    public string NewSimpleTask
    {
        get => _newSimpleTask;
        set
        {
            if (_newSimpleTask != value)
            {
                _newSimpleTask = value;
                OnPropertyChanged();
            }
        }
    }

    public ICommand AddSimpleTask { get; }

    public ICommand RemoveSimpleTask { get; } 👈

    public ListSimpleTasksViewModel()
    {
        AddSimpleTask = new RelayCommand(AddSimpleTaskAction);
        RemoveSimpleTask = new RelayCommand(RemoveSimpleTaskAction);
    }
    
    private void AddSimpleTaskAction()
    {
        SimpleTasks.Add(new SimpleTask()
        {
            Description = NewSimpleTask,
            Completed = false,
        });
        NewSimpleTask = string.Empty;
    }

    private void RemoveSimpleTaskAction() 👈
    {
        if (SelectedSimpleTask is not null)
        {
            SimpleTasks.Remove(SelectedSimpleTask);
            SelectedSimpleTask = null;
        }
       
    }
 
    public event PropertyChangedEventHandler? PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}


//ui


<Window x:Class="WpfApp1.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:WpfApp1"
        xmlns:viewModel="clr-namespace:WpfApp1.ViewModel"
        mc:Ignorable="d"
        Title="MainWindow">
    <Window.DataContext>
        <viewModel:ListSimpleTasksViewModel />
    </Window.DataContext>

    <StackPanel Margin="10">
        
        <TextBox Text="{Binding NewSimpleTask}"
                 Width="300" Margin="0 0 0 5" />
     
        
        <Button Content="Agregar Tarea" Command="{Binding AddSimpleTask}" 
                Width="120" />

        <!-- Lista de tareas -->
        <ListBox ItemsSource="{Binding SimpleTasks}"
                 SelectedItem="{Binding SelectedSimpleTask}"
                 Margin="0 10 0 5" Height="200">
            <!-- Definimos la plantilla visual para cada ítem Tarea -->
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <!-- Checkbox ligada a la propiedad Completada de Tarea -->
                        <CheckBox IsChecked="{Binding Completed, Mode=TwoWay}" />
                        <!-- Texto mostrando la descripción de la tarea -->
                        <TextBlock Text="{Binding Description}" Margin="5,0,0,0" />
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <Button Content="Delete" Command="{Binding RemoveSimpleTask}" Width="150" /> 👈
    </StackPanel>
</Window>

Ahí podemos ver todo el código, tanto del ViewModel como de la interfaz. 

 

Si tienes algo más complejo como puede ser una base de datos a través de repository pattern, o casos de uso, etc, puedes hacer inyección de dependencias normal dentro del ViewModel y tendrás que mantener tanto la BBDD como las colecciones en tu ViewModel de forma sincronizada continuamente. 

 

Por ejemplo, cuando añades una tarea, lo haces tanto en la bbdd como en tu lista, lo mismo al eliminarla, la eliminas de la bbdd y cuando termina la eliminas de tu lista. 

© copyright 2025 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café