Паттерн MVP

129

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

Структура MVP

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

Бизнес-модель MVP

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

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

UML-диаграмма классов проекта MVP

Представление использует класс 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 со следующими членами:

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
    }
}

Здесь используются следующие обработчики событий:

Главное окно приложения 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 в нескольких направлениях:

Пройди тесты
Лучший чат для C# программистов