Implementação do MVP com base em ApplicationController e IoC em um aplicativo WinForms

Boa tarde!

Neste artigo, falarei sobre como implementei o padrão MVP no meu aplicativo Windows Forms e descreverei situações e recursos práticos do uso de IoC e ApplicationController. Mudar de codebehind para MVP me permitiu:

  • melhorar a legibilidade devido a uma melhor separação de código (SRP) - separe BL do View;
  • desenvolver uma metodologia para expandir ainda mais a funcionalidade do aplicativo;
  • se livrar do singleton, que eu costumava trabalhar com as configurações do aplicativo.

Sobre o aplicativo


Um aplicativo para ajudar a gerenciar grupos de VC. Permite que você preencha um grupo com postagens pendentes. A principal funcionalidade no momento é carregar postagens pendentes no grupo VK com fotos ou vídeos, hashtags, pesquisas, localização geográfica e a capacidade de configurar o horário da publicação e o número de postagens por dia. No momento, o aplicativo possui um formulário.

Com o crescimento de sua funcionalidade, muitos codebehind acumularam, o que começou a confundir o fato de que o formulário ficou muito carregado e continha tudo em si. Ao planejar o desenvolvimento do projeto, eu não conseguia imaginar o que poderia ser continuado no mesmo espírito, e agora chegou o momento em que a tarefa principal não era refinar o funcional, mas refatorar. E comecei a procurar uma solução que ajudasse a otimizar e dividir o código e, em geral, melhorar a arquitetura do aplicativo, para que fosse mais agradável trabalhar com ele.

Fonte

Solução de refatoração


A solução foi implementar o padrão MVP. Como base, peguei o artigo Recursos de Implementação do MVP para Windows Forms .

O artigo analisa um exemplo extenso de uma aplicação simples com 3 formas: 2 básica e 1 modal. O artigo discute uma abordagem muito avançada:

  • além de ApplicationController e IoC, o adaptador também é usado lá, o que permite o uso de IoCs diferentes;
  • 3 tipos de formulários: com parâmetros, sem parâmetros e modal;
  • O princípio DIP é amplamente aplicado.

No meu projeto, uso apenas um formulário sem argumentos, abandonei o adaptador (seguindo o princípio YAGNI), pois o IoC Lightinject é suficiente para mim e, em menor grau, uso o DIP para simplificar o projeto.

Implementação MVP


MVP (Model-View-Presenter) é um padrão de design projetado para a conveniência de separar a lógica de negócios da maneira como é exibida. Você pode ler mais sobre a teoria no artigo acima. Vou descrever os componentes na minha implementação:

  • Modelo é uma estrutura de dados transferida entre o View e o Presenter e contém dados para exibição e execução da lógica. No meu caso, o modelo é Configurações. Quando o projeto é iniciado, as Configurações são carregadas no MainFormView e, quando o carregamento é iniciado, o MainFormView verifica e passa Settigns para o Presenter para que o Presenter execute a lógica de lado.
  • View é um formulário no qual os dados são exibidos para o usuário. No meu caso, esses são os dados do modelo Configurações e o View também fornece eventos para o Presenter associar o View ao BL.

MainFormView implementa uma interface IView comum a todos os View

    public interface IView
    {
        void Show();

        void Close();
    }

bem como uma interface privada IMainFormView específica apenas para esta visualização. No começo, pensei em abandoná-lo, mas se você associar o Presenter diretamente ao formulário, ao trabalhar com essa Visualização, todo o conjunto de métodos específicos para o Form estará disponível, o que não é 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;
    }

Outra inovação do MVP é que o método Show foi substituído no formulário e ApplicationContext é passado para o formulário através do construtor, para que, ao alternar de um formulário para outro e fechar, o formulário principal seja reatribuído.

        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 é uma classe que encapsula View, Services e lógica de negócios (BL) dentro de si, com a ajuda da qual organiza a interação entre View e Services. O BL é implementado principalmente nos manipuladores de eventos do View. Diferentemente do CodeBehind usado anteriormente, no MVP, os manipuladores de eventos que executam BL são exibidos no Presenter e, por simplicidade, os eventos no View são exibidos na forma de Ação sem argumentos. Os manipuladores recebem todos os dados necessários para execução através do modelo obtido do formulário através do método público.

O Presenter contém o método Run, chamado pelo ApplicationController e que inicia o formulário:

    public interface IPresenter
    {
        void Run();
    }

ApplicationController - um único ponto de controle e execução de todo o aplicativo. Encapsula toda a lógica em si: IoC, Apresentadores, Exibir, Serviços.

O gerenciamento é feito pelo método Run, que chama o apresentador correspondente. Todos os apresentadores são conectados um ao outro através do ApplicationController, que o apresentador recebe no construtor. Assim, o Presenter pode chamar outro Presenter chamando o método Run, que chama internamente o IoC Container para obter o Presenter desejado e iniciá-lo.

    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();
        }
    }

O contêiner de IoC é um agregador de todas as "dependências" usadas na lógica do aplicativo. Contém:

  • Exibir construtores
  • Apresentadores de construtores
  • instâncias de serviço
  • contexto de aplicação
  • ApplicationController

Todas as dependências são adicionadas ao contêiner durante a inicialização. Isso pode ser visto no arquivo 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 a IoC, usei o componente Lightinject, que deve ser instalado através do NPM antes do uso.

Portanto, o contêiner pode conter os construtores de objetos e os próprios objetos, como é feito com Settings e VKGroupHelperWorker (cliente da API da VK), formando o conjunto de todos os recursos de aplicativos usados. Um recurso útil do contêiner é que todos esses recursos incorporados e classes podem ser obtidos através dos argumentos do construtor. Por exemplo,
ApplicationController, IMainFormView, VKGroupHelperWorker - dependências implementadas anteriormente, que podem ser construtores de objetos e instâncias. Se uma instância foi implementada, todos os objetos gerados funcionarão com a mesma instância, o que permite que você se livre do padrão singleton, se usado.

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();
        }

A implementação do MVP me permitiu:

  • livrar-me parcialmente do Singleton, que eu costumava trabalhar com as configurações do aplicativo;
  • Separe BL do View, melhorando assim a separação de código (SRP)
  • para desenvolver uma abordagem para expandir ainda mais o aplicativo sem sobrecarregar a visualização.

Mais informações sobre o que foi feito podem ser encontradas no repositório do projeto .

All Articles