José F. Romaniello

Las aventuras y desventuras de un codificador.

En mi entrada anterior explique el patrón Passive View". Ahora es el turno del patrón Supervising Controller.

Este patrón aprovecha la infraestructura de binding que posee winforms. A diferencia del Passive View, el View en este caso conoce un modelo al cual conectarse. Dado que el view puede interactuar con un modelo tanto su implementación como su interfaz es bastante mas simple en comparación con Passive View.

El Presenter en este caso se ocupa de la lógica mas complejas, mientras que el view se ocupa de la lógica simple, delegando esto último al mecanismo de databinding que usualmente se define de forma declarativa.

Sin más preámbulo, y siguiendo con nuestro ejemplo anterior esta es la interfaz del view:

public interface IEditarClienteView
{
    Cliente ClienteEnEdicion { get; set; }
    void CargarPaises(IEnumerable<Pais> paises);

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

Estos son los tests de nuestro presenter:

[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 repositorioPaises = CrearDobleDeRepositorioPaises();
        var presenter = new EditarClientePresenter(
            view.Object,
            CrearDobleRepositorioClientes(),
            repositorioPaises);

        presenter.Iniciar(1);

        view.Verify(v => v.CargarPaises(repositorioPaises.ObtenerOrdenadosPorNombre()));
    }

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

        var repositorioClientes = CrearDobleRepositorioClientes();

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

        presenter.Iniciar(1);
        view.Object.ClienteEnEdicion
            .Should().Be.SameInstanceAs(repositorioClientes.Obtener(1));
    }


    [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 CuandoPresionoElBotonGuardarEntoncesGuardoConRepositorio()
    {
        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.Raise(v => v.GuardarClick += null, EventArgs.Empty);

        Mock.Get(repositorioClientes).Verify(r => r.Actualizar(cliente));
    }


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

        new EditarClientePresenter(
            view.Object,
            CrearDobleRepositorioClientes(),
            CrearDobleDeRepositorioPaises());

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

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


}

Esta es la implementación del presenter:

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

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

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

    void EditarClienteViewGuardarClick(object sender, EventArgs e)
    {
        repositorioClientes.Actualizar(editarClienteView.ClienteEnEdicion);
        editarClienteView.Close();
    }

    public void Iniciar(int idCliente)
    {
        editarClienteView.CargarPaises(repositorioPaises.ObtenerOrdenadosPorNombre());
        editarClienteView.ClienteEnEdicion = repositorioClientes.Obtener(1);
        editarClienteView.Show();
    }
}

Ahora bien, esta es la implementación de nuestro view:

public partial class FormEditarCliente : Form, IEditarClienteView
{
    public FormEditarCliente()
    {
        InitializeComponent();
    }

    public Cliente ClienteEnEdicion
    {
        get { return (Cliente) clienteBindingSource.DataSource; }
        set { clienteBindingSource.DataSource = value; }
    }

    public void CargarPaises(IEnumerable<Pais> paises)
    {
        paisBindingSource.DataSource = paises;
    }

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

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

"clienteBindingSource" y "paisBindingSource" son controles en nuestro formulario creados al hacer databinding. Si alguien no conoce como hacer binding en winforms, aquí les dejo un mini-screencast:

Unable to display content. Adobe Flash is required.

Por último el código fuente puede ser descargado desde esta enlace.

| More

21 comentarios:

Ernesto Camilo Vera dijo...

Muy buen Post, Felicitaciones!! Me resulta super util! Gracias!

José F. Romaniello dijo...

Gracias a vos por tus comentarios!

Gracias José, es aqui que logre entender MVP, y gracias nuevamente por escribir en castellano

Edgar

<span>Muy buen articulo Jose, me imagino que esto se puede aplicar a cualquier tecnología de escritorio ya sea Winform o WPF.</span>

Alejandro

José F. Romaniello dijo...

<span>En WPF se puede utilizar este pattern, sin embargo, hay otro patrón que va mejor y es el MVVM (o Presentation Model). Ya vas a ver la diferencia en mi siguiente post.</span>

José F. Romaniello dijo...

<span><span>Un placer Edgar, me alegro que te haya sido útil. </span></span>

Lo primero felicitarte por estos dos articulos tan buenos que has explicado.

Me gustaria preguntarte, entre el Passive View y este, ¿cual es mas facil de implementar?

A mi me parece que este ultimo, porque aprovecha el data binding de Winforms. 

¿que me dices?

Un saludo

José F. Romaniello dijo...

SI, para este caso de uso es m�s sencillo Supervising controller... No
obstante para un caso de uso como por ejemplo; el men� principal de una
aplicaci�n standard este patr�n no tiene mucho sentido y el passive view es
mas aplicable.. Ya que no hay ning�n modelo.

Jose, No se ve nada en tu respuesta

José F. Romaniello dijo...

<span>Perdón, te decía que para este caso de uso que explico, defintivamente es mejor Supervising Controller.Sin embargo hay otros casos donde quedaría mejor passive view, un ejemplo de ello es cuando no hay un modelo... como el menu principal de la aplicación cuyo presenter solo invoca metodos para abrir pantallas.. y no existe un modelo.</span>

Carlos de Jesus Baez dijo...

Muy Buen Post, seria mas util para nosotros los novatos que queremos empezar con buen pie, si utilizaras en detalle una sola entidad(clase Clientes, Clase Items, etc) y detallarla desde su inicio- implementacion-interaccion con la base de datos-vista. Quizas es mucho pedir pero como se que tu objetivo es que nosotros aprendamos.. los novatos tambien lo necesitamos. Gracias...

Carlos de Jesus Baez dijo...

Se me olvido decir que ha sido uno de los mejores post que he visto en la web

José F. Romaniello dijo...

<span>Gracias por tu comentarios, justamente la ida de trabajar con patrones es separar al máximo las responsabilidades. En estos artículos estoy intentando explicar como desacoplar la lógica de interfaz de usuario de los detalles de la vista. Cuál es el punto de interacción con la base de datos? Si descargas el proyecto de ejemplo veras una interfaz IRepositorioClientes... Esa interfaz puede ser implementada con Ado.Net, pueden guardarse los clientes en un archivo XML, en un CSV, puede utilizarse un ORM como NHibernate, puede hacerse con una base de datos orientada a objetos u orientada a documentos. Las posibilidades son infinitas, pero te repito la clave de todo buen diseño... es separar al máximo y con inteligencia las responsabilidades de cada cosa.</span>

Carlos de Jesus Baez dijo...

Gracias por tu comentario, esta muy bueno el codigo, yo lo dividi un poco mas respetando las reglas del concepto. seria bueno que por favor me explicaras mejor aunque creo que voy entendiendo todo, pues hay interes de aprender. Gracias por tu pronta respuesta..

Nicolás Marzoni dijo...

<span>Muy bueno el ejemplo José. Estoy armando la arquitectura para el proyecto final de la facultad y me sirvió de mucho.</span>
<span>Una consulta. Hay alguna manera de cancelar las modificaciones cuando estamos usando bindingsource? o quizá actualizar únicamente cuando hacen click sobre el botón aceptar?</span>

José F. Romaniello dijo...

Hola Nicol�s, gracias por comentar. Desconozco si eso existe, en principio
no ser�a necesario:
1. Si el usuario hace click en aceptar deber�as persistir los cambios,
por ejemplo.
2. Si el usuario hace click en cancelar, no deber�as hacer nada, y
simplemente cerrar el formulario. Autom�ticamente la instancia de tu
viewmodel u objeto de dominio que estes utilizando ser�a garbage collected.

Nicolás Marzoni dijo...

<span>No se ve tu respuesta José. Podrías volver a responder?</span>
<span>Gracias.</span>

José F. Romaniello dijo...

<span><span>Hola Nicolás, gracias por comentar. Desconozco si eso existe, en principio no sería necesario:Si el usuario hace click en aceptar deberías persistir los cambios, por ejemplo.Si el usuario hace click en cancelar, no deberías hacer nada, y simplemente cerrar el formulario. Automáticamente la instancia de tu viewmodel u objeto de dominio que estes utilizando sería garbage collected.</span></span>

Nicolás Marzoni dijo...

En mi caso la instancia proviene de un listado en un formulario anterior. Voy a crear una nueva instancia para pasarsela a la vista de edición.
Gracias por la respuesta josé voy a trabajar un poco más en mi modelo.

José F. Romaniello dijo...

<span>no envies una entidad desde un formulario a otro, no importa si create una instancia nueva o no.</span>
Te recomiendo utilizar clases simples con por ejemplo el ID para comunicación entre distintas pantallas,  yo suelo enviar algo así para darte una idea "new EditarCliente(12)".
Por otro lado nunca hago referencia una pantalla (o presenter) desde otro, lo hago mediante un patrón llamado event aggregator. Digamos que si quiero que se abra una ventana para editar el cliente 11, utilizo EventAggregator.Publish(new EditarCliente(12)), luego alguien subscripto a ese evento responde.

Tengo varios posts sobre event aggregator, en mi blog espero te sean utiles y mucha suerte.

Nicolás Marzoni dijo...

<span>

<span>Voy a investigar más sobre el patrón EventAggregator.</span>
Muchas Gracias José!
</span>

Publicar un comentario en la entrada