Паттерн MVC

158

Термин модель-представление-контроллер (model-view-controller) используется с конца 70-х гг. прошлого столетия. Эта модель явилась результатом проекта Smalltalk в компании Xerox PARC, где она была задумана как способ организации некоторых из ранних приложений графического пользовательского интерфейса. Некоторые из нюансов первоначальной модели MVC были связаны с концепциями, специфичными для Smalltalk, такими как экраны и инструменты, но более глобальные понятия все еще применимы к приложениям, и особенно хорошо они подходят для веб-приложений (MVC нашел отличное применение в ASP.NET, но ниже мы рассмотрим этот паттерн в WPF).

Если оперировать понятиями высокого уровня, архитектурный шаблон MVC означает, что приложение MVC будет разделено, по крайней мере, на три части:

Модели

Содержат или представляют данные, с которыми работают пользователи. Они могут быть простыми моделями представлений, которые только представляют данные, передаваемые между представлениями и контроллерами; или же они могут быть моделями предметной области, которые содержат бизнес-данные, а также операции, преобразования и правила для манипулирования этими данными.

Представления

Применяются для визуализации некоторой части модели в виде пользовательского интерфейса.

Контроллеры

Обрабатывают поступающие запросы, выполняют операции с моделью и выбирают представления для визуализации пользователю.

Ниже структура MVC показана на диаграмме:

Структура MVC

Если рассматривать приложение в призме бизнес-логики, то можно выделить три уровня на которых строится приложение:

Уровень представления

Данный уровень отвечает за отображение данных, обеспечение обратной связи с пользователем, сбор пользовательской информации, которая передается в уровень бизнес-логики для обработки.

Бизнес-уровень

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

Уровень данных

Уровень данных отвечает за получение, передачу и сохранение данных в файле, базе данных, службе или XML.

Данная структура представлена ниже на рисунке:

MVC и бизнес-логика

Итак, давайте продемонстрируем реализацию паттерна MVC на простом приложении WPF, разделив его на три части - модель, представление и контроллер.

Модель

Создайте новый проект WPF в Visual Studio, добавьте ссылку на сборку ProjectBilling.DataAccess, которая рассматривалась в предыдущей статье, добавьте класс ProjectsModel со следующим содержимым:

using System;
using System.Collections.Generic;
using System.Linq;
using ProjectBilling.DataAccess;

namespace ProjectBilling.Business.MVC
{
    public interface IProjectsModel
    {
        IEnumerable<Project> Projects { get; set; }
        event EventHandler<ProjectEventArgs> ProjectUpdated;
        void UpdateProject(Project project);
    }

    public class ProjectsModel : IProjectsModel
    {
        public IEnumerable<Project> Projects { get; set; }

        public event EventHandler<ProjectEventArgs>
            ProjectUpdated = delegate { };

        public ProjectsModel()
        {
            Projects = new DataServiceStub().GetProjects();
        }

        private void RaiseProjectUpdated(Project project)
        {
            ProjectUpdated(this,
                new ProjectEventArgs(project));
        }

        public void UpdateProject(Project project)
        {
            Project selectedProject
                = Projects.Where(p => p.ID == project.ID)
                      .FirstOrDefault() as Project;
            selectedProject.Name = project.Name;
            selectedProject.Estimate = project.Estimate;
            selectedProject.Actual = project.Actual;
            RaiseProjectUpdated(selectedProject);
        }
    }

    public class ProjectEventArgs : EventArgs
    {
        public Project Project { get; set; }

        public ProjectEventArgs(Project project)
        {
            Project = project;
        }
    }
}

Этот код создает модель для использования нашим представлением. Представление получает обновления из модели и через контроллер передает данные обратно в модель используя ссылку на IProjectsModel. Мы создали класс ProjectsModel реализующий IProjectsModel для возможности легкой расширяемости и тестируемости приложения. Давайте рассмотрим этот код более подробно. Интерфейс IProjectsModel содержит следующие члены, необходимые для реализации в классе:

Класс модели соответственно реализует данный интерфейс добавляя простейшую функциональность.

Контроллер

Следующий код был добавлен в файл ProjectsController.cs и реализует функциональность контроллера:

using System;
using ProjectBilling.Business.MVC;
using ProjectBilling.DataAccess;
using System.Windows;

namespace ProjectBilling.UI.MVC
{
    public interface IProjectsController
    {
        void ShowProjectsView(Window owner);
        void Update(Project project);
    }

    public class ProjectsController : IProjectsController
    {
        private readonly IProjectsModel _model;

        public ProjectsController(IProjectsModel projectModel)
        {
            if (projectModel == null)
                throw new ArgumentNullException(
                    "projectModel");
            _model = projectModel;
        }

        public void ShowProjectsView(Window owner)
        {
            ProjectsView view
                = new ProjectsView(this, _model);
            view.Owner = owner;
            view.Show();
        }

        public void Update(Project project)
        {
            _model.UpdateProject(project);
        }
    }
}

Мы реализовали контроллер на основе интерфейса IProjectsController, включающего два метода. ShowProjectsView - метод позволяющий отображать представление (реализованное классом ProjectsView) конечному пользователю. Update - метод обновления данных проекта, который вызывает прототип IProjectsModel.

Представление

Представление реализовано в файлах ProjectView.xaml и ProjectView.xaml.cs:

<Window x:Class="ProjectBilling.UI.MVC.ProjectsView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Projects" MinHeight="180" Height="180"
        MinWidth="350" Width="350" Padding="5"
        FocusManager.FocusedElement
            ="{Binding ElementName=ProjectsComboBox}">
    <Grid Margin="5">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <Label Content="Проект:" />
        <ComboBox Name="ProjectsComboBox" Margin="5" Grid.Column="1"
                SelectionChanged="ProjectsComboBox_SelectionChanged" />
        <Label Content="Сметная стоимость:" Grid.Row="1" />
        <TextBox Name="EstimatedTextBox" Grid.Column="1" Grid.Row="1" Margin="5" IsEnabled="False" />
        <Label Content="Фактическая стоимость:" Grid.Row="2" />
        <TextBox Name="ActualTextBox" Grid.Row="2" Grid.Column="1" Margin="5" IsEnabled="False" />
        <Button Name="updateButton" Content="Update" Grid.Row="3" Grid.ColumnSpan="2" Margin="5"
                IsEnabled="False" Click="UpdateButton_Click" />
    </Grid>
</Window>

Этот код создает простую форму, для отображения подробностей о проекте. Затем добавьте следующий код для ProjectsView.xaml.cs:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using ProjectBilling.Business.MVC;
using ProjectBilling.DataAccess;

namespace ProjectBilling.UI.MVC
{
    public partial class ProjectsView : Window
    {
        private readonly IProjectsModel _model;
        private readonly IProjectsController _controller
            = null;
        private const int NONE_SELECTED = -1;

        public ProjectsView(
            IProjectsController projectsController,
            IProjectsModel projectsModel)
        {
            InitializeComponent();
            _controller = projectsController;
            _model = projectsModel;

            _model.ProjectUpdated += model_ProjectUpdated;
            ProjectsComboBox.ItemsSource = _model.Projects;
            ProjectsComboBox.DisplayMemberPath = "Name";
            ProjectsComboBox.SelectedValuePath = "ID";
        }


        #region Event Handlers

        void model_ProjectUpdated(object sender,
            ProjectEventArgs e)
        {
            int selectedProjectId = GetSelectedProjectId();

            if (selectedProjectId > NONE_SELECTED)
            {
                ProjectsComboBox.SelectedValue
                    = selectedProjectId;
                if (selectedProjectId == e.Project.ID)
                {
                    UpdateDetails(e.Project);
                }
            }
        }

        private void ProjectsComboBox_SelectionChanged(
            object sender, SelectionChangedEventArgs e)
        {
            Project project = GetSelectedProject();
            if (project != null)
            {
                EstimatedTextBox.Text = project.Estimate.ToString();
                EstimatedTextBox.IsEnabled = true;
                ActualTextBox.Text
                    = project.Actual.ToString();
                ActualTextBox.IsEnabled = true;
                updateButton.IsEnabled = true;
                UpdateEstimatedColor();
            }
        }

        private void UpdateButton_Click(object sender,
            RoutedEventArgs e)
        {
            Project project = new Project()
            {
                ID = (int)ProjectsComboBox.SelectedValue,
                Name = ProjectsComboBox.Text,
                Estimate = GetDouble(
                    EstimatedTextBox.Text),
                Actual = GetDouble(ActualTextBox.Text)
            };
            _controller.Update(project);
        }

        #endregion Event Handlers

        #region Helpers

        private void UpdateEstimatedColor()
        {
            double actual
                = GetDouble(ActualTextBox.Text);
            double estimated
                = GetDouble(EstimatedTextBox.Text);
            if (actual == 0)
            {
                EstimatedTextBox.Foreground
                    = ActualTextBox.Foreground;
            }
            else if (actual > estimated)
            {
                EstimatedTextBox.Foreground
                    = Brushes.Red;
            }
            else
            {
                EstimatedTextBox.Foreground
                    = Brushes.Green;
            }
        }

        private void UpdateDetails(Project project)
        {
            EstimatedTextBox.Text
                = project.Estimate.ToString();
            ActualTextBox.Text
                = project.Actual.ToString();
            UpdateEstimatedColor();
        }

        private double GetDouble(string text)
        {
            return string.IsNullOrEmpty(text) ?
                0 : double.Parse(text);
        }

        private Project GetSelectedProject()
        {
            return ProjectsComboBox.SelectedItem
                as Project;
        }

        private int GetSelectedProjectId()
        {
            Project project = GetSelectedProject();
            return (project == null)
                ? NONE_SELECTED : project.ID;
        }

        #endregion Helpers
    }
}

Этот код практически аналогичен рассмотренному коду в предыдущей статье, но при этом он добавляет взаимосвязь с моделью и контроллером. Чтобы отобразить работоспособность этого приложения давайте добавим код вызова этой формы из главного окна приложения, т.е. MainWindow, добавив следующий код в соответствующие файлы:

<!-- MainWindow.xaml -->
<Window x:Class="ProjectBilling.UI.MVC.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Shell" Height="150" Width="150"
        MinHeight="150" MinWidth="150">
    <StackPanel>
        <Button Content="Update Projects" 
                Name="ShowProjectsButton" Margin="5"
                Click="ShowProjectsButton_Click" />
    </StackPanel>
</Window>
using System.Windows;
using ProjectBilling.Business.MVC;

namespace ProjectBilling.UI.MVC
{
    public partial class MainWindow : Window
    {
        private IProjectsController _controller;

        public MainWindow()
        {
            InitializeComponent();
            _controller
                = new ProjectsController(new ProjectsModel());
        }

        private void ShowProjectsButton_Click(object sender,
            RoutedEventArgs e)
        {
            _controller.ShowProjectsView(this);
        }
    }
}

Этот код будет служить в качестве главного окна приложения и будет отображать представление ProjectsView каждый раз, когда пользователь щелкает по кнопке Update. Давайте запустим это приложение и откроем два окна представления продукта:

Два окна представления (view)

Самое интересное в моделе MVC, что обновление распространяется через представления. Выполните следующие шаги чтобы это увидеть:

  1. Выберите проект Jones в раскрывающемся списке ComboBox первого диалогового окна.

  2. Установите актуальную стоимость для него 1600.

  3. Щелкните кнопку Update в этом окне.

Вы должны увидеть автоматическое обновление представления во втором окне:

Обновление представления (view)

Можете поэкспериментировать с другими проектами или открыть еще несколько окон, при этом везде обновление представления будет работать так же. Итак, как видно MVC абстрагирует бизнес-логику от уровня представления, тем самым добиваясь легкой синхронизации пользовательского интерфейса. Я не буду рассматривать здесь модульное тестирование (еще один аспект использования паттернов) просто потому, что MVC больше подходит для веб-приложений, нежели для приложений WPF. Тестирование будет показано позже с использованием паттерна MVVM.

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