Особливості реалізації 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.

Ну і накрутили… Як тепер ЦЕ використати?

Тепер, коли каркас готовий, додавання нової форми зводиться до таких кроків:

  1. Пишемо інтерфейс Подання, інтерфейс Моделі (якщо потрібно).
  2. Реалізуємо Представника, принагідно вирішивши, чи будемо в нього передавати якісь дані чи модель.
  3. [Опціонально] Пишемо тести для Представника, переконуємось, що все нормально.
  4. [Опціонально] Реалізуємо Модель та тести для неї.
  5. Накидаємо формочки та реалізуємо інтерфейсУявлення.

Зміна IoC-контейнера на вашого улюбленого відбувається шляхом реалізації простого інтерфейсу IContainer класом-адаптером.

Забрати демонстраційний проект можна з Github (для складання необхідно викачати Nuget-залежності).

Насамкінець хочу сказати, що все, що тут було описано — лише один із безлічі способів реалізації MVP, який, як мені здається, має непогану гнучкість і тестування.

Бонус. Хочу ще крутіше, більше, складніше!