Особливості реалізації MVP для Windows Forms, SavePearlHarbor
Ще одна копія хабора
Особливості реалізації MVP для Windows Forms
Постановка задачі

Трохи теорії
MVP, як і його батько, MVC (Model-View-Controller) придуманий для зручності розподілу бізнес-логіки від способу її відображення.
Далі розглядатиметься модифікація Passive View. Опишемо основні риси: - інтерфейс Подання (IView), який надає якийсь контракт для відображення даних; — Уявлення — конкретна реалізація IView, яка вміє відображати саму себе в конкретному інтерфейсі (чи то Windows Forms, WPF або навіть консоль) і нічого не знає про те, хто її керує. У разі це форми; - Модель - надає деяку бізнес-логіку (приклади: доступ до бази даних, репозиторії, сервіси). Може бути представлена у вигляді класу або знову ж таки, інтерфейсу та реалізації; — Представник містить посилання на Подання через інтерфейс (IView), керує ним, підписується на його події, здійснює просту валідацію (перевірку) введених даних; також містить посилання на модель або її інтерфейс, передаючи в неї дані з View і запитуючи оновлення.
Які плюси нам дає мала пов'язаність класів (використання інтерфейсів, подій)? 1. Дозволяє відносно вільно змінювати логіку будь-якого компонента, не ламаючи решту. 2. Великі можливості при unit-тестуванні. Шанувальники TDD мають бути у захваті. Почнемо!
Як організувати проекти?
Умовимося, що рішення складатиметься з 4-х проектів: - DomainModel - містить сервіси і всілякі репозиторії, одним словом - модель; - Presentation - містить логіку програми, яка не залежить від візуального подання, тобто. всі Представники, інтерфейси Уявлень та іншібазові класи; - UI - Windows Forms додаток, містить тільки форми (реалізацію інтерфейсів Уявлень) і логіку запуску; - Tests - unit-тести.
Що писати в Main()?
Стандартна реалізація запуску Windows Forms програми виглядає так:
Але ми домовилися, що Представники керуватимуть Представленнями, тому хотілося б, щоб код виглядав якось так:
Спробуємо реалізувати перший екран:
Створити форму і реалізувати в ній інтерфейс ILoginView не важко, як і написати реалізацію ILoginService. Слід лише відзначити одну особливість:
Це заклинання дозволить нашому додатку запуститися, відобразити форму, а після закриття форми коректно завершити програму. Але до цього ми ще повернемось.
А випробування будуть?
З моменту написання представника (LoginPresenter), з'являється можливість відразу ж його від-unit-тестувати, не реалізуючи ні форми, ні послуги. Для написання тестів я використовував бібліотеки NUnit та NSubstitute (бібліотека створення класів-заглушок за їхніми інтерфейсами, mock).
Тести досить дурні, як поки й сам додаток. Але так чи інакше вони успішно пройдені.
Хто і як запустить другий екран із параметром?
Але ми домовилися, що представники нічого не знають про уявлення, крім їх інтерфейсів. Що ж робити? На допомогу приходить патерн Application Controller (реалізований спрощено), всередині якого міститься IoC-контейнер, який знає, як за інтерфейсом отримати об'єкт реалізації. Контролер передається кожному Представнику параметром конструктора (знову DI) і реалізує приблизно такі методи:
Після невеликого рефакторингу запуск програми став виглядати так:
Не можна просто так взяти та закрити форму…
Один із підводних каменівпов'язаний із рядком View.Close() , після якого закривалася перша форма, а разом із нею і додаток. Справа в тому, що Application.Run(Form) запускає стандартний цикл обробки повідомлень Windows і розглядає передану форму як головну форму програми. Це виявляється у тому, що програма вішає ExitThread на подію Form.Closed , що викликає закриття програми після закриття форми. Обійти цю проблему можна декількома способами, один з них - використовувати інший варіант методу: Application.Run(ApplicationContext), потім вчасно замінюючи властивість ApplicationContext.MainForm. Передача контексту формам реалізована за допомогою Контролера додатка, в якому реєструється об'єкт (instance) ApplicationContext і потім підставляється в конструктор форми (знову DI) під час запуску Представника. Методи відображення перших двох екранів тепер мають такий вигляд:
Модальне вікно
Реалізація модального вікна не викликає труднощів. На кнопці «Змінити ім'я» виконується Controller.Run (user). Єдина відмінність цієї форми від інших - вона не головна, тому формі для показу не потрібно ApplicationContext:
Якщо потрібно відкрити звичайне вікно, метод взагалі не потрібно визначати, оскільки він вже реалізований у класі Form.
Ну і накрутили… Як тепер ЦЕ використати?
Тепер, коли каркас готовий, додавання нової форми зводиться до таких кроків:
- Пишемо інтерфейс Подання, інтерфейс Моделі (якщо потрібно).
- Реалізуємо Представника, принагідно вирішивши, чи будемо в нього передавати якісь дані чи модель.
- [Опціонально] Пишемо тести для Представника, переконуємось, що все нормально.
- [Опціонально] Реалізуємо Модель та тести для неї.
- Накидаємо формочки та реалізуємо інтерфейсУявлення.
Зміна IoC-контейнера на вашого улюбленого відбувається шляхом реалізації простого інтерфейсу IContainer класом-адаптером.
Забрати демонстраційний проект можна з Github (для складання необхідно викачати Nuget-залежності).
Насамкінець хочу сказати, що все, що тут було описано — лише один із безлічі способів реалізації MVP, який, як мені здається, має непогану гнучкість і тестування.