Implementación de MVP basado en ApplicationController e IoC en una aplicación WinForms

¡Buenas tardes!

En este artículo, hablaré sobre cómo implementé el patrón MVP en mi aplicación de Windows Forms y describiré situaciones prácticas y características del uso de IoC y ApplicationController. Cambiar de codebehind a MVP me permitió:

  • mejorar la legibilidad debido a una mejor separación de código (SRP): separe BL de la vista;
  • desarrollar una metodología para ampliar aún más la funcionalidad de la aplicación;
  • deshacerse de singleton, que solía trabajar con la configuración de la aplicación.

Acerca de la aplicación


Una aplicación para ayudar a administrar grupos de capital de riesgo. Le permite llenar un grupo con publicaciones pendientes. La funcionalidad principal en este momento es cargar publicaciones pendientes en el grupo VK con imágenes o videos, hashtags, encuestas, ubicación geográfica y la capacidad de configurar la hora de publicación y la cantidad de publicaciones por día. Por el momento, la aplicación tiene una forma.

Con el crecimiento de su funcionalidad, se acumuló mucho código detrás, lo que comenzó a confundir el hecho de que el formulario se volvió muy cargado y contenía todo en sí mismo. Cuando planifiqué el desarrollo posterior del proyecto, no podía imaginar qué podría continuar más allá con el mismo espíritu, y ahora llegó el momento en que la tarea principal no era refinar la funcionalidad, sino refactorizar. Y comencé a buscar una solución que ayudara a optimizar y dividir el código y, en general, mejorar la arquitectura de la aplicación para que sea más agradable trabajar con él.

Fuente

Solución de refactorización


La solución fue implementar el patrón MVP. Como base, tomé el artículo Características de implementación MVP para formularios Windows Forms .

El artículo analiza un ejemplo extendido de una aplicación simple con 3 formas: 2 principales y 1er modal. El artículo analiza un enfoque muy avanzado:

  • Además de ApplicationController e IoC, el Adaptador también se usa allí, lo que permite usar diferentes IoC;
  • 3 tipos de formas: con parámetros, sin parámetros y modal;
  • El principio DIP se aplica ampliamente.

En mi proyecto, uso solo una forma sin argumentos, abandoné el adaptador (siguiendo el principio YAGNI), ya que IoC Lightinject es suficiente para mí y, en menor medida, uso DIP para simplificar el proyecto.

Implementación MVP


MVP (Model-View-Presenter) es un patrón de diseño diseñado para la conveniencia de separar la lógica empresarial de la forma en que se muestra. Puedes leer más sobre la teoría en el artículo anterior. Describiré los componentes en mi implementación:

  • El modelo es una estructura de datos transferida entre View y Presenter y contiene datos tanto para la visualización como para la ejecución de la lógica. En mi caso, el modelo es Configuración. Cuando se inicia el proyecto, las configuraciones se cargan en MainFormView, y cuando comienza la carga, MainFormView comprueba y pasa Settigns a Presenter para que Presenter ejecute la lógica de su lado.
  • Ver es un formulario en el que los datos se muestran al usuario. En mi caso, estos son datos del modelo de Configuración, y View también proporciona eventos para que Presenter asocie View con BL.

MainFormView implementa una interfaz IView común común a todas las vistas

    public interface IView
    {
        void Show();

        void Close();
    }

así como una interfaz privada IMainFormView específica para esta vista solamente. Al principio, pensé en abandonarlo, pero si asocia Presenter directamente con el formulario, cuando trabaje con dicha Vista, estará disponible todo el conjunto de métodos específicos del Formulario, lo cual no es conveniente.

    public interface IMainFormView: IView
    {
        void LoadSettings(Settings settings);

        void UpdateSettings(Settings settings);

        void ShowMessage(string message);

        void LoadGroups(List<Group> groups);

        void EnableVKUploadGroupBox();

        bool Check();

        event Action Login;

        new event Action Close;

        event Action VKUpload;
    }

Otra innovación de MVP es que el método Show ha sido reemplazado en el formulario y el ApplicationContext se pasa al formulario a través del constructor, de modo que al cambiar de formulario a formulario y cerrar, el formulario principal se reasigna.

        protected ApplicationContext _context;

        public MainForm(ApplicationContext context)
        {
            _context = context;
            InitializeComponent();

            dateTimePickerBeginDate.Format = DateTimePickerFormat.Custom;
            dateTimePickerBeginDate.CustomFormat = "MM/dd/yyyy hh:mm:ss";

            buttonAuth.Click += (sender, args) => Invoke(Login);
            this.FormClosing += (sender, args) => Invoke(Close);
            buttonLoad.Click += (sender, args) => Invoke(VKUpload);
        }

        public new void Show()
        {
            _context.MainForm = this;
            Application.Run(_context);
        }

- Presenter es una clase que encapsula View, Services y business logic (BL) dentro de sí mismo, con la ayuda de la cual organiza la interacción entre View y Services. BL se implementa principalmente en Ver controladores de eventos. A diferencia del CodeBehind utilizado anteriormente, en los controladores de eventos MVP que ejecutan BL se muestran en Presenter, y para simplificar, los eventos en Vista se muestran en forma de acción sin argumentos. Los manejadores reciben todos los datos necesarios para su ejecución a través del modelo obtenido del formulario a través del método público.

Presenter contiene el método Run, que es llamado por el ApplicationController y que lanza el formulario:

    public interface IPresenter
    {
        void Run();
    }

ApplicationController : un único punto de control y ejecución de toda la aplicación. Encapsula toda la lógica en sí misma: IoC, presentadores, vista, servicios.

La administración se realiza a través del método Run, que llama al presentador correspondiente. Todos los presentadores están conectados entre sí a través del ApplicationController, que el presentador recibe en el constructor. Por lo tanto, el Presentador puede llamar a otro Presentador llamando al método Ejecutar, que internamente llama al Contenedor de IoC para obtener el Presentador deseado y comenzarlo.

    public class ApplicationController
    {
        ServiceContainer _container;

        public ApplicationController(ServiceContainer serviceContainer)
        {
            _container = serviceContainer;
            _container.RegisterInstance<ApplicationController>(this);
        }

        public void Run<TPresenter>() where TPresenter:class, IPresenter
        {
            var presenter = _container.GetInstance<TPresenter>();
            presenter.Run();
        }
    }

El contenedor de IoC es un agregador de todas las "dependencias" utilizadas en la lógica de la aplicación. Contiene:

  • Ver constructores
  • Presentadores de constructores
  • instancias de servicio
  • contexto de aplicación
  • ApplicationController

Todas las dependencias se agregan al contenedor durante el inicio, esto se puede ver en el archivo Program.cs:

         static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            ulong appid = ulong.Parse(ConfigurationManager.AppSettings["AppIdForTest"]);
            VKGroupHelperWorker vk = new VKGroupHelperWorker(appid);


            ServiceContainer container = new ServiceContainer();
            container.RegisterInstance<VKGroupHelperWorker>(vk);
            container.RegisterInstance<Settings>(Globals.Settings);
            container.RegisterInstance<ApplicationContext>(Context);
            container.Register<IMainFormView,MainForm>();
            container.Register<MainFormPresenter>();

            ApplicationController controller = new ApplicationController(container);
            controller.Run<MainFormPresenter>();
        }

Para IoC, utilicé el componente Lightinject, que debe instalarse a través de NPM antes de su uso.

Por lo tanto, el contenedor puede contener constructores de objetos y los propios objetos, como se hace con Configuración y VKGroupHelperWorker (cliente API VK), formando el conjunto de todos los recursos de aplicación utilizados. Una característica útil del contenedor es que todos estos recursos integrados, las clases se pueden obtener a través de los argumentos del constructor. Por ejemplo,
ApplicationController, IMainFormView, VKGroupHelperWorker: dependencias implementadas previamente, que pueden ser tanto constructores de objetos como instancias. Si se ha implementado una instancia, todos los objetos generados funcionarán con la misma instancia, lo que le permite deshacerse del patrón singleton, si se usa.

public MainFormPresenter(ApplicationController applicationController, IMainFormView mainForm, Settings settings, VKGroupHelperWorker vk)
        {
            _view = mainForm;
            _settings = settings;
            _vk = vk;

            _view.Login += () => Login();
            _view.Close += () => Close();
            _view.VKUpload += () => VKUpload();
        }

La implementación de MVP me permitió:

  • deshacerse parcialmente de Singleton, que solía trabajar con la configuración de la aplicación;
  • Separe BL de Vista, mejorando así la separación de código (SRP)
  • para desarrollar un enfoque para expandir aún más la aplicación sin saturar la Vista.

Puede encontrar más información sobre lo que se ha hecho en el repositorio del proyecto .

All Articles