pattern mvc ejemplo c# .net winforms dependency-injection simple-injector

c# - mvc - Winforms-Patrón MVP: ¿Usando ApplicationController estático para coordinar la aplicación?



mvc c# windows forms ejemplo (1)

Una clase estática es, por supuesto, útil en algunos casos, pero hay muchas desventajas en este enfoque.

  • Los tienden a crecer en algo así como una clase de Dios. Ya ves que esto sucede. Entonces esta clase viola a SRP
  • Una clase estática no puede tener dependencias y, por lo tanto, necesita utilizar el patrón anti del Localizador de servicios para obtener sus dependencias. Este no es un problema si considero que esta clase es parte de la raíz de la composición , pero, sin embargo, a menudo esto va por el camino equivocado.

En el código provisto veo tres responsabilidades de esta clase.

  1. EventAggregator
  2. Lo que llamas información de la Session
  3. Un servicio para abrir otras vistas

Algunos comentarios sobre estas tres partes:

EventAggregator

Aunque este es un patrón ampliamente utilizado y, a veces puede ser muy poderoso, yo no soy aficionado a este patrón. Veo este patrón como algo que proporciona optional runtime data donde en la mayoría de los casos estos datos de tiempo de ejecución no son opcionales. En otras palabras, solo use este patrón para datos realmente opcionales. Para todo lo que no es realmente opcional, use dependencias duras, usando inyección de constructor.

Los que necesitan la información en ese caso dependen de IEventListener<TMessage> . El que publica el evento depende de IEventPublisher<TMessage> .

public interface IEventListener<TMessage> { event Action<TMessage> MessageReceived; } public interface IEventPublisher<TMessage> { void Publish(TMessage message); } public class EventPublisher<TMessage> : IEventPublisher<TMessage> { private readonly EventOrchestrator<TMessage> orchestrator; public EventPublisher(EventOrchestrator<TMessage> orchestrator) { this.orchestrator = orchestrator; } public void Publish(TMessage message) => this.orchestrator.Publish(message); } public class EventListener<TMessage> : IEventListener<TMessage> { private readonly EventOrchestrator<TMessage> orchestrator; public EventListener(EventOrchestrator<TMessage> orchestrator) { this.orchestrator = orchestrator; } public event Action<TMessage> MessageReceived { add { orchestrator.MessageReceived += value; } remove { orchestrator.MessageReceived -= value; } } } public class EventOrchestrator<TMessage> { public void Publish(TMessage message) => this.MessageReceived(message); public event Action<TMessage> MessageReceived = (e) => { }; }

Para poder garantizar que los eventos se almacenan en una única ubicación, extraemos ese almacenamiento (el event ) en su propia clase, EventOrchestrator .

El registro es el siguiente:

container.RegisterSingleton(typeof(IEventListener<>), typeof(EventListener<>)); container.RegisterSingleton(typeof(IEventPublisher<>), typeof(EventPublisher<>)); container.RegisterSingleton(typeof(EventOrchestrator<>), typeof(EventOrchestrator<>));

El uso es trivial:

public class SomeView { private readonly IEventPublisher<GuardChanged> eventPublisher; public SomeView(IEventPublisher<GuardChanged> eventPublisher) { this.eventPublisher = eventPublisher; } public void GuardSelectionClick(Guard guard) { this.eventPublisher.Publish(new GuardChanged(guard)); } // other code.. } public class SomeOtherView { public SomeOtherView(IEventListener<GuardChanged> eventListener) { eventListener.MessageReceived += this.GuardChanged; } private void GuardChanged(GuardChanged changedGuard) { this.CurrentGuard = changedGuard.SelectedGuard; } // other code.. }

Si otra vista recibirá una gran cantidad de eventos, siempre podría envolver todos los IEventListeners de esa vista en una clase específica EventHandlerForViewX que obtenga todo IEventListener<> inyectado.

Sesión

En la pregunta, define varias variables de ambient context como información de Session . La exposición de este tipo de información a través de una clase estática promueve el acoplamiento estricto a esta clase estática y, por lo tanto, hace que resulte más difícil probar partes de la aplicación. OMI toda la información provista por la Session es estática (en el sentido de que no cambia a lo largo de la vida de la aplicación) datos que podrían ser fácilmente inyectados en aquellas partes que realmente necesitan estos datos. Por lo tanto, la Session debería eliminarse por completo de la clase estática. Algunos ejemplos de cómo resolver esto de manera SÓLIDA:

Valores de configuración

La raíz de composición se encarga de leer toda la información de la fuente de configuración (por ejemplo, su archivo app.config). Esta información puede almacenarse en una clase POCO diseñada para su uso.

public interface IMailSettings { string MailAddress { get; } string DefaultMailSubject { get; } } public interface IFtpInformation { int FtpPort { get; } } public interface IFlowerServiceInformation { string FlowerShopAddress { get; } } public class ConfigValues : IMailSettings, IFtpInformation, IFlowerServiceInformation { public string MailAddress { get; set; } public string DefaultMailSubject { get; set; } public int FtpPort { get; set; } public string FlowerShopAddress { get; set; } } // Register as public static void RegisterConfig(this Container container) { var config = new ConfigValues { MailAddress = ConfigurationManager.AppSettings["MailAddress"], DefaultMailSubject = ConfigurationManager.AppSettings["DefaultMailSubject"], FtpPort = Convert.ToInt32(ConfigurationManager.AppSettings["FtpPort"]), FlowerShopAddress = ConfigurationManager.AppSettings["FlowerShopAddress"], }; var registration = Lifestyle.Singleton.CreateRegistration<ConfigValues>(() => config, container); container.AddRegistration(typeof(IMailSettings),registration); container.AddRegistration(typeof(IFtpInformation),registration); container.AddRegistration(typeof(IFlowerServiceInformation),registration); }

Y donde necesite información específica, por ejemplo, información para enviar un correo electrónico, puede poner IMailSettings en el constructor del tipo que necesita la información.

Esto también le dará la posibilidad de probar un componente usando diferentes valores de configuración, lo que sería más difícil de hacer si toda la información de configuración tuviera que venir del ApplicationController estático.

Para obtener información de seguridad, por ejemplo, el Usuario conectado puede usar el mismo patrón. Defina una abstracción de IUserContext , cree una implementación de WindowsUserContext y llene esto con el usuario que IUserContext sesión en la raíz de la composición. Debido a que el componente ahora depende de IUserContext lugar de obtener al usuario en tiempo de ejecución de la clase estática, el mismo componente también se podría usar en una aplicación MVC, donde se reemplazaría el WindowsUserContext con una implementación HttpUserContext .

Apertura de otras formas

Esta es en realidad la parte difícil. Normalmente también uso una gran clase estática con todo tipo de métodos para abrir otras formas. No IFormOpener el IFormOpener de esta respuesta a mis otros formularios, porque solo necesitan saber qué hacer, no qué forma les hace esa tarea. Entonces mi clase estática expone este tipo de métodos:

public SomeReturnValue OpenCustomerForEdit(Customer customer) { var form = MyStaticClass.FormOpener.GetForm<EditCustomerForm>(); form.SetCustomer(customer); var result = MyStaticClass.FormOpener.ShowModalForm(form); return (SomeReturnValue) result; }

Sin embargo....

No estoy contento con este enfoque, porque con el tiempo esta clase crece y crece. Con WPF utilizo otro mecanismo, que creo que también podría usarse con WinForms. Este enfoque se basa en una arquitectura basada en mensajes que se describe en esta y en estas increíbles publicaciones de blog. Aunque al principio la información parece no estar relacionada en absoluto, ¡es el concepto basado en mensajes lo que permite que estos patrones se llenen de piedras!

Todas mis ventanas WPF implementan una interfaz genérica abierta, por ejemplo, IEditView. Y si alguna vista necesita editar un cliente, solo obtiene esto IEditView inyectado. Un decorador se usa para mostrar la vista de la misma manera que lo hace el mencionado FormOpener . En este caso, utilizo una función específica de Inyector simple, llamada decorar decorador de fábrica , que puede usar para crear formularios siempre que sea necesario, del mismo modo que FormOpener utilizó el contenedor directamente para crear formularios siempre que lo necesita.

Así que realmente no probé esto, por lo que podría haber algunas dificultades con WinForms, pero este código parece funcionar en una primera y única ejecución.

public class EditViewShowerDecorator<TEntity> : IEditView<TEntity> { private readonly Func<IEditView<TEntity>> viewCreator; public EditViewShowerDecorator(Func<IEditView<TEntity>> viewCreator) { this.viewCreator = viewCreator; } public void EditEntity(TEntity entity) { // get view from container var view = this.viewCreator.Invoke(); // initview with information view.EditEntity(entity); using (var form = (Form)view) { // show the view form.ShowDialog(); } } }

Los formularios y el decorador deben registrarse como:

container.Register(typeof(IEditView<>), new[] { Assembly.GetExecutingAssembly() }); container.RegisterDecorator(typeof(IEditView<>), typeof(EditViewShowerDecorator<>), Lifestyle.Singleton);

Seguridad

IUserContext debe ser la base de toda seguridad.

Para la interfaz del usuario, normalmente oculto todos los controles / botones a los que un determinado usuario no tiene acceso. El mejor lugar es realizar esto en el evento Load .

Como utilizo el patrón de comando / controlador como se describe aquí para todas mis acciones externas a mis formularios / vistas, uso un decorador para verificar si un usuario tiene permiso para realizar este determinado comando (o consulta).

Te aconsejo que leas esta publicación unas cuantas veces hasta que realmente le compres el truco. Una vez que te hayas familiarizado con este patrón, ¡no harás nada más!

Si tiene alguna pregunta sobre estos patrones y cómo aplicar un decorador (permiso), ¡agregue un comentario!

Fondo

Estoy construyendo una aplicación C # .net de dos niveles:

  1. Nivel 1: Aplicación cliente de Winforms que utiliza el patrón de diseño MVP (Modelo-Vista-Presentador).
  2. Nivel 2: Servicio RESTful de WebAPI que se encuentra en la parte superior de Entity Framework y SQL Server.

Actualmente, tengo preguntas relacionadas con la arquitectura general de la aplicación de cliente Winforms. Soy nuevo en la programación (alrededor de un año) pero he progresado mucho con esta aplicación. Quiero dar un paso atrás y volver a evaluar mi enfoque actual para verificar que, en general, voy en la dirección correcta.

Dominio de aplicación

La aplicación Winforms es una aplicación de seguimiento de personal de seguridad bastante sencilla. La vista principal (Formulario) es el foco de la aplicación y tiene diferentes secciones que agrupan el contenido en áreas funcionales (por ejemplo, una sección para el seguimiento de los horarios del personal, una sección para rastrear a quién se asigna el lugar, etc.). Un menú al costado de la aplicación inicia vistas secundarias (por ejemplo, historial, estadísticas, contactos, etc.). La idea es que la aplicación pueda ser utilizada por una oficina de seguridad para organizar las operaciones diarias y luego mantener un historial detallado de todo en una base de datos para informar en el futuro.

Detalles técnicos

Como se mencionó, el cliente de Winforms se construye usando el patrón MVP (vista pasiva), centrándose en el uso de la inyección de dependencia tanto como sea posible (a través del contenedor SimpleInjector IoC). Cada vista (formulario) se combina con un solo presentador. Las vistas implementan interfaces, lo que permite al presentador controlar la vista (independientemente de la implementación concreta). La vista genera eventos para que el presentador se suscriba. Actualmente, los presentadores no pueden comunicarse directamente con otro presentador.

Un controlador de aplicación se usa para coordinar la aplicación. Esta es el área de la arquitectura de mi aplicación donde soy más shakey (de ahí el título del post). El controlador de la aplicación se usa actualmente para:

  1. Abra nuevas vistas (formularios) y administre formularios abiertos.
  2. Facilite la comunicación entre los componentes de la aplicación a través de un agregador de eventos. Un presentador publica un evento y cualquier número de presentador puede suscribirse a ese evento.
  3. Información de sesión de host (es decir, contexto de seguridad / inicio de sesión, datos de configuración, etc.)

El contenedor IoC se registra en el controlador de la aplicación al inicio de la aplicación. Esto permite que el controlador de la aplicación, por ejemplo, cree un presentador desde el contenedor y luego tenga todas las dependencias subsiguientes (vista, servicios, etc.) para ser manejadas automáticamente por el contenedor.

Pregunta

Para hacer que el Controlador de aplicación sea accesible para todos los presentadores, he creado el controlador como una clase estática.

public static class ApplicationController { private static Session _session; private static INavigationWorkflow _workflow; private static EventAggregator _aggregator; #region Registrations public static void RegisterSession(Session session) {} public static void RegisterWorkflow(INavigationWorkflow workflow) {} public static void RegisterAggregator(EventAggregator aggregator) {} #endregion #region Properties public static Session Session { get { return _session; } } #endregion #region Navigation public static void NavigateToView(Constants.View view) {} #endregion #region Events public static Subscription<TMessageType> Subscribe<TMessageType>(Action<TMessageType> action) {} public static void Publish<TMessageType>(TMessageType message) {} public static void Unsubscribe<TMessageType>(Subscription<TMessageType> subscription) {} #endregion }

¿Se considera esto una práctica aceptable para hacer una clase estática como esta? Quiero decir, ciertamente funciona. Simplemente se siente ... apagado? ¿Hay otros agujeros que pueda ver en mi arquitectura en función de lo que he descrito?

-

** EDIT **

Esta edición se realiza en respuesta a la respuesta de Ric .Net publicada a continuación.

He leído todas sus sugerencias. Como estoy comprometido con la utilización de la inyección de dependencia en la medida de lo posible, estoy a bordo con todas sus sugerencias. Ese fue mi plan desde el principio, pero cuando me encontré con cosas que no entendía cómo lograrlas mediante inyección, recurrí a la clase de controlador estático global para resolver mis problemas (una clase de dios se está convirtiendo, de hecho, ¡sí!) . Algunas de esas preguntas aún existen:

Agregador de eventos

La línea que define aquí es lo que debería considerarse opcional, creo. Proporcionaré un poco más de contexto sobre mi aplicación antes de delinear mi problema. Utilizando la terminología web, mi forma principal generalmente actúa como una vista de diseño , controles de navegación de alojamiento y una sección de notificación en el menú de la izquierda, y vistas parciales alojadas en el centro. Volviendo a la terminología de winforms, las vistas parciales son simplemente UserControls personalizados que trato como vistas, y cada uno de ellos está emparejado con su propio presentador. Tengo 6 de estas vistas parciales alojadas en mi formulario principal, y sirven como carne y patatas de la aplicación.

Como ejemplo, una vista parcial enumera las protecciones de seguridad disponibles y otra enumera posibles áreas de patrulla. En un caso de uso típico, un usuario arrastraría un guardia de seguridad disponible desde la lista disponible a una de las posibles áreas de patrulla, asignándose efectivamente a esa área. La vista del área de patrulla se actualizaría para mostrar el guardia de seguridad asignado y el protector se eliminaría de la vista de lista disponible. Utilizando eventos de arrastrar y soltar, puedo manejar esta interacción.

Mis preguntas surgen cuando necesito manejar otros tipos de interactividad entre las diversas vistas parciales. Por ejemplo, al hacer doble clic en la guardia que está asignada a una ubicación (como se ve en una vista parcial) podría resaltar el nombre de ese guardia en otra vista parcial que muestra todos los horarios del personal o mostrar los detalles / historial de los empleados en otra vista parcial. Pude ver el gráfico / matriz de lo que las vistas parciales están interesadas en los eventos que ocurren en otras vistas parciales como algo bastante complejo, y no estoy seguro de cómo manejar eso a través de la inyección. Con 6 vistas parciales, no me gustaría inyectar las otras 5 vistas parciales / presentadores en cada una. Estaba planeando lograr esto a través del agregador de eventos. Otro ejemplo que podría pensar es la necesidad de actualizar los datos en una vista separada (su propio formulario) en función de un evento que ocurre en una de las vistas parciales en el formulario principal.

Abridor de sesión y formulario

Realmente me gustan tus pensamientos aquí. Voy a tomar estas ideas y correr con ellas, ¡y ver dónde terminé!

Seguridad

¿Cuáles son sus pensamientos sobre el control del acceso de los usuarios a determinadas funciones en función del tipo de cuenta que tienen? Las recomendaciones que he estado leyendo en línea dicen que la seguridad podría implementarse modificando las vistas según su tipo de cuenta. La idea es que si un usuario no puede interactuar con un elemento de IU para iniciar una determinada tarea, entonces nunca se le pedirá al presentador que realice esa tarea. Tengo curiosidad si se inyecta WindowsUserContext en cada presentador y se realizan comprobaciones adicionales, especialmente para las solicitudes enlazadas al servicio http.

Todavía no he hecho demasiado desarrollo en lo que respecta al servicio, pero para las solicitudes vinculadas al servicio http, imagino que debe enviar información de seguridad junto con cada solicitud para que el servicio pueda autenticar la solicitud. Mi plan era inyectar el WindowsUserContext directamente en los agentes de servicio de winforms que terminan haciendo las solicitudes de servicio (es decir, la validación de seguridad no vendría del presentador). En ese caso, los agentes de servicio podrían realizar una verificación de seguridad de último minuto antes de enviar una solicitud.