#c_sharp #winforms #шаблоны_проектирования
В описании паттерна MVP (Model-View-Presenter) сказано следующее:
Model (Модель) — предоставляет данные для пользовательского интерфейса.
View (Представление) — реализует отображение данных (Модели) и маршрутизацию пользовательских
команд или событий Presenter-у.
Presenter — управляет Моделью и Представлением. Например извлекает данные из Модели
и форматирует их для отображения в Представлении.
Что в WinForms приложениях соответствует View и Presenter?
Ответы
Ответ 1
Стандартная лапша - в code-behind форм в обработчиках событий производится запросы
к модели, какая-то работа и визуализация результата.
В mvp модель остается моделью, то есть это набор бизнес-логики, а код в форме делится
на View и Presenter
View - это формы, code-behind этих форм, где производится работа по визуальной составляющей,
а также туда же сводятся подписки на события контролов
Presenter - туда вынесен рабочий код, который не являются частью визуализации. View
содержит ссылку на presenter и вызывает его методы. А presenter содержит ссылку на
View через интерфейс
То есть View в обработчиках событий контролов делегирует presenter-у "сделай это",
а presenter содержит управляющий код, который опросит модель, получит результат и потом
дернет View и скажет ему "отрисуй такое то"
Таким образом визуализирующий и управляющий код разделяются.
По просьбе @Stack привожу пример. Псевдошарпокод.
Имеем простую модель (она же бизнес логика). Просто читает файл планет
class PlanetReader{
public IList ReadPlanetsFromFile(){
return File.ReadLines(...)
}
}
Далее интерфейс представления
public interface IMainView{
void ShowPlanets(IList planets);
}
ну и презентер
public class Presenter{
private IMainView _view;
public Presenter(IMainView view){
_view=view;
}
public void LoadPlanets(){
var planets=new PlanetReader().ReadPlanetsFromFile();
_view.ShowPlanets(planets);
}
}
и представление winforms
public partial class MainForm : Form, IMainView{
private Presenter _presenter;
public MainForm(){
InitializeComponent();
button.Click+=OnButtonClick;
_presenter=new Presenter(this);
}
public void OnButtonClick(...){
_presenter.LoadPlanets();
}
public void ShowPlanets(IList planets){
TextBox.Text=string.Join("\n", planets);
}
}
для WPF код в общем то идентичен Winforms кроме класса родителя и сигнатур обработчиков
событий (хотя лучше все таки MVVM)
public partial class MainWindow : Window, IMainView
для консоли
class ConsoleApp : IMainView {
private Presenter _presenter;
public ConsoleApp(){
_presenter=new Presenter(this);
}
public void Run(){
Console.WriteLine("press any key to load planets');
Console.ReadKey();
_presenter.LoadPlanets();
}
public void ShowPlanets(IList planets){
Console.WritrLine(string.Join("\n", planets));
}
}
за IMainView может быть скрыто что-угодно - от простой формы winforms до самописной
системы отрисовки на удаленной машине.
Плюшки:
"Логика интерфейса" по факту делится на "презентационную логику" и собственно "логику
визуализации".
В "презентационную логику" входит решение, что делать по нажатию на кнопку и что
делать с результатом - по сигналу от UI работает с моделью и просит потом "логику визуализации"
отобразить результат.
В "логику визуализации" входит отображение того, что просят - контролы, события,
установки полей у контролов, кобминирование контролов и так далее.
Презентер не имеет прямой ссылки на представление, поэтому детали отображения на
UI инкапсулированы (опять же чистота кода, вся сложность спрятана) и возможность заменять
представление на другое.
Презентер не имеет прямой ссылки на представление, а значит вместо представлениям
можно подсунуть заглушку и протестировать презентер. Тестировать же логику интерфейса
без такого разделения крайне сложно (нужно тыкать мышью руками или автоматикой и считывать
как то результаты с экрана)
Ответ 2
Решил добавить еще один ответ, потому что размер комментариев ограничен, а у @Stack
все перемешалось в голове и мой пост отвечает не на вопрос "как", а на "где и почему?"
Цель MVP не сделать возможность подмены UI (для этого просто нужно менять UI), а
разделить ответственности и обеспечить тестируемость.
Допустим нам нужно написать приложение под разные абстрактные платформы.
у нас разные UI на разных платформах.
варианты:
Делаем проект-ядро, где будет логика приложения и создаем по проекту
UI на каждую платформу и при компиляции оно разберется
Делаем UI на кроссплатформенных контролах
прячем за фасадом (паттерн такой) любые реализации UI приводя их к обобщенному виду
фасада. Банально делается класс/интерфейс за которым прячется реальный UI. но это
не MVP, а просто сокрытие сложности и зависимостей
Для подобной же цели служит паттерн "мост". Отличие от варианта с "фасадом", что,
вместо сокрытия реализации за фасадом, более ярко выражена связующая шина между приложением
и графическим приложением и независимость реализации обоих частей
А где же MVP?
Когда задача подмены UI решена, то нужно реализовать эти самые UI.
Допустим одна из наших UI на WinForms.
В силу особенностей Visual Studio мы получаем на выходе в Code-Behind основной формы
кучу методов (евент-хендлеры и кучу приватных методов реализующих логику интерфейса).
Мало того, что это лапша кода, так еще все это невозможно тестировать - для тестирования
нужно тыкать на кнопки и отслеживать изменение GUI, что сложно.
Возникает желание навести порядок. Это можно сделать 101 способом (MVC/MVP/MVVM/MVVMC/свой
вариант), просто некоторые в конкретном случае будут менее удобны чем другие, а ведь
еще хочется не терять визуальный дизайнер студии. И паттерн MVP хорошо подходит для
UI, где нет нормальных биндингов, потому что прост и прячет кучу визуализационного
кода за интерфейсом. Это и Winforms, и я вот под андроид тоже использую MVP.
вообще то это очевидное решение - разделить godobject на классы и удалить зависимости
от реализаций, спрятав за интерфейсами. Типичные варианты решений и обозвали паттернами
Разделив визуализацию и логику мы получаем возможность тестировать ту логику, что
вошла в презентер (а туда входит вся управляющая логика взаимодействия с моделью и
управления визуализацией). Тестировать просто, ведь презентер просто класс с зависимостями.
Если же еще хочется тестировать кнопочки, то winium в руки, но тестировать UI всегда сложно
"как в вашем примере тестировать Presenter. из-за new в методе LoadPlanets() { var
planets=new PlanetReader()...} его невозможно тестировать отдельно от конкретной модели"
@Stack
Для этого используется то, что называется "принцип инверсии зависимостей". Еще одно
очевидное решение, имеющее свое название
Делаем интерфейс IPlanetReader и выносим его в конструктор. Или же используем ServiceLocator.
Или же IoC-контейнеры. Или любой другой вариант. MVP всего лишь вариант разделения
на ответственности "где за что отвечает", а реализация ответственностей уже не входит
в него и там полная свобода действий
тестирование UI это проверка того как смотрятся шрифты -это вообще тестируется только
глазами. мы же говорим про автоматическое тестирование.
Тестирование через эмуляцию нажатий кнопок и считыванием информации с контролов.
недостатки: сложность считывания даннных (мало ли какие контролы), сложность изоляции
(для проверки, что пользователь существует нам нужно подсунуь базу где пользователь
есть), скорость работы (из-за того, что полностью работают все слои приложения). Не
выявляет функциональные ошибки, когда делает не то, но показывает правильный результат.
При тестировании презентера можно изолировать и подсунуть любые данные, проверить
что идет правильный порядок вызовов модели. Пишется намного проще, есть изоляция и
высокая скорость работы. В идеале должно быть больше различных тестов, но выбор необходимых
тестов определяется возможностями (тесты бизнесу не нужны. они нужны программисту и
за них платить не любят).
Лично мне куда проще тестировать презентер (он выявляет функциональные ошибки и за
них мне по шапке дают), а кривизна дизайна не фатальна (вся логика в презентере, в
представлении логики практически нет и сломать его сложнее, а вреда от этого минимум).
Ответ 3
Давайте представим, что надо создать справочник планет Солнечной системы в виде Console
Application. Список планет выводится в консоль. Для выбора планеты надо набрать ее
порядковый номер и нажать Enter.
В соответствии с описанием паттерна в своей программе надо определить три отдельных
класса. При этом Presenter не должен знать как работает View, который по команде Presenter'a,
выводит список планет в консоль, а введенный порядковый номер планеты возвращает Presenter'у.
Считается, что такое разделение и сокрытие деталей реализации позволяет сделать части
системы заменяемыми. Но это не так.
Представьте, что прошло время, и надо в справочник планет добавить миллионы астероидов,
сделать 3D визуализацию движения объектов и реализовать сенсорное управление.
Понятно, что если Model можно дополнить, то Presenter и View придется создавать с
нуля, т.к. UI-логика совершенно отличается от той, что была в консольном приложении.
При этом логика в Model (бизнес-логика) никак не меняется и не зависит от UI-логики.
Паттерн MVP является производным от MVC (Model-View-Controller), который был представлен
в 1979 г. и назывался Thing-Model-View-Editor. На сайте автора, профессора Реенскауг
Трюгве, видно на графике, что Controller и несколько View объединяются в Tool.
Графический интерфейс в Windows построен на основе оконной подсистемы - Windows Manager,
GDI и DWM. Есть десятки системных функций WinAPI, которые позволяют управлять окнами,
перехватывать события движения мыши и т.д.
В WinForms базовым классом для создания графического интерфейса является Control.
Если посмотреть на определение MVP, то получается, что раз Control позволяет отображать
данные и получать события мыши и т.д., то Control - это и есть View. Но это не так.
Дело в том, что Control является оберткой для оконной подсистемы Windows, взаимодействие
с которой происходит через вызовы функций Win API (их можно увидеть в исходниках Control).
Движение мыши, нажатия клавиш и другие системные события попадают в Message Queue
(это очередь системных событий, которые транслируются в вызовы соответствующих методов
Control; системные события можно перехватывать, если переопределить метод WndProc или
PreProcessMessage).
Получается, что View - это оконная подсистема Windows, а Presenter - это Control.
MVP : WinForms приложение
----------------------------------------------
Мodel : Объекты для работы с данными
----------------------------------------------
View : Оконная подсистема Windows
----------------------------------------------
Presenter : Производные от Control
Пример на C# с цитатами из определения паттерна MVP.
interface IModel {
int Data { get; set; }
}
class Presenter : TextBox {
IModel Model;
public Presenter(IModel m) {
this.Model = m;
var d = m.Data; // извлекаем данные из модели
var s = d.ToString(); // форматируем данные для отображения
this.Text = s; // передаем в Представление (оконная подсистема Windows).
}
// маршрутизация пользовательских команд
protected override void OnKeyDown(KeyEventArgs e) {
base.OnKeyDown(e);
if (this.Text.Contains("1")) { // логика
var d = int.Parse(this.Text); // форматируем данные
this.Model.Data = d; // управляем моделью
}
}
}