Добрый день!
В этой статье я расскажу о том как я внедрял паттерн MVP в своём Windows Forms приложении и опишу практические ситуации и особенности использования IoC и ApplicationController. Переход от codebehind к MVP мне позволил:
Приложение для помощи в ведении групп ВК. Позволяет наполнять группу отложенными постами. Основная функциональность на данный момент — загрузка отложенных постов в группу ВК с картинками или видео, хэштэгами, опросами, геопозицией и возможностью конфигурировать время публикации и количество постов по дням. На данный момент у приложения одна форма.
С ростом его функциональности накопилось достаточно много codebehind, который стал смущать тем, что форма стала очень загружена и содержала всё в себе. Планируя дальнейшее развитие проекта я не мог представлять, что дальше можно продолжать в том же духе и вот пришёл момент, когда главной задачей стало не наращивание функционала, а рефакторинг. И я стал искать решение, которое бы помогло оптимизировать и разделить код и в целом улучшить архитектуру приложения, чтобы с ним было приятнее работать.
Исходный код
Решение было — внедрение паттерна MVP. За основу я взял статью Особенности реализации MVP для Windows Forms.
В статье разбирается расширенный пример простого приложения с 3мя формами: 2мя основными и 1й модальной. В статье рассматривается очень расширенный подход:
В своём проекте я использую только одну форму без аргументов, отказался от адаптера (следуя принципу YAGNI), так как мне достаточно будет IoC Lightinject и в меньшей мере применяю DIP, чтобы упростить проект.
MVP (Model-View-Presenter) — паттерн проектирования, придуманный для удобства разделения бизнес-логики от способа её отображения. Подробнее о теории можно прочитать в статье выше. Опишу составные части в моей реализации:
MainFormView реализует общий интерфейс IView, характерный для всех View
а так же частный интерфейс IMainFormView, характерный только для данного View. В начале я думал от него отказаться, но если связать Presenter непосредственно с формой, то при работе с таким View, будет доступен весь набор методов, характерных для Form, что не удобно.
Ещё одно нововведение MVP в том, что у формы заменён метод Show и через конструктор в форму передаётся ApplicationContext, таким образом, чтобы при переключении от формы к форме и закрытии — переназначалась главная форма.
— Presenter — это класс, который инкапсулирует в себе View, Services и бизнес-логику (BL), с помощью которой организует взаимодествие между View и Services. BL реализована в основном в обработчиках событий View. В отличии от ранее используемого CodeBehind, в MVP обработчики событий, выполняющих BL выведены в Presenter, а так же для простоты события у View выведены в виде Action без аргументов. Все необходимые данные для выполнения обработчики получают через модель, полученную из формы через публичный метод.
Presenter содержит метод Run, который вызывается ApplicationController-ом и который запускает форму:
ApplicationController — единая точка управления и выполнения всего приложения. Инкапсулирует в себе всю логику: IoC, Presenters, View, Services.
Управление происходит через метод Run, который вызывает соответствующий Presenter. Все Presenter-ы соединены друг с другом через ApplicationController, который Presenter получают в конструкторе. Таким образом Presenter может вызвать другой Presenter вызвав метод Run, который внутри себя обращается к IoC Container для получения нужного Presenter и его запуска.
IoC container — это агрегатор всех «зависимостей» используемых в логике работы приложение. Он содержит в себе:
Все зависимости добавляются в контейнер во время запуска, это можно видеть в файле Program.cs:
Для IoC я использовал компонент Lightinject, который перед использованием необходимо установить через NPM.
Таким образом контейнер может содержать как конструкторы объектов, так и сами объекты, как это сделано с Settings и VKGroupHelperWorker (клиент ВК API), образуя множество всех используемых ресурсов приложения. Полезной особенностью контейнера является то, что все эти внедрённые ресурсы, классы могут получить через аргументы конструктора. Например
ApplicationController, IMainFormView, VKGroupHelperWorker — ранее внедрённые зависимости, которые могут быть как конструкторами объектов так и инстансами. В случае если был внедрён инстанс, то все образуемые объекты будут работать с одним и тем же инстансом, что позволяет избавиться от паттерна синглтон, если он использовался.
Внедрение MVP мне позволило:
Подробнее о том что было сделано можно посмотреть в репозитории проекта.
В этой статье я расскажу о том как я внедрял паттерн 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.
Подробнее о том что было сделано можно посмотреть в репозитории проекта.