Modelo-Vista-Presentador en WinForms

90

Estoy tratando de implementar el método MVP por primera vez, usando WinForms.

Estoy tratando de comprender la función de cada capa.

En mi programa tengo un botón GUI que cuando se hace clic en él abre una ventana de diálogo abierto.

Entonces, usando MVP, la GUI maneja el evento de clic de botón y luego llama presenter.openfile ();

Dentro de presenter.openfile (), ¿debería eso delegar la apertura de ese archivo a la capa del modelo, o como no hay datos o lógica para procesar, debería simplemente actuar en la solicitud y abrir la ventana openfiledialog?

Actualización: he decidido ofrecer una recompensa ya que siento que necesito más ayuda en esto, y preferiblemente adaptado a mis puntos específicos a continuación, para tener contexto.

Bien, después de leer sobre MVP, he decidido implementar la vista pasiva. Efectivamente, tendré un montón de controles en un Winform que serán manejados por un Presentador y luego las tareas delegadas al Modelo (s). Mis puntos específicos están a continuación:

  1. Cuando se carga el winform, debe obtener una vista de árbol. ¿Estoy en lo cierto al pensar que la vista debería llamar a un método como: presenter.gettree (), este a su vez delegará en el modelo, que obtendrá los datos para la vista de árbol, lo creará y lo configurará, lo devolverá al presentador, que a su vez pasará a la vista que luego simplemente lo asignará a, digamos, un panel?

  2. ¿Sería lo mismo para cualquier control de datos en Winform, ya que también tengo una vista de cuadrícula de datos?

  3. Mi aplicación tiene varias clases de modelos con el mismo ensamblaje. También es compatible con una arquitectura de complementos con complementos que deben cargarse al inicio. ¿La vista simplemente llamaría a un método de presentador, que a su vez llamaría a un método que carga los complementos y muestra la información en la vista? Qué nivel controlaría las referencias de los complementos. ¿La vista tendría referencias a ellos o al presentador?

  4. ¿Estoy en lo correcto al pensar que la vista debe manejar todo lo relacionado con la presentación, desde el color del nodo de la vista de árbol hasta el tamaño de la cuadrícula de datos, etc.?

Creo que son mis principales preocupaciones y si entiendo cómo debe ser el flujo de estos, creo que estaré bien.

Darren Young
fuente
Este enlace lostechies.com/derekgreer/2008/11/23/… explica algunos de los estilos de MVP. Podría resultar útil además de la excelente respuesta de Johann.
ak3nat0n

Respuestas:

123

Esta es mi humilde opinión sobre MVP y sus problemas específicos.

Primero , cualquier cosa con la que un usuario pueda interactuar, o simplemente mostrarse, es una vista . Las leyes, el comportamiento y las características de tal vista se describen mediante una interfaz . Esa interfaz se puede implementar utilizando una interfaz de usuario de WinForms, una interfaz de usuario de consola, una interfaz de usuario web o incluso ninguna interfaz de usuario (generalmente cuando se prueba un presentador); la implementación concreta simplemente no importa siempre que obedezca las leyes de su interfaz de vista .

En segundo lugar , una vista siempre está controlada por un presentador . Las leyes, el comportamiento y las características de dicho presentador también se describen mediante una interfaz . Esa interfaz no tiene interés en la implementación de la vista concreta siempre que obedezca las leyes de su interfaz de vista.

En tercer lugar , dado que un presentador controla su vista, para minimizar las dependencias realmente no hay ganancia en que la vista sepa algo sobre su presentador. Existe un contrato acordado entre el presentador y la vista y eso lo establece la interfaz de la vista.

Las implicaciones de Third son:

  • El presentador no tiene ningún método al que pueda llamar la vista, pero la vista tiene eventos a los que el presentador puede suscribirse.
  • El presentador conoce su punto de vista. Prefiero lograr esto con inyección de constructor en el presentador de concreto.
  • La vista no tiene idea de qué presentador la controla; simplemente nunca se le proporcionará un presentador.

Para su problema, lo anterior podría verse así en un código algo simplificado:

interface IConfigurationView
{
    event EventHandler SelectConfigurationFile;

    void SetConfigurationFile(string fullPath);
    void Show();
}

class ConfigurationView : IConfigurationView
{
    Form form;
    Button selectConfigurationFileButton;
    Label fullPathLabel;

    public event EventHandler SelectConfigurationFile;

    public ConfigurationView()
    {
        // UI initialization.

        this.selectConfigurationFileButton.Click += delegate
        {
            var Handler = this.SelectConfigurationFile;

            if (Handler != null)
            {
                Handler(this, EventArgs.Empty);
            }
        };
    }

    public void SetConfigurationFile(string fullPath)
    {
        this.fullPathLabel.Text = fullPath;
    }

    public void Show()
    {
        this.form.ShowDialog();        
    }
}

interface IConfigurationPresenter
{
    void ShowView();
}

class ConfigurationPresenter : IConfigurationPresenter
{
    Configuration configuration = new Configuration();
    IConfigurationView view;

    public ConfigurationPresenter(IConfigurationView view)
    {
        this.view = view;            
        this.view.SelectConfigurationFile += delegate
        {
            // The ISelectFilePresenter and ISelectFileView behaviors
            // are implicit here, but in a WinForms case, a call to
            // OpenFileDialog wouldn't be too far fetched...
            var selectFilePresenter = Gimme.The<ISelectFilePresenter>();
            selectFilePresenter.ShowView();
            this.configuration.FullPath = selectFilePresenter.FullPath;
            this.view.SetConfigurationFile(this.configuration.FullPath);
        };
    }

    public void ShowView()
    {
        this.view.SetConfigurationFile(this.configuration.FullPath);
        this.view.Show();
    }
}

Además de lo anterior, normalmente tengo una IViewinterfaz base en la que guardo la Show()vista del propietario o el título de la vista de la que suelen beneficiarse mis vistas.

A sus preguntas:

1. Cuando se carga el winform, debe obtener una vista de árbol. ¿Estoy en lo cierto al pensar que la vista debería llamar a un método como: presenter.gettree (), esto a su vez delegará en el modelo, que obtendrá los datos para la vista de árbol, lo creará y lo configurará, lo devolverá al presentador, que a su vez pasará a la vista que luego simplemente lo asignará a, digamos, un panel?

Llamaría IConfigurationView.SetTreeData(...)desde IConfigurationPresenter.ShowView(), justo antes de la llamada aIConfigurationView.Show()

2. ¿Sería lo mismo para cualquier control de datos en Winform, ya que también tengo una vista de cuadrícula de datos?

Sí, lo llamaría IConfigurationView.SetTableData(...). Depende de la vista formatear los datos que se le proporcionen. El presentador simplemente obedece el contrato de la vista de que quiere datos tabulares.

3. Mi aplicación tiene varias clases de modelos con el mismo ensamblaje. También es compatible con una arquitectura de complementos con complementos que deben cargarse al inicio. ¿La vista simplemente llamaría a un método de presentador, que a su vez llamaría a un método que carga los complementos y muestra la información en la vista? Qué nivel controlaría las referencias de los complementos. ¿La vista tendría referencias a ellos o al presentador?

Si los complementos están relacionados con la vista, las vistas deberían conocerlos, pero no el presentador. Si se trata de datos y modelos, entonces la vista no debería tener nada que ver con ellos.

4. ¿Estoy en lo cierto al pensar que la vista debería manejar todo lo relacionado con la presentación, desde el color del nodo de la vista de árbol hasta el tamaño de la cuadrícula de datos, etc.?

Si. Piense en ello como el presentador que proporciona XML que describe los datos y la vista que toma los datos y les aplica una hoja de estilo CSS. En términos concretos, el presentador puede llamar IRoadMapView.SetRoadCondition(RoadCondition.Slippery)y la vista luego muestra la carretera en color rojo.

¿Qué pasa con los datos de los nodos en los que se hace clic?

5. Si cuando hago clic en los árboles, ¿debo pasar a través del nodo específico al presentador y luego, a partir de ahí, el presentador calculará qué datos necesita y luego le pide al modelo esos datos, antes de presentarlos de nuevo a la vista?

Si es posible, pasaría todos los datos necesarios para presentar el árbol en una vista de una sola vez. Pero si algunos datos son demasiado grandes para pasarlos desde el principio o si son de naturaleza dinámica y necesitan la "última instantánea" del modelo (a través del presentador), entonces agregaría algo como event LoadNodeDetailsEventHandler LoadNodeDetailsa la interfaz de vista, para que el El presentador puede suscribirse a él, obtener los detalles del nodo LoadNodeDetailsEventArgs.Node(posiblemente a través de su ID de algún tipo) del modelo, de modo que la vista pueda actualizar los detalles del nodo que se muestran cuando el delegado del controlador de eventos regrese. Tenga en cuenta que los patrones asíncronos de esto pueden ser necesarios si la obtención de los datos puede ser demasiado lenta para una buena experiencia de usuario.

Johann Gerell
fuente
3
No creo que necesariamente tengas que desacoplar la vista y el presentador. Por lo general, desacojo el modelo y el presentador, haciendo que el presentador escuche los eventos del modelo y actúe en consecuencia (actualice la vista). Tener un presentador en la vista facilita la comunicación entre la vista y el presentador.
kasperhj
11
@lejon: Dices que tener un presentador en la vista facilita la comunicación entre la vista y el presentador , pero estoy totalmente en desacuerdo. Mi punto de vista es el siguiente: cuando la vista conoce al presentador, entonces, para cada evento de vista, la vista debe decidir qué método de presentador es el adecuado para llamar. Eso es "2 puntos de complejidad", ya que la vista no sabe realmente qué evento de vista corresponde a qué método de presentador . El contrato no especifica eso.
Johann Gerell
5
@lejon: Si, por otro lado, la vista solo expone el evento real, entonces el presentador mismo (quién sabe lo que quiere hacer cuando ocurre un evento de vista) simplemente se suscribe para hacer lo correcto. Eso es solo "1 punto de complejidad", que en mi libro es el doble de bueno que "2 puntos de complejidad". En términos generales, menos acoplamiento significa menos costo de mantenimiento durante la ejecución de un proyecto.
Johann Gerell
9
Yo también suelo usar el presentador encapsulado como se explica en este enlace lostechies.com/derekgreer/2008/11/23/… en el que la vista es el único titular del presentador.
ak3nat0n
3
@ ak3nat0n: Con respecto a los tres estilos de MVP explicados en el enlace que proporcionó, creo que esta respuesta de Johann podría estar más estrechamente alineada con el tercer estilo que se llama Estilo de presentador observador : "El beneficio del estilo Presentador observador es que desacopla completamente el conocimiento del presentador de la vista, lo que hace que la vista sea menos susceptible a cambios dentro del presentador ".
DavidRR
11

El presentador, que contiene toda la lógica en la vista, debe responder al botón que se hace clic como dice @JochemKempe . En términos prácticos, el controlador de eventos de clic de botón llama presenter.OpenFile(). El presentador puede entonces determinar qué se debe hacer.

Si decide que el usuario debe seleccionar un archivo, vuelve a llamar a la vista (a través de una interfaz de vista) y deja que la vista, que contiene todos los aspectos técnicos de la interfaz de usuario, muestre el OpenFileDialog. Esta es una distinción muy importante en el sentido de que no se debe permitir al presentador realizar operaciones vinculadas a la tecnología de interfaz de usuario en uso.

El archivo seleccionado se devolverá al presentador, que continúa con su lógica. Esto puede involucrar cualquier modelo o servicio que deba manejar el procesamiento del archivo.

La razón principal para usar un patrón MVP, en mi opinión, es separar la tecnología UI de la lógica de la vista. Por lo tanto, el presentador orquesta toda la lógica mientras la vista la mantiene separada de la lógica de la interfaz de usuario. Esto tiene el efecto secundario muy agradable de hacer que el presentador sea completamente comprobable.

Actualización: dado que el presentador es la encarnación de la lógica que se encuentra en una vista específica , la relación vista-presentador es, en mi opinión , una relación uno a uno. Y para todos los propósitos prácticos, una instancia de vista (por ejemplo, un formulario) interactúa con una instancia de presentador, y una instancia de presentador interactúa con una sola instancia de vista.

Dicho esto, en mi implementación de MVP con WinForms, el presentador siempre interactúa con la vista a través de una interfaz que representa las capacidades de la interfaz de usuario de la vista. No hay limitación sobre qué vista implementa esta interfaz, por lo tanto, diferentes "widgets" pueden implementar la misma interfaz de vista y reutilizar la clase de presentador.

Peter Lillevold
fuente
Gracias. Entonces, en el método presenter.OpenFile (), ¿no debería tener el código para mostrar el openfiledialog? En su lugar, ¿debería volver a la vista para mostrar esa ventana?
Darren Young
4
Bien, nunca dejaría que el presentador abriera cuadros de diálogo directamente, ya que eso rompería sus pruebas. Descargue eso en la vista o, como he hecho en algunos escenarios, tenga una clase "FileOpenService" separada que maneje la interacción real del diálogo. De esa manera, puede falsificar el servicio de apertura de archivos durante las pruebas. Poner dicho código en un servicio separado puede darle buenos efectos secundarios de reutilización :)
Peter Lillevold
2

El presentador debe actuar en la solicitud y mostrar la ventana openfiledialog como sugirió. Dado que no se requieren datos del modelo, el presentador puede y debe manejar la solicitud.

Supongamos que necesita los datos para crear algunas entidades en su modelo. Puede pasar el canal de transmisión a la capa de acceso donde tiene un método para crear entidades a partir de la transmisión, pero le sugiero que maneje el análisis del archivo en su presentador y use un constructor o un método Create por entidad en su modelo.

JochemKempe
fuente
1
Gracias por la respuesta. Además, ¿tendría un solo presentador para la vista? ¿Y ese presentador maneja la solicitud, o si se requieren datos, entonces delega en cualquier número de clases de modelos que actúan sobre las solicitudes específicas? ¿Es esa la forma correcta? Gracias de nuevo.
Darren Young
3
Una vista tiene un presentador, pero un presentador puede tener varias vistas.
JochemKempe