MVP – Model View Presenter – паттерн организации PL (presentation layer – уровень представления).
MVP применяется при создании десктопных интерфейсов. Выделяют три комопнента: есть модель – группа классов, которые отдают данные или получают команды, представление – форма обладающая состоянием и некоторым поведением. Презентер создают для отделения бизнес-логики от деталей GUI-фреймворка. В отличие от MVC в MVP представление определяет презентер, а не наоборот.
MVP обычно строится вокруг существующих GUI-фреймворков. На практике существуют две принципиально различные различные реализации паттерна – Supervising Controller и Passive View.
В первом случае логика помещается в обработчики событий button_click, а сами обработчики помещаются в отдельный класс. Для полной изоляции презентера от деталей представления надо писать достаточно много врапперов\адаптеров.
Во втором случае создается пара интерфейсов для общения между представлением и презентером. При совершении какого-либо действия представление напрямую обращается к презентеру, тот выполняет некоторый код и вызывает установку свойств представления. Passive View способствует максимальному перемещению кода в в презентер, что облегчает тестирование.
Создание MVP в WinForms с помощью Unity.
Создаем новое winforms приложение и грохаем оттуда форму.
Для начала определим служебные интерфейсы.
//Маркерный интерфейс представления
public interface IView
{
}
//Интрефейс презентера
public interface IPresenter<T> where T:IView
{
T View { get; set; }
}
//Базовый класс для презентера
public abstract class BasePresenter<T> :IPresenter<T> where T:IView
{
public T View { get; set; }
}
Класс “бизнес-логики” будем использовать тот же, что и в предыдущем посте.
public interface ISayHelloService
{
string SayHello(string name);
}
public class SayHelloSerivce : ISayHelloService
{
public string SayHello(string name)
{
return "Привет, " + name;
}
}
Теперь определим рабочие интерфейсы.
public interface ISayHelloPresenter : IPresenter<ISayHelloView>
{
void SayHello();
}
public interface ISayHelloView: IView
{
string GetInputText();
void SetOutputText(string text);
}
Для перезнтера код будет тривиальный.
public class SayHelloPresenter : BasePresenter<ISayHelloView>, ISayHelloPresenter
{
ISayHelloService _service;
public SayHelloPresenter(ISayHelloService service)
{
this._service = service;
}
public void SayHello()
{
this.View.SetOutputText(_service.SayHello(this.View.GetInputText()));
}
}
Теперь нарисуем простую формочку:
В коде напишем следеющее:
public partial class Form1 : Form, ISayHelloView
{
ISayHelloPresenter _presenter;
public Form1(ISayHelloPresenter presenter)
{
InitializeComponent();
_presenter = presenter;
//Циклическая зависимость
_presenter.View = this;
}
public string GetInputText()
{
return textBox1.Text;
}
public void SetOutputText(string text)
{
textBox2.Text = text;
}
private void button1_Click(object sender, EventArgs e)
{
_presenter.SayHello();
}
}
Циклическая зависимость в Passive View не позволяет с помощью контейнера пропихнуть все зависимости. Поэтому передача презентеру ссылки на представление делается в коде view.
Теперь чтобы увязать это вместе надо создать и сконфигурировать контейнер. Сделаем это прямо в Program.cs.
static void Main()
{
var container = new UnityContainer();
container
.RegisterType<ISayHelloService, SayHelloSerivce>()
.RegisterType<ISayHelloPresenter, SayHelloPresenter>()
.RegisterType<ISayHelloView, Form1>()
;
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run((Form)container.Resolve<ISayHelloView>());
}
Вот и все.
11 коммент.:
Почему бы вам не отзеркалить посты на Хабр? И еше на blogs.gotdotnet.ru например?
Гладко было на бумаге, да забыли про овраги. На практике, когда необходимо связать несколько представлений в единый агрегат, стройная концепция рушится под натиском противных мелочей, про которые в таких обзорных статьях забывают или намеренно о них умалчивают.
Например (двухзвенка).
В общем случае Presenter ничего не должен знать о реализации конкретного сервиса, с которым он общается через интерфейс этого самого сервиса.
Если конкретный сервис использует какой-то физически или логически изолированный ресурс, будь то DbConnection или контекст модели Linq2SQL или Entity Framework, то очень скоро мы нарвемся на ограничения, накладываемые этой изоляцией. Например, в Linq2SQL и EF сущность, возвращенная одним сервисом, не может быть связана с сущностью, возвращенную другим сервисом без явного отсоединения её от первого контекста и присоединения ко второму.
Например, мы редактируем накладную заказа, пользуемся справочником подбора номенклатуры и хотим передать ссылку на выбранную в форме подбора позицию в спецификацию накладной. Решение «в лоб» не работает, так как сущности созданы в разных контекстах и для того, чтобы свзяать номенклатуру с позицией спецификации, мы должны отдетачить номенклатуру от контекста формы выбора. Это бесит и приходится задействовать свои велосипеды, как то: импортировать сущность из другого контекста сериализацией и восстановлением, или клонированием, или вообще забить на объекты и гонять между контекстами только их идентификаторы. Нос вылез, хвост увяз.
В ADO.NET обязательно возникнут проблемы с транзакционным обновлением изменений, накопленных обоими изолированными сервисами, которые в общем случае нельзя решить без использования распределённых транзакций.
Другой популярный юз-кейс: компоновка сложных форм из элементарных представлений. Если каждая форма будет связана со своим презентером, а тот в свою очередь со своим сервисом, то как обеспечить надежное взамодействие представлений, а тем более целостность данных, если сервисы физически изолированы?
Можно, конечно, отказаться от физической изоляции сервисов и настроить Container так, чтобы они использовали один общий ресурс – например, единый экземпляр ObjectContext, но тогда вообще туши свет. Пользователь открыл форму, чето там поправил, попытался сохранить, не получилось, форму закрыл, ошибочные изменения остались в контексте и как их откатывать – не понятно.
Та же проблема, КМК, актуальна и для случаев, когда несколько презентеров шарят общий сервис, или несколько представлений шарят общий презентер
>Почему бы вам не отзеркалить посты на Хабр? И еше на blogs.gotdotnet.ru например?
Ну на хабре у меня нету аккаунта, а насчет другого сервиса блогов я уже думаю.
>Гладко было на бумаге, да забыли про овраги. На практике, когда необходимо связать несколько представлений в единый агрегат, стройная концепция рушится под натиском противных мелочей, про которые в таких обзорных статьях забывают или намеренно о них умалчивают.
Все проблемы как всегда решаются введением дополнительного слоя абстракции.
Чтобы победить Linq2SQL\EF надо отказаться от использования объектов, возвращаемых ими, вернее отказаться от change tracking.
Надо написать свою прослойку из сервисов, которые будут получать некоторые данные на входе, а уже внутри себя оперировать контекстами.
Это конечно не тривиальная задача, но все описанные проблемы исчезают.
А когда у вас работа с контекстами отвязана от деталей отображения, тогда уже совсем несложно прикрутить транзакционность. В том числе средствами механизма aop в unity.
Чтобы обеспечить передачу измененных данных между предствалениями при наличии слоя, опписанного выше, достаточно в этом слое сделать событие, срабатывающее на изменение данных (как INotifyPropertyChanged), тогда все контроллеры смогут перечитывать данные при необходимости.
А вот кстати при такой архитектуре, как я описал в посте, не должно быть ситуаций когда несколько представлений шарят общий презентер.
Хотя если сделать в презентере список View, то уже легче такое провернуть.
А это точно Passive View ?
Ведь оно знает о презентере.
private void button1_Click(object sender, EventArgs e)
{
_presenter.SayHello();
}
Passive View обозначает что само View не изменяет свое состояние, только по команде контроллера.
В общем не совсем понятно, почему View должно знать о Presenter ?
Потому что создаетс в первую очередь View, оно каким-то образом должно создавать презентера для себя. Значит как минимум требуется знать интерфейс презентера.
Что-то мешает создать презентера и вернуть View ?
View должно быть отделено от логики. Зачем вводится лишняя зависимость от Presenter`а ?
Или я как-то не так понимаю MVP ?
Не-не-не. Не View должно быть отделено от логики, а логика от View. Тестировать ведь презентер надо, а не view.
При любом расслоении системы нижние слои не должны зависеть от особенностей верхних слоев.
Отправить комментарий