MVP: Patrón Passive View para WinForms

Este es el primer post de una serie que voy a escribir, explicando distintos patrones de Presentación para aplicaciones de escritorio, los cuales en general permiten la separación de responsabilidades. Dicha separación nos permitirá escribir tests de los diferentes artefactos.

En esta entrada voy a hablar puntualmente del patrón Passive View definido por Martin Fowler y voy a mostrar una implementación para un caso de uso sencillo. Hacia al final del artículo he dejado el link para descargar el código completo del ejemplo.

En este patrón el View expone una serie de métodos los cuales permiten al Presenter manejar cada uno de sus widgets (controles) y  sus respectivas propiedades. Una característica importante de este patrón es que el View no conoce ningún Modelo.

Imaginemos un caso de uso para editar los datos de un cliente, al iniciar la pantalla tenemos que mostrar los datos actuales del cliente, luego el usuario realizará modificaciones y guardará o cancelará.

Esta sería nuestra pantalla:

image

La interfaz del artefacto View se podría definir de la siguiente manera:

public interface IEditarClienteView
{
    string Nombre { get; set; }
    string Apellido { get; set; }
    string CodigoPais { get; set; }

    void CargarPaisParaSeleccion(string codigoIso, string nombre);

    event EventHandler GuardarClick;
    event EventHandler CancelarClick;
    
    void Close();
    void Show();
}

El Presenter sería algo así:

public class EditarClientePresenter
{
    private readonly IEditarClienteView editarClienteView;
    private readonly IRepositorioClientes repositorioClientes;
    private readonly IRepositorioPaises repositorioPaises;
    private int clienteEnEdicion;

    public EditarClientePresenter(IEditarClienteView editarClienteView, 
        IRepositorioClientes repositorioClientes, 
        IRepositorioPaises repositorioPaises)
    {
        this.editarClienteView = editarClienteView;
        this.repositorioClientes = repositorioClientes;
        this.repositorioPaises = repositorioPaises;
        editarClienteView.GuardarClick += EditarClienteViewGuardarClick;
        editarClienteView.CancelarClick += EditarClienteViewCancelarClick;
    }

    public void Iniciar(int idCliente)
    {
        clienteEnEdicion = idCliente;
        foreach (var pais in repositorioPaises.ObtenerOrdenadosPorNombre())
        {
            editarClienteView.CargarPaisParaSeleccion(pais.CodigoIso, pais.Nombre);
        }

        var cliente = repositorioClientes.Obtener(idCliente);
        
        editarClienteView.Nombre = cliente.Nombre;
        editarClienteView.Apellido = cliente.Apellido;
        editarClienteView.CodigoPais = cliente.Pais.CodigoIso;

        editarClienteView.Show();
    }

    private void EditarClienteViewCancelarClick(object sender, EventArgs e)
    {
        editarClienteView.Close();
    }

    private void EditarClienteViewGuardarClick(object sender, EventArgs e)
    {
        var cliente = repositorioClientes.Obtener(clienteEnEdicion);
        cliente.Nombre = editarClienteView.Nombre;
        cliente.Apellido= editarClienteView.Apellido;
        cliente.Pais = repositorioPaises.Obtener(editarClienteView.CodigoPais);
    }
}

Cabe mencionar que este presenter, así como la interfaz del view, nace a partir de una serie de tests que fui escribiendo al principio. Esta técnica se denomina comúnmente TDD, aunque sus derivados como BDD también son válidos. Esta es la serie de tests que definieron mi implementación:

using System;
using Moq;
using NUnit.Framework;
using PassiveView.Dominio;
using PassiveView.Repositorios;
using SharpTestsEx;

namespace PassiveView.Tests
{
    [TestFixture]
    public class EditarClientePresenterTests
    {
        public IRepositorioPaises CrearDobleDeRepositorioPaises()
        {
            var repositorioPaises = new Mock<IRepositorioPaises>();
            repositorioPaises.Setup(r => r.ObtenerOrdenadosPorNombre())
                .Returns(new[]
                             {
                                 new Pais {CodigoIso = "AR", Nombre = "Argentina"},
                                 new Pais {CodigoIso = "UY", Nombre = "Uruguay"}
                             });

            return repositorioPaises.Object;
        }

        private static IRepositorioClientes CrearDobleRepositorioClientes()
        {
            var repositorio = new Mock<IRepositorioClientes>();
            var cliente = new Cliente
                                    {
                                        Nombre = "Jose",
                                        Apellido = "Romaniello",
                                        Pais = new Pais {CodigoIso = "AR", Nombre = "Argentina"}
                                    };
            repositorio.Setup(r => r.Obtener(1)).Returns(cliente);
            return repositorio.Object;
        }

        [Test]
        public void AlIniciarCargarPaises()
        {
            var view = new Mock<IEditarClienteView>();

            var presenter = new EditarClientePresenter(
                view.Object,
                CrearDobleRepositorioClientes(),
                CrearDobleDeRepositorioPaises());

            presenter.Iniciar(1);

            view.Verify(v => v.CargarPaisParaSeleccion("AR", "Argentina"));
            view.Verify(v => v.CargarPaisParaSeleccion("UY", "Uruguay"));
        }

        [Test]
        public void CuandoInicioCargoLosDatosDelCliente()
        {
            var view = new Mock<IEditarClienteView>();
            view.SetupAllProperties();

            var presenter = new EditarClientePresenter(
                view.Object,
                CrearDobleRepositorioClientes(),
                CrearDobleDeRepositorioPaises());

            presenter.Iniciar(1);

            view.Object.Satisfy(v => v.Apellido == "Romaniello"
                                     && v.Nombre == "Jose"
                                     && v.CodigoPais == "AR");
        }

        [Test]
        public void CuandoInicioMostrarForm()
        {
            var view = new Mock<IEditarClienteView>();
            view.SetupAllProperties();

            var presenter = new EditarClientePresenter(
                view.Object,
                CrearDobleRepositorioClientes(),
                CrearDobleDeRepositorioPaises());

            presenter.Iniciar(1);

            view.Verify(v => v.Show());
        }

        [Test]
        public void CuandoGuardoCargarLosValoresDeLaPantalla()
        {
            var view = new Mock<IEditarClienteView>();
            view.SetupAllProperties();

            var repositorioClientes = CrearDobleRepositorioClientes();
            var cliente = repositorioClientes.Obtener(1);

            var presenter = new EditarClientePresenter(
                view.Object,
                repositorioClientes,
                CrearDobleDeRepositorioPaises());

            presenter.Iniciar(1);

            view.Object.Nombre = "Pedro";

            view.Raise(v => v.GuardarClick += null, EventArgs.Empty);

            cliente.Nombre.Should().Be.EqualTo("Pedro");

        }
        [Test]
        public void CuandoCanceloCierroElForm()
        {
            var view = new Mock<IEditarClienteView>();
            view.SetupAllProperties();

            
            var presenter = new EditarClientePresenter(
                view.Object,
                CrearDobleRepositorioClientes(),
                CrearDobleDeRepositorioPaises());

            view.Raise(v => v.CancelarClick += null, EventArgs.Empty);

            view.Verify(v => v.Close());
        }


    }
}

La implementación del view es algo que he hecho al final, y es la siguiente:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Forms;

namespace PassiveView
{

    public partial class FormEditarCliente : Form, IEditarClienteView
    {
        private readonly BindingList<KeyValuePair<string, string>>  paises 
                = new BindingList<KeyValuePair<string, string>>();

        public FormEditarCliente()
        {
            InitializeComponent();

            cmbPaises.DataSource = paises;
            cmbPaises.DisplayMember = "Value";
            cmbPaises.ValueMember = "Key";
        }

        public string Nombre
        {
            get { return tNombre.Text; }
            set { tNombre.Text = value; }
        }

        public string Apellido
        {
            get { return tApellido.Text; }
            set { tApellido.Text = value; }
        }

        public string CodigoPais
        {
            get { return cmbPaises.SelectedValue.ToString(); }
            set { cmbPaises.SelectedValue = value; }
        }

        public void CargarPaisParaSeleccion(string codigoIso, string nombre)
        {
            paises.Add(new KeyValuePair<string, string>(codigoIso, nombre));
        }

        public event EventHandler GuardarClick
        {
            add { btAceptar.Click += value; }
            remove { btAceptar.Click -= value; }
        }

        public event EventHandler CancelarClick
        {
            add { btCancelar.Click += value; }
            remove { btCancelar.Click -= value; }
        }
    }
}

Este código esta escrito en el código behind del formulario.

Conclusiones

Este patrón tiene la ventaja de lograr una muy buena separación entre Model View y Presenter, nuestro código de view es relativamente sencillo y el view hace muy pocas cosas, por lo tanto podríamos decir que el patrón logra muy bien su objetivo.

Como desventaja podríamos decir, que en este caso de uso sencillo se puede observar que estamos desaprovechando las características de databinding que winforms nos ofrece, haciendo que la interfaz de nuestra View sea muy pesada (muchos métodos) y el mapeo entre propiedades y controles puede llegar a ser tedioso.

El código completo de este sencillo ejemplo puede ser descargado de aquí.


blog comments powered by Disqus
  • Categories

  • Archives