Saturday, September 11, 2010

Introduction to MVVM

Я собираюсь написать серию статей про MVVM, где подробно рассмотрю проблемы, которые решает этот паттерн, его отличия от аналогов, его структуру, модификации, существующие рекомендации по использованию, а также некоторые доступные сейчас фреймворки, избавляющие от рутины при использовании MVVM.

Это первая обзорная статья. Здесь я хочу представить сам паттерн и привести небольшой пример иллюстрирующий его идею.

Итак. При создании любого WPF приложения следует тщательно подумать над следующими вопросами:

  1. Как правильно спроектировать приложение?
  2. Как правильно обеспечить разделение ответственности между модулями?
  3. Как обеспечить тестирование приложения? Какие модули можно покрыть юнит тестами?

На самом деле вопросов, конечно же, больше, но именно на эти вопросы позволяет найти ответы паттерн MVVM.

MVVM появился не на пустом месте. Существует целое семейство так называемых Separated Presentation паттернов, отделяющих визуальные компоненты UI от логики определяющей состояние и поведение этих компонентов, а также от бизнес логики. Первым из них в далеком 1979 году появился Model-View-Controller паттерн, затем Model-View-Presenter и его модификации.

В 2004 году Мартин Фаулер (Martin Fowler) представил Presentation Model паттерн, в котором блок модели представления (presentation model) играет роль фасада для модели и координирует все взаимодействие представления с моделью. Presentation Model Фаулера не привязан ни к какой конкретной платформе, и не описывает, как представление должно использовать данные из модели представления.

Наконец в 2005м году в своем блоге Джон Госсман (John Gossman) представил паттерн MVVM, который является реализацией Presentation Model Фаулера, учитывающей специфическую для WPF функциональность, такую, например, как привязки (Bindings) и команды (Commands).

MVVMStructure

В MVVM блок Model представляет собой один или несколько модулей реализующих бизнес-логику приложения. Блок View представляет собой XAML-файл содержащий UI представление. И наконец, блок ViewModel является как бы фасадом, который преобразовывает интерфейс модели в форму удобную для использования в UI представлении, а также содержит логику управления состоянием UI. При этом очень важно, что ViewModel ничего не знает о конкретных UI контролах находящихся во View.

Разделение UI (View) и логики управления состоянием UI (ViewModel) на отдельные компоненты является серьезным аргументом в пользу использования MVVM вместо CodeBehind, т.к. позволяет легко покрывать ViewModel unit-тестами.

Такое разделение не было бы возможно без data bindings – технологии связывания данных появившейся в WPF. Именно data bindings позволяет связывать свойства во View со свойствами во ViewModel при этом, не создавая жестких связей между ними.

Команды (commands) – еще одна технология WPF, которую активно использует MVVM. Именно команды позволяют пользователю, используя UI контролы на View, запускать функциональность, реализованную во ViewModel.

Давайте теперь рассмотрим небольшое демо, иллюстрирующее MVVM паттерн. Единственное окно нашего приложения будет содержать всего три UI элемента:

  • ListBox, со списком неких строковых элементов
  • TextBox, в который пользователь сможет ввести текст для нового элемента списка
  • Button, нажав на который пользователь сможет добавить новый элемент в список

MVVMDemo1App

Давайте посмотрим на XAML, описывающий это окно:

<Window x:Class="PureMVVMSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="250">
<DockPanel LastChildFill="True">
<TextBlock Text="Tags:" Margin="5 5 0 0" DockPanel.Dock="Top" />

<StackPanel DockPanel.Dock="Bottom">
<TextBox
Text="{Binding NewTag, UpdateSourceTrigger=PropertyChanged}"
Margin="5" />
<Button
Content="Add" MaxWidth="100" Margin="5"
Command="{Binding AddNewTagCommand}" />
</StackPanel>

<ListBox
Margin="5 0"
SelectionMode="Single"
ItemsSource="{Binding Tags}" />
</DockPanel>
</Window>

Обратите внимание на следующие два момента. Во-первых, ни одному из элементов не присвоено имя. Это потому что имена контролам просто не нужны. Мы не будем ниоткуда явно на них ссылаться. Во вторых, точно так же как и имена, отсутствуют и обработчики событий. Дело в том, что обработчики событий потребуют от нас написания какого-то кода в CodeBehind, а это как раз одна из вещей которых мы хотим избежать.

Вместо имен для явных ссылок и обработчиков событий, мы используем data bindings и commands. Давайте посмотрим на описание ListBox’a.

<ListBox
Margin="5 0"
SelectionMode="Single"
ItemsSource="{Binding Tags}" />

Обратите внимание на binding, который связывает свойство ListBox’a ItemsSource со свойством Tags, определенным во ViewModel. Что именно представляет из себя Tags мы рассмотрим позже. Сейчас же важно понять, что этот binding является единственным местом, где мы связываем UI контрол ListBox с источником данных для него. Причем, смотрите здесь нет никаких упоминаний о типе ViewModel’a. Т.е. View и ViewModel не привязаны друг к другу жестко. Наш ListBox может отображать данные из абсолютно любого ViewModel, в котором определено свойство Tags возвращающее коллекцию элементов.

Давайте теперь посмотрим на описание кнопки. Вы видите, что здесь нет обработчика события Click. Зато к свойству Command кнопки привязана команда AddNewTagCommand определенная во ViewModel. Точно также как и в случае привязки источника данных к ListView здесь нет жесткой привязки нашего View к какому-то конкретному ViewModel.

<Button
Content="Add" MaxWidth="100" Margin="5"
Command="{Binding AddNewTagCommand}" />

Ну и наконец, давайте убедимся в отсутствии какой-либо логики в CodeBehind нашего View. Вы видите, что здесь есть только конструктор.

public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}

Model в нашем случае очень простая. Она реализует некий интерфейс IDataProvider, позволяя во ViewModel получать необходимые данные.

internal interface IDataProvider
{
IEnumerable<string> Tags { get; }
}

internal sealed class DataProvider : IDataProvider
{
public IEnumerable<string> Tags
{
get
{
return new List<string>
{
"c#", "java", "php", ".net"
};
}
}
}

Разобравшись с Model и View, давайте теперь рассмотрим ViewModel – наиболее интересный блок в MVVM. Для начала давайте посмотрим на свойство Tags, из которого получает данные ListBox во View:

public ObservableCollection<string> Tags
{
get;
private set;
}

Итак, Tags – это обычное свойство, которое светит коллекцию. Тип коллекции ObservableCollection выбран неслучайно. ObservableCollection умеет нотифицировать клиентов в случае добавления \ удаления ее элементов. Это позволяет UI контролам связанным со свойством Tags обновлять свой контент самостоятельно без какого-либо дополнительного кода.

Инициализируется Tags в конструкторе класса ViewModel.

public MainViewModel(IDataProvider provider)
{
Tags = new ObservableCollection<string>(provider.Tags);
}

Теперь давайте разберемся, что представляет из себя команда и посмотрим, как она реализована в нашем демо. Все команды в WPF должны реализовывать интерфейс ICommand:

public interface ICommand
{
event EventHandler CanExecuteChanged;

bool CanExecute(object parameter);
void Execute(object parameter);
}

Тут все достаточно прозрачно. Метод Execute содержит логику самой команды. Метод CanExecute определяет можно или нет выполнить команду. Событие CanExecuteChanged оповещает клиентов об изменении возможности выполнения команды.

public void Execute(object parameter)
{
_viewModel.Tags.Add(_viewModel.NewTag);
_viewModel.NewTag = string.Empty;
}

В нашем случае метод Execute всего лишь добавляет новый элемент в коллекцию Tags. При этом текст для нового элемента он берет из свойства NewTag определенного во ViewModel. Обратите внимание, что свойство NewTag привязано к свойству Text UI контрола TextBox описанного на View.

<TextBox
Text="{Binding NewTag, UpdateSourceTrigger=PropertyChanged}"
Margin="5" />

Процесс добавления элемента в список можно описать следующим образом:

  1. Пользователь вводит текст в TextBox
  2. DataBinding пропихивает значение свойства Text TextBox’a в свойство NewTag во ViewModel.
  3. Команда AddNewTagCommand добавляет новый элемент в коллекцию Tags используя значение свойства NewTag.
  4. При добавлении элемента коллекция Tags уведомляет своих клиентов (в нашем случае ListBox) об изменениях. Клиенты обновляют свой контент.

Т.е. вы видите, что команда не использует явные ссылки на UI контролы. Вместо того чтобы явно взять текст из TextBox’a и затем добавить его в коллекцию элементов ListBox’a, команда работает только со свойствами определенными во ViewModel и не зависящими от View.

Метод CanExecute для определения возможности выполнения команды аналогичным образом использует только свойства определенные во ViewModel.

public bool CanExecute(object parameter)
{
return
!string.IsNullOrEmpty(_viewModel.NewTag) &&
!_viewModel.Tags.Contains(_viewModel.NewTag);
}

Последний момент, который я хочу показать – это как же все-таки устанавливается связь между View и ViewModel. При запуске приложения свойству DataContext определенному на View присваивается экземпляр ViewModel. И все. Этого достаточно для работы data binding’a.

private void Application_Startup(object sender, StartupEventArgs e)
{
var viewModel = new MainViewModel(new DataProvider());

var mainWindow = new MainWindow
{
DataContext = viewModel
};

Current.MainWindow = mainWindow;
mainWindow.Show();
}

На этом, пожалуй, все. Полный код демо можно скачать отсюда. В следующей части я рассмотрю существующие MVVM тулкиты и более детально остановлюсь на MVVM Light Toolkit.

3 comments:

  1. Очень подробно и интересно)
    Спасибо..

    Всегда хотел теоретически ознакомится с MVVM, на практике не встречаю, так как не работаю с WPF.

    ReplyDelete
  2. Здравствуйте!
    Хорошая статья и блог в целом, объяснено так как надо!
    НЕ могли бы вы выложить полный код, а то не удаётся скачать!!

    ReplyDelete
  3. Спасибо за труд. Очень кстати, внятная информация по паттерну на русском языке.

    ReplyDelete