Паттерн MVC
158WPF --- Периферия WPF --- Паттерн MVC
Термин модель-представление-контроллер (model-view-controller) используется с конца 70-х гг. прошлого столетия. Эта модель явилась результатом проекта Smalltalk в компании Xerox PARC, где она была задумана как способ организации некоторых из ранних приложений графического пользовательского интерфейса. Некоторые из нюансов первоначальной модели MVC были связаны с концепциями, специфичными для Smalltalk, такими как экраны и инструменты, но более глобальные понятия все еще применимы к приложениям, и особенно хорошо они подходят для веб-приложений (MVC нашел отличное применение в ASP.NET, но ниже мы рассмотрим этот паттерн в WPF).
Если оперировать понятиями высокого уровня, архитектурный шаблон MVC означает, что приложение MVC будет разделено, по крайней мере, на три части:
- Модели
Содержат или представляют данные, с которыми работают пользователи. Они могут быть простыми моделями представлений, которые только представляют данные, передаваемые между представлениями и контроллерами; или же они могут быть моделями предметной области, которые содержат бизнес-данные, а также операции, преобразования и правила для манипулирования этими данными.
- Представления
Применяются для визуализации некоторой части модели в виде пользовательского интерфейса.
- Контроллеры
Обрабатывают поступающие запросы, выполняют операции с моделью и выбирают представления для визуализации пользователю.
Ниже структура MVC показана на диаграмме:
Если рассматривать приложение в призме бизнес-логики, то можно выделить три уровня на которых строится приложение:
- Уровень представления
Данный уровень отвечает за отображение данных, обеспечение обратной связи с пользователем, сбор пользовательской информации, которая передается в уровень бизнес-логики для обработки.
- Бизнес-уровень
Бизнес-уровень или, если говорить проще, уровень приложения, обеспечивает логику взаимодействия представления и данных. В MVC бизнес-уровень реализует структуру модели.
- Уровень данных
Уровень данных отвечает за получение, передачу и сохранение данных в файле, базе данных, службе или XML.
Данная структура представлена ниже на рисунке:
Итак, давайте продемонстрируем реализацию паттерна 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 содержит следующие члены, необходимые для реализации в классе:
Projects - свойство, содержащее коллекцию проектов, загружаемых из кода доступа к данным.
ProjectUpdated - это событие для уведомления экземпляра класса Project об обновлении.
UpdateProject - метод представления проекта, который будет обновляться.
Класс модели соответственно реализует данный интерфейс добавляя простейшую функциональность.
Контроллер
Следующий код был добавлен в файл 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. Давайте запустим это приложение и откроем два окна представления продукта:
Самое интересное в моделе MVC, что обновление распространяется через представления. Выполните следующие шаги чтобы это увидеть:
Выберите проект Jones в раскрывающемся списке ComboBox первого диалогового окна.
Установите актуальную стоимость для него 1600.
Щелкните кнопку Update в этом окне.
Вы должны увидеть автоматическое обновление представления во втором окне:
Можете поэкспериментировать с другими проектами или открыть еще несколько окон, при этом везде обновление представления будет работать так же. Итак, как видно MVC абстрагирует бизнес-логику от уровня представления, тем самым добиваясь легкой синхронизации пользовательского интерфейса. Я не буду рассматривать здесь модульное тестирование (еще один аспект использования паттернов) просто потому, что MVC больше подходит для веб-приложений, нежели для приложений WPF. Тестирование будет показано позже с использованием паттерна MVVM.