Хабрахабр

Реализация MVP на основе ApplicationController и IoC в WinForms приложении

Добрый день!

В этой статье я расскажу о том как я внедрял паттерн 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.

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

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»