Паттерн MVP
129WPF --- Периферия WPF --- Паттерн MVP
Model View Presenter (MVP) - шаблон, который впервые появился в IBM, а затем использовался в Taligent в 1990-х. MVP является производным от MVC, при этом имеет несколько иной подход. В MVP представление не тесно связано с моделью, как это было в MVC. На следующей диаграмме показана структура MVP:

Как видно на этой диаграмме, Presenter занял место контроллера и отвечает за перемещение данных, введенных пользователем, а также за обновление представления при изменениях, которые происходят в модели. Presenter общается с представлением через интерфейс, который позволяет увеличить тестируемость, так как модель может быть заменена на специальный макет для модульных тестов. Также как и для MVC, давайте рассмотрим структуру MVP со стороны бизнес-логики приложения:

В данном случае структура MVP аналогична MVC в том плане, что код доступа к данным находится вне этой модели, а вся бизнес-логика приложения концентрируется в модели (эта схожесть подтверждает то, что MVP является производным от MVC, изменилась только логика представления).
Давайте рассмотрим пример приложения WPF использующего MVP, аналогичный тому, что был рассмотрен в предыдущих статьях (на примере приложения выбора проектов). Конечная структура приложения показана на следующей UML-диаграмме:

Представление использует класс ProjectsView который реализует интерфейс IProjectsView. ProjectsView и IProjectsView содержат все необходимые методы для обновления представления и связи с Presenter, который в свою очередь содержит ссылки на прототипы интерфейсов IProjectsView и IProjectsModel в виде переменных ProjectsPresenter.view и ProjectsPresenter.model. Такая конструкция обеспечивает взаимосвязь между моделью и представлением через Presenter.
Вот как это работает - если пользователь щелкнет на кнопке Update в пользовательском интерфейсе, сработает обработчик события IProjectView.ProjectUpdated, который вызовет метод ProjectsPresenter.view_ProjectUpdated() который обновит модель. При обновлении модели срабатывает обработчик события IProjectsModel.ProjectUpdated, который, через Presenter, обновляет графический интерфейс приложения.
Подобная модель обладает лучшей тестируемостью нежели модель MVC, т.к. мы перенесли логику построения представления в Presenter (в MVC представление строилось непосредственно из модели).
Давайте теперь соберем это приложение (оно будет работать аналогично рассмотренному ранее приложению MVC).
Модель
Создайте новый проект WPF в Visual Studio, добавьте ссылку на сборку ProjectBilling.DataAccess, которая рассматривалась в статье RAD и Monolithic. Данная сборка реализует код доступа к данным и во всех рассмотренных проектах остается одинаковой. Добавьте класс ProjectsModel со следующим содержимым:
using System;
using System.Collections.Generic;
using System.Linq;
using ProjectBilling.DataAccess;
namespace ProjectBilling.Business
{
#region ProjectsEventArgs
public class ProjectEventArgs : EventArgs
{
public Project Project { get; set; }
public ProjectEventArgs(Project project)
{
Project = project;
}
}
#endregion ProjectsEventArgs
public interface IProjectsModel
{
void UpdateProject(Project project);
IEnumerable<Project> GetProjects();
Project GetProject(int Id);
event EventHandler<ProjectEventArgs> ProjectUpdated;
}
public class ProjectsModel : IProjectsModel
{
private IEnumerable<Project> projects = null;
public event EventHandler<ProjectEventArgs>
ProjectUpdated = delegate { };
public ProjectsModel()
{
projects = new DataServiceStub().GetProjects();
}
public void UpdateProject(Project project)
{
ProjectUpdated(this,
new ProjectEventArgs(project));
}
public IEnumerable<Project> GetProjects()
{
return projects;
}
public Project GetProject(int Id)
{
return projects.Where(p => p.ID == Id)
.First() as Project;
}
}
}
Класс модели ProjectsModel реализует интерфейс IProjectsModel со следующими членами:
UpdateProject() - этот метод позволяет обновлять проект в текущем состоянии сеанса. Этот метод может быть расширен для поддержки обновления через состояния, но здесь, для простоты это не рассматривается.
GetProjects() - метод, возвращает список проектов.
GetProject() - возвращает проект по ID.
ProjectUpdated - событие, которое вызывается, когда проект был обновлен.
ProjectEventArgs - вспомогательный класс члены которого могут использоваться в обработчике события ProjectUpdated.
Представление
Представление реализовано в файлах ProjectView.xaml и ProjectView.xaml.cs:
<Window x:Class="ProjectBilling.UI.MVP.ProjectsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Проекты" MinHeight="250" Height="250"
MinWidth="250" Width="250">
<StackPanel>
<Label Content="Проекты:" Margin="5, 0, 5, 0" />
<ComboBox Name="projectsComboBox" Margin="5, 0, 5, 0"
SelectionChanged="projectsComboBox_SelectionChanged" />
<Label Content="Название:" Margin="5, 0, 5, 0" />
<TextBox Name="nameTextBox" Margin="5, 0, 5, 0"
IsEnabled="False" />
<Label Content="Сметная стоимость:" Margin="5, 0, 5, 0" />
<TextBox Name="estimatedTextBox" Margin="5, 0, 5, 0"
IsEnabled="False" />
<Label Content="Фактическая стоимость:" Margin="5, 0, 5, 0" />
<TextBox Name="actualTextBox" Margin="5, 0, 5, 0"
IsEnabled="False" />
<Button Name="updateButton" Content="Update"
Margin="5, 0, 5, 0" IsEnabled="False"
Click="updateButton_Click" />
</StackPanel>
</Window>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using ProjectBilling.Business;
using ProjectBilling.DataAccess;
namespace ProjectBilling.UI.MVP
{
#region IProjectsView
public interface IProjectsView
{
int NONE_SELECTED { get; }
int SelectedProjectId { get; }
void UpdateProject(Project project);
void LoadProjects(IEnumerable<Project> projects);
void UpdateDetails(Project project);
void EnableControls(bool isEnabled);
void SetEstimatedColor(Color? color);
event EventHandler<ProjectEventArgs> ProjectUpdated;
event EventHandler<ProjectEventArgs> DetailsUpdated;
event EventHandler SelectionChanged;
}
#endregion IProjectsView
#region ProjectsView
public partial class ProjectsView : Window, IProjectsView
{
#region Initialization
public int NONE_SELECTED { get { return -1; } }
public event EventHandler<ProjectEventArgs>
ProjectUpdated = delegate { };
public int SelectedProjectId { get; private set; }
public event EventHandler SelectionChanged
= delegate { };
public event EventHandler<ProjectEventArgs>
DetailsUpdated = delegate { };
public ProjectsView()
{
InitializeComponent();
SelectedProjectId = NONE_SELECTED;
}
#endregion Initialization
#region Event handlers
private void updateButton_Click(object sender,
RoutedEventArgs e)
{
Project project = new Project();
project.Name = nameTextBox.Text;
project.Estimate =
GetDouble(estimatedTextBox.Text);
project.Actual =
GetDouble(actualTextBox.Text);
project.ID =
int.Parse(
projectsComboBox.SelectedValue.
ToString());
ProjectUpdated(this,
new ProjectEventArgs(project));
}
private void projectsComboBox_SelectionChanged(
object sender, SelectionChangedEventArgs e)
{
SelectedProjectId
= (projectsComboBox.SelectedValue == null)
? NONE_SELECTED
: int.Parse(
projectsComboBox.SelectedValue.
ToString());
SelectionChanged(this,
new EventArgs());
}
#endregion Event handlers
#region Public methods
public void UpdateProject(Project project)
{
IEnumerable<Project> projects =
projectsComboBox.ItemsSource as
IEnumerable<Project>;
Project projectToUpdate =
projects.Where(p => p.ID == project.ID)
.First() as Project;
projectToUpdate.Name = project.Name;
projectToUpdate.Estimate = project.Estimate;
projectToUpdate.Actual = project.Actual;
if (project.ID == SelectedProjectId)
UpdateDetails(project);
}
public void LoadProjects(IEnumerable<Project> projects)
{
projectsComboBox.ItemsSource = projects;
projectsComboBox.DisplayMemberPath = "Name";
projectsComboBox.SelectedValuePath = "ID";
}
public void EnableControls(bool isEnabled)
{
estimatedTextBox.IsEnabled = isEnabled;
actualTextBox.IsEnabled = isEnabled;
updateButton.IsEnabled = isEnabled;
}
public void SetEstimatedColor(Color? color)
{
estimatedTextBox.Foreground
= (color == null)
? actualTextBox.Foreground
: new SolidColorBrush((Color)color);
}
public void UpdateDetails(Project project)
{
nameTextBox.Text = project.Name;
estimatedTextBox.Text
= project.Estimate.ToString();
actualTextBox.Text
= project.Actual.ToString();
DetailsUpdated(this,
new ProjectEventArgs(project));
}
#endregion Public methods
#region Helpers
private double GetDouble(string text)
{
return string.IsNullOrEmpty(text)
? 0 : double.Parse(text);
}
#endregion Helpers
}
#endregion ProjectsView
}
Presenter
Presenter включает в себя следующий код:
using System;
using System.Windows.Media;
using ProjectBilling.Business;
using ProjectBilling.DataAccess;
namespace ProjectBilling.UI.MVP
{
public class ProjectsPresenter
{
#region Initialization
private IProjectsView view = null;
private IProjectsModel model = null;
public ProjectsPresenter(IProjectsView projectsView,
IProjectsModel projectsModel)
{
view = projectsView;
view.ProjectUpdated += view_ProjectUpdated;
view.SelectionChanged
+= view_SelectionChanged;
view.DetailsUpdated += view_DetailsUpdated;
model = projectsModel;
model.ProjectUpdated += model_ProjectUpdated;
view.LoadProjects(
model.GetProjects());
}
#endregion Initialization
#region Event handlers
private void view_DetailsUpdated(object sender,
ProjectEventArgs e)
{
SetEstimatedColor(e.Project);
}
private void view_SelectionChanged(object sender,
EventArgs e)
{
int selectedId = view.SelectedProjectId;
if (selectedId > view.NONE_SELECTED)
{
Project project =
model.GetProject(selectedId);
view.EnableControls(true);
view.UpdateDetails(project);
SetEstimatedColor(project);
}
else
{
view.EnableControls(false);
}
}
private void model_ProjectUpdated(object sender,
ProjectEventArgs e)
{
view.UpdateProject(e.Project);
}
private void view_ProjectUpdated(object sender,
ProjectEventArgs e)
{
model.UpdateProject(e.Project);
SetEstimatedColor(e.Project);
}
#endregion Event handlers
#region Helpers
private void SetEstimatedColor(Project project)
{
if (project.ID == view.SelectedProjectId)
{
if (project.Actual <= 0)
{
view.SetEstimatedColor(null);
}
else if (project.Actual
> project.Estimate)
{
view.SetEstimatedColor(Colors.Red);
}
else
{
view.SetEstimatedColor(Colors.Green);
}
}
}
#endregion Helpers
}
}
Здесь используются следующие обработчики событий:
view_DetailsUpdated - обработчик вызывается в ответ на событие IProjectsView.DetailsUpdated и просто вызывает SetEstimateColor() для обновления цвета TextBox со сметной стоимостью проекта. Это позволяет добавить простую логику для приложения.
view_SelectionChanged - обновляет представление при возникновении события IProjectsView.SelectionChanged (выбора нового проекта в ComboBox).
model_ProjectUpdated - вызывается в ответ на IProjectsModel.ProjectUpdate, также вызывая обновление модели и представления.
Главное окно приложения MainWindow.xaml просто позволяет запустить несколько окон с представлением ProjectsView:
<Window x:Class="ProjectBilling.UI.MVP.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="250" Width="250"
MinHeight="250" MinWidth="250">
<StackPanel>
<Button Content="Показать проекты"
Name="showProjectsButton" Margin="5, 0, 5, 0"
Click="showProjectsButton_Click" />
</StackPanel>
</Window>
using System.Windows;
using ProjectBilling.Business;
namespace ProjectBilling.UI.MVP
{
public partial class MainWindow : Window
{
private IProjectsModel model = null;
public MainWindow()
{
InitializeComponent();
model = new ProjectsModel();
}
private void showProjectsButton_Click(object sender,
RoutedEventArgs e)
{
ProjectsView view = new ProjectsView();
ProjectsPresenter presenter
= new ProjectsPresenter(view, model);
view.Owner = this;
view.Show();
}
}
}
Запустив это приложение, вы получите аналогичный результат, как и для MVC рассмотренного ранее. Главный интерес здесь представляет только структура Presenter, который используется в MVP.
Итак, MVP представляет собой большой шаг вперед по сравнению с MVC в нескольких направлениях:
MVP обеспечивает проверяемость состояния и логики представления, перемещая их в Presenter.
Представление жестко отделяется от модели, при этом связь организуется через Presenter. В отличие от MVC, MVP допускает повторное использование бизнес-логики без непосредственного видоизменения модели (за счет использования интерфейса IView). Например, если вы захотите перенести это WPF-приложение на Silverlight, вам потребуется только создать представление в Silverlight, реализующее IProjectsView, а IProjectsPresenter и IProjectsModel можно использовать повторно.