Tuesday, September 21, 2010

MVVM: Command bindings, Messaging and Multithreading

В предыдущих статьях, посвященных MVVM Light toolkit, я уже приводил примеры использования RelayCommand и ViewModelBase классов. Сейчас я хочу подробнее остановиться на еще трех классах, которые предлагает этот toolkit, а именно:

  1. EventToCommand behavior позволяет привязывать команды к любым событиям любых UI контролов.
  2. Класс Messenger позволяет организовывать обмен сообщениями внутри приложения.
  3. Класс DispatcherHelper облегчает жизнь при работе с UI потоком.

EventToCommand

Несмотря на все свои достоинства, в WPF команды поддерживаются очень ограниченным числом UI элементов (только производными от типа ButtonBase). Более того команду можно привязать только к событию Click. Получается, что для того чтоб запустить комаду, определенную во ViewModel, по событию сгенерированному UI элементом, не поддерживающим команды, нам понадобится в codebehind создавать обработчик этого события и из него руками вызывать команду.

Однако всех этих сложностей можно избежать, если использовать тип EventToCommand предлагаемый MVVM Light toolkit. Давайте рассмотрим небольшой пример. Допустим у нас есть TextBox и мы хотим к его событию TextChanged привязать команду ChangeMode. Сделать это очень просто:

<TextBox Text="{Binding TagName, UpdateSourceTrigger=PropertyChanged}"
         Grid.Column="1" Margin="0 0 0 5" MinWidth="200">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="TextChanged">
            <cmd:EventToCommand
                Command="{Binding ChangeMode, Mode=OneWay}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</TextBox>

Messenger

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

Messaging

Вместо того чтобы подписываться на сообщения от какого-то конкретного объекта получатели просто сообщают Messenger’у какие именно сообщения они хотят получать. Например, вот так можно зарегистрироваться для получения сообщений типа TagInfo:

Messenger.Default.Register<TagInfo>(this,
    x =>
    {
        Tags.Add(x);
    });

Теперь получателю будут приходить все сообщения, содержащие TagInfo, от любого отправителя. Лямбда, приведенная в примере, описывает реакцию получателя на сообщение.

С другой стороны отправители не занимаются рассылкой самостоятельно. Вместо этого они создают экземпляр сообщения нужного типа и просят Messenger’а его оправить:

Messenger.Default.Send<TagInfo, MainViewModel>(
    new TagInfo
    {
        Name = TagName,
        Score = TagScore
    });

После отправки это сообщение будет доставлено всем заинтересованным получателям.

Хочу отметить следующие три момента связанные с Messenger’ом:

  • Связи между отправителями и получателями можно настраивать. Совсем не обязательно рассылать сообщения всем подряд. Например, используя токены при регистрации и отправке, можно организовать общение только между конкретными экземплярами отправителя и получателя.
  • Messenger никак не работает с потоками. Т.е. если необходимо пробрасывать сообщения из одного потока в другой, то организовывать такую переброску нужно самостоятельно.
  • Messenger не хранит жестких ссылок на получателей. Т.е. после регистрации ничто не мешает сборщику мусора удалить экземпляр получателя.

DispatcherHelper

DispatcherHelper – это класс, который упрощает запуск операций в потоке UI из других потоков. При инициализации DispatcherHelper сохраняет в своем свойстве UIDispatcher ссылку на диспетчера потока UI. После этого любая операция, запущенная с помощью DispatcherHelper.CheckBeginInvokeOnUI() гарантированно будет выполнена в потоке UI. При этом если запуск происходит из потока UI, то операция будет выполнена стазу же синхронно. Если же из другого потока, то операция будет поставлена в очередь выполнения диспетчера потока UI и выполнена асинхронно.

Вот так, например, можно отправить сообщение из фонового потока в поток UI:

DispatcherHelper.CheckBeginInvokeOnUI(
    () =>
    {
        Messenger.Default.Send<TagInfo, MainViewModel>(
            new TagInfo
            {
                Name = DateTime.Now.ToLongTimeString(),
                Score = 1
            });
    }
);

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

Wednesday, September 15, 2010

MVVM: How do I wire up my View and ViewModels?

В этой статье я хочу рассмотреть три способа связывания View c ViewModel.

Способ первый. В коде. Как мне кажется наиболее гибкий вариант. Позволяет использовать параметризированные конструкторы. Это полезно когда, например, вам нужно инжектировать View во ViewModel. Я уже показывал этот способ, когда мы рассматривали ViewModel-First approach.

private void Application_Startup(object sender, StartupEventArgs e)
{
var mainWindow = new MainWindow();
Current.MainWindow = mainWindow;

var viewModel = new MainViewModel(new DataProvider(), mainWindow);

mainWindow.DataContext = viewModel;
mainWindow.Show();
}

Способ второй. Объявить ViewModel как статический ресурс и затем в XAML прописать привязку для View. Этот способ менее гибкий, т.к. для создания ресурса будет использован дефолтный конструктор.

В MVVM Light Toolkit проблему дефолтного конструктора решили. Вместо того чтоб объявлять сам ViewModel как статический ресурс, ресурсом там сделали класс ViewModelLocator, который внутри себя в коде создает нужные ViewModel и соответственно может использовать параметризованные конструкторы. Однако, я пока не нашел решения как используя этот способ инжектировать View во ViewModel.

Этот способ мы уже тоже видели в примерах, где использовался MVVM Light Toolkit.

<Application.Resources>
<vm:ViewModelLocator x:Key="Locator"
d:IsDataSource="True" />
</Application.Resources>

<Window
DataContext="{Binding Main, Source={StaticResource Locator}}">

Способ третий. Наверное не совсем очевидный но тем не менее. Внутри DataTemplate свойство DataContext всегда хранит ссылку на объект, к которому это DataTemplate применяется. Если использовать термины MVVM, то DataTemplate – это View, а сам объект – это ViewModel. Т.е. внутри DataTemplate можно привязывать UI контролы к любым свойствам и командам, которые светит объект.

Давайте рассмотрим небольшой пример демонстрирующий использование DataTemplate. За основу возьмем демо приложение из предыдущих статей. Список тегов для него я взял со StackOverflow. Давайте добавим к тегам еще их рейтинг. Описывать каждый тег теперь будет тип TagInfo. Это будет наш ViewModel.

public sealed class TagInfo
{
public string Name { get; set; }
public int Score { get; set; }
}

А вот так выглядит DataTemplate для TagInfo.

<DataTemplate DataType="{x:Type m:TagInfo}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" MinWidth="150" />
<TextBlock Text="{Binding Score}" />
</StackPanel>
</DataTemplate>

Если помните, ListBox в главном окне привязан к коллекции Tags из MainViewModel. Раньше эта коллекция хранила строки, а теперь объекты TagInfo. Когда ListBox захочет отобразить свои элементы, то он надет для них наш DataTemplate и подставит ему в DataContext соответствующий TagInfo.

Вот так будет выглядеть список тегов после применения DataTemplate.

DataTemplatesDemo

Исходный код этого примера можно скачать отсюда.

MVVM: multiple Views for a single ViewModel

В примерах, которые я показывал до сих пор, всегда был один ViewModel, который связывался только с одним View. Давайте теперь посмотрим, как можно связать ViewModel с несколькими View.

Давайте в демо приложение из предыдущей статьи добавим еще одно окошко DetailsWindow, в котором будем показывать текущее число тегов. XAML для нового окна будет очень простой.


<Window x:Class="MultipleViewsDemo.DetailsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="150" Height="75"
VerticalContentAlignment="Center" HorizontalContentAlignment="Center"
WindowStartupLocation="CenterScreen"
Title="DetailsWindow"
DataContext="{Binding Main, Source={StaticResource Locator}}">

<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="Number of Tags:" />
<TextBlock Text="{Binding Tags.Count}" Margin="5 0 0 0" />
</StackPanel>
</Window>

Ключевой момент, на который нужно обратить внимание – это привязка свойства DataContext нового окна. Она абсолютно идентична привязке DataContext’a главного окна. Т.е. оба окна привязаны к одному и тому же ViewModel.

DataContext="{Binding Main, Source={StaticResource Locator}}"

Теперь при добавлении новых тегов оба окна будут реагировать на изменения во ViewModel. Главное окно будет показывать новые теги в списке, а в DetailsWindow будет обновляться количество тегов.

Исходный код можно скачать отсюда.

Tuesday, September 14, 2010

MVVM Light Toolkit overview

В предыдущих двух статьях про MVVM для создания примеров я пользовался только стандартными средствами, которые out of box предоставляет WPF. Для очень простых случаев, возможно, такой подход и оправдан. Недостаток его в том, что приходится писать много plumbing кода. А это рутина, от которой всегда хочется избавиться.

Для имплементации команды AddNewTagCommand мы создавали приватный класс во ViewModel. Но у нас была всего одна команда. Если понадобится больше команд, то писать для каждой из них свой класс – не самая лучшая идея. Надо будет каким-то образом выносить общую функциональность.

Наш ViewModel реализовывал интерфейс INotifyPropertyChanged. Дублировать его реализацию в каждом ViewModel нехорошо. Поэтому ее тоже надо выносить, например, в базовый класс.

Не смотря на все удобство команд, они почему-то поддерживаются только контролами производными от ButtonBase и только для события Click. Что же делать, если вы хотите использовать команду для реакции на SelectionChanged у ListBox’a? Написать свой собственный behavior, который сможет привязать команду к произвольному событию.

Для того чтобы использовать эти наработки в других своих WPF приложениях, наверняка вы вынесете их в отдельную библиотеку. Поздравляю, вы написали свой собственный MVVM Toolkit! :)

На самом деле MVVM – это просто паттерн, и для его применения не нужны никакие тулкиты. Все тулкиты – это просто хэлперы, которые облегчают вам жизнь, избавляя от рутины. Список желающих помочь вам оказался настолько внушительным, что я отказался от первоначальной идеи делать обзор и сравнение их всех. Итак, согласно Википедии сейчас существует 14 различных MVVM toolkit’ов:

Для полноты картины следует добавить сюда еще PRISM (который, на самом деле, гораздо больше чем просто очередной MVVM toolkit) и nRote.

В комментариях в блоге MVVM Light Toolkit автора спросили: «Laurent, почему ты написал свой собственный тулкит? Ведь ты общаешься со столькими WPF гуру, каждый из которых, кстати, тоже написал свой тулкит :) Так почему же вы, парни, не пообщались и не написали что-то одно? Вместо того чтобы запутывать людей этой горой тулкитов, функционал которых пересекается на 90% и отличается нюансами имплементации.»

Я хочу привести здесь ответ, потому что он достаточно четко описывает идеи заложенные в MVVM Light Toolkit: «I think that the main reason is that we have different reasons to use (and make) such a framework. For me, the main reason is really Blendability (design time data, specifically), with testability of the UI a close second. For others, validation plays a very large role in their adoption of the MVVM pattern, etc. In addition, to be useful to me, a framework needs to work in Silverlight as well as in WPF, while others do not care about Silverlight (yet?) and find it too limited. I cannot rely on WPF-only mechanisms in my framework, but others do because their craft is in a different area than mine.

For what it is worth, there is quite an intense collaboration between some of the disciples and me on this toolkit. The toolkit I provide is a "blend" (pun intended) of code and ideas coming from different sources. The credits section on the Get Started page lists some of the major contributors, most of them from the disciples group. I have fished many ideas and solved many problems on the group.

Lastly, I would be a bit reluctant to engage in a large scale operation to provide a "one and only" MVVM framework. To be blunt, it sounds like a lot less fun than what I am doing now. The good news is that many at Microsoft are aware of this effort, and I think we will see improvements in the framework (WPF and Silverlight) directly that should make some of this toolkit redundant. This is why I put a lot of effort in keeping it "light", because I am pretty sure it will be obsolete some day.»

Итак, MVVM Light Toolkit – это легковесная библиотека классов, которая упростит вам жизнь, избавив от рутины при имплементации MVVM паттерна для WPF, Silverlight и Windows Phone 7 приложений. Давайте посмотрим, что именно предлагает этот тулкит:

  1. Классы RelayCommand и RelayCommand<T>. Реализуют интерфейс ICommand для WPF и Silverlight. Упрощают создание команд.
  2. Класс ViewModelBase может быть использован как базовый для всех ViewModel’ов. Избавляет от необходимости реализовывать INotifyPropertyChanged.
  3. EventToCommand behavior позволяет привязывать команды к любым событиям любых UI контролов.
  4. Класс Messenger позволяет организовывать обмен сообщениями внутри приложения.
  5. Класс DispatcherHelper облегчает жизнь при работе с потоками.

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

Давайте начнем с команд. Т.к. Теперь у нас есть RelayCommand, то мы можем избавиться от класса CustomCommand. Теперь команда AddNewTagCommand будет выглядеть вот так:

public ICommand _addNewTagCommand;
public ICommand AddNewTagCommand
{
get
{
if (_addNewTagCommand == null)
_addNewTagCommand = new RelayCommand(AddNewTagExecute);

return _addNewTagCommand;
}
}

public void AddNewTagExecute()
{
Tags.Add(NewTag);
NewTag = string.Empty;
}

Обратите внимание, что логика команды переехала во ViewModel.

Кроме реализации ICommand из нашего ViewModel ушла также и реализация INotifyPropertyChanged. Вместо этого теперь наш ViewModel наследуется от ViewModelBase, в котором INotifyPropertyChanged уже реализован.

public sealed class MainViewModel : ViewModelBase

На этом все. В следующей статье я рассмотрю примеры использования EventToCommand behavior’a, Messenger’a и DispatcherHelper’a.

Исходный код можно скачать отсюда

Sunday, September 12, 2010

MVVM: ViewModel-First approach

Я решил ненадолго отложить обзор MVVM тулкитов, а вместо этого продолжить разбираться с самим паттерном.

В предыдущей статье я описал основные блоки MVVM, их ответственности и на простом примере показал взаимодействие между ними. Тот вариант реализации MVVM называется View-First, т.к. именно View через свое свойство DataContext знает про ViewModel, а ViewModel наоборот абсолютно ничего не знает про View.

MVVM-ViewFirst

Сегодня я хочу описать другой подход к организации взаимодействия между блоками в паттерне MVVM. Называется он ViewModel-First и отличается от предыдущего тем, что здесь ViewModel знает про View c которым работает и может вызывать его методы, подписываться на его события и т.д.

MVVM-ViewModelFirst

Зачем ViewModel’у может понадобиться знать про View? Как правило для того чтобы управлять состоянием UI, но при этом не иметь жестких ссылок на UI контролы.

Для того чтобы применить ViewModel-First подход нужно сделать следующее:

  1. Определить максимально узкий и независимый от элементов UI интерфейс, необходимый для взаимодействия с View из ViewModel
  2. Имплементировать этот интерфейс во View
  3. Каким-либо образом пропихнуть экземпляр этого интерфейса во ViewModel

Давайте на примере посмотрим как можно реализовать ViewModel-First подход. За основу возьмем приложение управляющее списком тегов из предыдущей статьи. Допустим у нас появилась новая задача: «Сделать так чтобы при добавлении нового тега в Listbox’e выделенным становился 4й видимый элемент списка».

Давайте немного подумаем :) Сейчас у нас добавляет новые элементы в список команда AddNewTagCommand, определенная на ViewModel. При этом она просто добавляет новый элемент в коллекцию Tags и абсолютно ничего не знает про то, что эта коллекция является источником для Listbox’а. И это очень хорошо, что не знает. В этом как раз одна из идей MVVM – ViewModel независящий от UI контролов.

С другой стороны именно при добавлении нового элемента нам нужно управлять выбранным элементом в Listbox’e. С точки зрения разделения ответственности между блоками в паттерне MVVM, управление состоянием UI контролов - это однозначно зона ответственности блока View.

Ладно, меньше слов – больше кода. Давайте посмотрим как решить поставленную задачу. Для начала определим интерфейс через который ViewModel будет взаимодействовать с View. Нам хватит одного метода. Назовем его SetReadyState. ViewModel будет знать, что после добавления элемента нужно дернуть у View этот метод, чтобы View мог привести себя в состояние готовности. При этом ViewModel’у незачем знать, что именно там будет делать View. Более того разные View могут делать там разные вещи.

internal interface IView
{
void SetReadyState();
}

Т.к. нам потребуется доступ к ListBox’у, то надо дать ему имя:

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

Теперь давайте посмотрим на имплементацию интерфейса во View.

public partial class MainWindow : Window, IView
{
...

public void SetReadyState()
{
TagsListView.SelectedIndex =
GetFirstVisible(TagsListView) + 3;
}
...
}
ViewModel тоже нужно будет модифицировать так, чтобы он мог принимать интерфейс IView. Мы сделаем это через дополнительный параметр конструктора.

internal sealed class MainViewModel : INotifyPropertyChanged
{
private IView _view;

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

После этого мы сможем в команде AddNewTagCommand после добавления нового элемента вызвать у View метод SetReadyState.

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

_viewModel._view.SetReadyState();
}

И, наконец, последнее изменение. Надо при инициализации приложения пропихивать View в конструктор ViewModel.

private void Application_Startup(object sender, StartupEventArgs e)
{
var mainWindow = new MainWindow();
Current.MainWindow = mainWindow;

var viewModel =
new MainViewModel(new DataProvider(), mainWindow);

mainWindow.DataContext = viewModel;
mainWindow.Show();
}

Исходный код можно скачать отсюда

P.S. Небольшое пояснение.

Я перечитал еще разок эту и предыдущую статью про MVVM и решил прояснить вот такой момент, который может показаться противоречивым. В первой статье я написал что логика по управлению состоянием UI – это зло и ее надо переносить во ViewModel. А в этой статье наоборот написал, что управление состоянием UI контролов - это зона ответственности блока View.

На самом деле тут нет противоречия. Если использовать оригинальные термины, то View отвечает за UI Logic, а ViewModel за Presentation Logic. Под UI Logic я понимаю все операции, которым необходим непосредственный доступ к UI контролам такие как установка фокуса, смена выделенного элемента списка и т.д. А Presentation Logic – это операции, которые управляют состоянием UI при этом, не имея прямого доступа к UI. Например, команда AddNewTagCommand из нашего демо.

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.