Добрый день!

В этой статье я расскажу о том как я внедрял паттерн MVP в своём Windows Forms приложении и опишу практические ситуации и особенности использования IoC и ApplicationController. Переход от codebehind к MVP мне позволил:

  • улучшить читатемость за счёт лучшего разделения кода (SRP) — отделить BL от View;
  • выработать методику дальнейшего расширения функциональности приложения;
  • избавиться от singleton, который я использовал для работы с настройками приложения.

О приложении


Приложение для помощи в ведении групп ВК. Позволяет наполнять группу отложенными постами. Основная функциональность на данный момент — загрузка отложенных постов в группу ВК с картинками или видео, хэштэгами, опросами, геопозицией и возможностью конфигурировать время публикации и количество постов по дням. На данный момент у приложения одна форма.

С ростом его функциональности накопилось достаточно много codebehind, который стал смущать тем, что форма стала очень загружена и содержала всё в себе. Планируя дальнейшее развитие проекта я не мог представлять, что дальше можно продолжать в том же духе и вот пришёл момент, когда главной задачей стало не наращивание функционала, а рефакторинг. И я стал искать решение, которое бы помогло оптимизировать и разделить код и в целом улучшить архитектуру приложения, чтобы с ним было приятнее работать.

Исходный код

Решение по рефакторингу


Решение было — внедрение паттерна MVP. За основу я взял статью Особенности реализации MVP для Windows Forms.

В статье разбирается расширенный пример простого приложения с 3мя формами: 2мя основными и 1й модальной. В статье рассматривается очень расширенный подход:

  • помимо ApplicationController и IoC там используется ещё и Adapter, позволяющий использовать разные IoC;
  • 3 вида форм: с параметрами, без параметров и модальная;
  • широко применяется принцип DIP.

В своём проекте я использую только одну форму без аргументов, отказался от адаптера (следуя принципу YAGNI), так как мне достаточно будет IoC Lightinject и в меньшей мере применяю DIP, чтобы упростить проект.

Реализация MVP


MVP (Model-View-Presenter) — паттерн проектирования, придуманный для удобства разделения бизнес-логики от способа её отображения. Подробнее о теории можно прочитать в статье выше. Опишу составные части в моей реализации:

  • Model — это структура данных передаваемая между View и Presenter и содержит данные как для отображения так и для исполнения логики. В моём случае модель — это Settings. При старте проекта Settings загружаются во MainFormView, а при запуске загрузки MainFormView проверяет и передаёт Settigns в Presenter, для того чтобы Presenter выполнил логику на своей стороне.
  • View — это форма, в которой отображаются данные для пользователя. В моем случае это данные модели Settings, а так же View предоставляет события, для того, чтобы Presenter связал View с BL.

MainFormView реализует общий интерфейс IView, характерный для всех View

    public interface IView
    {
        void Show();

        void Close();
    }

а так же частный интерфейс IMainFormView, характерный только для данного View. В начале я думал от него отказаться, но если связать Presenter непосредственно с формой, то при работе с таким View, будет доступен весь набор методов, характерных для Form, что не удобно.

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

Ещё одно нововведение MVP в том, что у формы заменён метод Show и через конструктор в форму передаётся ApplicationContext, таким образом, чтобы при переключении от формы к форме и закрытии — переназначалась главная форма.

        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 — это класс, который инкапсулирует в себе View, Services и бизнес-логику (BL), с помощью которой организует взаимодествие между View и Services. BL реализована в основном в обработчиках событий View. В отличии от ранее используемого CodeBehind, в MVP обработчики событий, выполняющих BL выведены в Presenter, а так же для простоты события у View выведены в виде Action без аргументов. Все необходимые данные для выполнения обработчики получают через модель, полученную из формы через публичный метод.

Presenter содержит метод Run, который вызывается ApplicationController-ом и который запускает форму:

    public interface IPresenter
    {
        void Run();
    }

ApplicationController — единая точка управления и выполнения всего приложения. Инкапсулирует в себе всю логику: IoC, Presenters, View, Services.

Управление происходит через метод Run, который вызывает соответствующий Presenter. Все Presenter-ы соединены друг с другом через ApplicationController, который Presenter получают в конструкторе. Таким образом Presenter может вызвать другой Presenter вызвав метод Run, который внутри себя обращается к IoC Container для получения нужного Presenter и его запуска.

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

IoC container — это агрегатор всех «зависимостей» используемых в логике работы приложение. Он содержит в себе:

  • конструкторы View
  • конструкторы Presenters
  • инстансы сервисов
  • контекст приложения
  • ApplicationController

Все зависимости добавляются в контейнер во время запуска, это можно видеть в файле 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>();
        }

Для IoC я использовал компонент Lightinject, который перед использованием необходимо установить через NPM.

Таким образом контейнер может содержать как конструкторы объектов, так и сами объекты, как это сделано с Settings и VKGroupHelperWorker (клиент ВК API), образуя множество всех используемых ресурсов приложения. Полезной особенностью контейнера является то, что все эти внедрённые ресурсы, классы могут получить через аргументы конструктора. Например
ApplicationController, IMainFormView, VKGroupHelperWorker — ранее внедрённые зависимости, которые могут быть как конструкторами объектов так и инстансами. В случае если был внедрён инстанс, то все образуемые объекты будут работать с одним и тем же инстансом, что позволяет избавиться от паттерна синглтон, если он использовался.

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

Внедрение MVP мне позволило:

  • частично избавиться от Singleton, который я использовал для работы с настройками приложения;
  • отделить BL от View, тем самым улучшив разделение кода (SRP);
  • выработать подход к дальнейшему расширению приложения, не загромождая View.

Подробнее о том что было сделано можно посмотреть в репозитории проекта.