Страницы

Поиск по вопросам

четверг, 13 февраля 2020 г.

Как перевести понятия MVP/MVC в термины WinForms?

#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; // управляем моделью } } }

Комментариев нет:

Отправить комментарий