Тема, конечно, неоднократно поднятая, но я все равно напишу шпаргалку на тот случай, если вдруг когда-нибудь забуду. А то с WCSF тоже вроде разобралась, а прошло два месяца со сдачи проекта, и уже ничего и не помню...
Сразу предупрежу, что статья предназначена для того, чтобы быстро въехать в MVVM или быстро его вспомнить. Так что описание будет предельно краткое и схематичное.
Итак, MVVM. Расшифровывается как Model-View-ViewModel. Рассмотрим на примере.
Пример
Для начала отмечу, что для упрощения нашей жизни с данным паттерном добрые люди разрабатывают MVVM Toolkit, включающий шаблон для Visual Studio:

Для данного проекта сразу создается следующая структура файлов:

Задача
Возьмем какую-нибудь каноничную задачу. Например, отображение списка книг читального зала. У книги есть:
- Название
- Автор
- Доступное количество
Ну и хватит пока. Добавим немного интерактивности. Пусть к нам ходят читатели и то и дело берут книги почитать или возвращают их обратно. А нам надо в любой момент знать, сколько экземпляров той или иной книги у нас осталось и можем ли мы ее выдать.
На примере такой незатейливой задачи мы и рассмотрим MVVM
Model
Как несложно догадаться, Model — это сущности системы. У нас модель будет состоять из одного простого класса:
Book.cs
class Book { public string Title { get; set; } public string Author { get; set; } public int Count { get; set; } public Book(string title, string author, int count) { this.Title = title; this.Author = author; this.Count = count; } }
ViewModel
ViewModel — это, пожалуй, ключевой момент всей этой истории. Это такие специальные классы, которые:
- Осуществляют связь между моделью и формой.
- Отслеживают изменения в данных, произведенные пользователем.
- Обрабатывают логику работы View (механизм команд)
В учетом датабиндинга в WPF всё это дает замечательный результат: в C#-коде формы становится совсем не надо ничего писать. Впрочем, обо все по порядку. Напишем ViewModel для нашей модели:
Book.cs
class BookViewModel : ViewModelBase { public Book Book; public BookViewModel(Book book) { this.Book = book; } public string Title { get { return Book.Title; } set { Book.Title = value; OnPropertyChanged("Title"); } } public string Author { get { return Book.Author; } set { Book.Author = value; OnPropertyChanged("Author"); } } public int Count { get { return Book.Count; } set { Book.Count = value; OnPropertyChanged("Count"); } } }
BookViewModel унаследован от класса ViewModelBase, который заботливо сгенерил нам MVVM Toolkit. ViewModelBase же, в свою очередь, реализует интерфейс INotifyPropertyChanged и содержит функцию OnPropertyChanged. Все это нужно для того, чтобы всегда можно было вызвать событие "изменилось такое-то поле". Как видно в коде, при любом изменении поля мы такое событие вызываем и передаем в качестве параметра его название. Потом на форме биндинг может это событие обработать и, как следствие, интерфейс и ViewModel всегда будут друг с другом синхронизированы. Впрочем, это я опять забегаю вперед.
Помимо BookViewModel у нас есть еще класс MainViewModel, уже сгенерированный и даже связанный с формой. Добавим в него поле:
ObservableCollection<BookViewModel> BooksList { get; set; }
ObservableCollection — это такая специальная коллекция, которая умеет отслеживать изменения в себе. Также слегка изменим конструктор:
public MainViewModel(List<Book> books) { BooksList = new ObservableCollection<BookViewModel>(books.Select(b => new BookViewModel(b))); }
View
Это и есть наше окно, либо User Control. У любого FrameworkElement-а WPF есть такое поле DataContext. DataContext может быть любым object-ом, иметь какие угодно поля, а его главная задача — являться источником данных для Databinding-а. Форма у нас всего одна, DataContext для нее заполняется в методе OnStartup, что в App.xaml.cs. Немного модифицируем то, что сделал нам MVVM Toolkit, получится следующее:
App.xaml.cs
private void OnStartup(object sender, StartupEventArgs e) { List<Book> books = new List<Book>() { new Book("Колобок", null, 3), new Book("CLR via C#", "Джеффри Рихтер", 1), new Book("Война и мир", "Л.Н. Толстой", 2) }; MainView view = new MainView(); // создали View MainViewModel viewModel = new ViewModels.MainViewModel(books); // Создали ViewModel view.DataContext = viewModel; // положили ViewModel во View в качестве DataContext view.Show(); }
Осталось написать XAML-код формы. Он прост и незатейлив:
MainView.xaml
<Window x:Class="SampleMVVM.Views.MainView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:c="clr-namespace:SampleMVVM.Commands" Title="Main Window" Height="400" Width="800"> <ListView ItemsSource="{Binding BooksList}"> <ListView.ItemTemplate> <DataTemplate> <Border BorderBrush="Bisque" BorderThickness="1" Margin="10"> <StackPanel Margin="10"> <TextBlock Text="{Binding Title}" FontWeight="Bold"/> <TextBlock Text="{Binding Author}" /> <StackPanel Orientation="Horizontal"> <TextBlock Text="Осталось:" /> <TextBlock Text="{Binding Count}" FontWeight="Bold" Margin="10,0"/> <TextBlock Text="шт" /> </StackPanel> </StackPanel> </Border> </DataTemplate> </ListView.ItemTemplate> </ListView> </Window>
Обратите внимание на конструкцию Binding в разметке формы. Именно таким образом можно привязывать поля объекта, находящегося в DataContext-е, к атрибутам контролов. Мы не написали ни строчки кода, но тем не менее при запуске получим следующее:

Редактирование
Сделаем так, что для выделенной в списке книги будет открываться редактор. Изменим XAML-разметку формы:
MainView.xaml
<Window x:Class="SampleMVVM.Views.MainView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:c="clr-namespace:SampleMVVM.Commands" Title="Main Window" Height="400" Width="350"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <ListView ItemsSource="{Binding BooksList}" IsSynchronizedWithCurrentItem="True"> <ListView.ItemTemplate> <DataTemplate> <Border BorderBrush="Bisque" BorderThickness="1" Margin="10"> <StackPanel Margin="10"> <TextBlock Text="{Binding Title}" FontWeight="Bold"/> <TextBlock Text="{Binding Author}" /> <StackPanel Orientation="Horizontal"> <TextBlock Text="Осталось:" /> <TextBlock Text="{Binding Count}" FontWeight="Bold" Margin="10,0"/> <TextBlock Text="шт" /> </StackPanel> </StackPanel> </Border> </DataTemplate> </ListView.ItemTemplate> </ListView><ContentControl Grid.Column="1" Content="{Binding BooksList}"> <ContentControl.ContentTemplate> <DataTemplate> <Border BorderBrush="Bisque" BorderThickness="1" Margin="10"> <StackPanel Margin="10"> <TextBlock Text="Название:"/> <TextBox Text="{Binding Title, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,10"/> <TextBlock Text="Автор:"/> <TextBox Text="{Binding Author, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,10"/> </StackPanel> </Border> </DataTemplate> </ContentControl.ContentTemplate> </ContentControl></Grid> </Window>
Стоит обратить внимание на конструкцию UpdateSourceTrigger=PropertyChanged в строке биндинга. Это значит, что любое изменение, производимое в данном поле, будет немедленно отражаться на источнике. Это легко увидеть:

Если этого не написать, источник будет обновляться только по окончании редактирования (т.е. когда контрол будет терять фокус). Это может привести к следующей ошибке интерфейса: когда жмешь "Сохранить", сохраняется все, кроме только что измененного поля.
Команды
Добавим в приложение функциональности. Пусть некие читатели берут книги и возвращают. Соответственно, сделаем две кнопки — «Выдать» и «Забрать—, меняющие количество имеющихся в наличии книг. Если книг не осталось (Count = 0), кнопка «Выдать» должна дизаблиться.
В MVVM не пишутся обработчики событий. Функции, которые нужно выполнять контролам, пишутся во ViewModel и биндятся к контролам точно так же, как поля. Только используется механизм команд.
Команда должна представлять из себя экземпляр класса, реализующего интерфейс ICommand. К счастью, MVVM Toolkit снова нам помог и сгенерил целых два таких класса — DelegateCommand для реализации команды без параметров и DelegateCommand<T> — для реализации команды с параметром типа T.
Мы параметры передавать не будем. Код во ViewModel будет таков:
BookViewModel.cs
#region Забрать private DelegateCommand getItemCommand; public ICommand GetItemCommand { get { if (getItemCommand == null) { getItemCommand = new DelegateCommand(GetItem); } return getItemCommand; } } private void GetItem() { Count++; } #endregion #region Выдать private DelegateCommand giveItemCommand; public ICommand GiveItemCommand { get { if (giveItemCommand == null) { giveItemCommand = new DelegateCommand(GiveItem, CanGiveItem); } return giveItemCommand; } } private void GiveItem() { Count--; } private bool CanGiveItem() { return Count > 0; } #endregion
Обратите внимание, что этот код добавляется в BookViewModel, а не в MainViewMode. Дело в том, что мы будем добавлять кнопки в ContentControl, DataContext-ом которого является именно BookViewModel.
С первой командой все ясно. Создали команду, и в назначили ей в качестве действия метод GetItem, который и будет вызываться при ее активации. Со второй немного интереснее, но тоже просто. Помимо того, что она выполняет некоторое действие, она еще и может проверять с помощью метода CanGiveItem(), может она выполняться или нет.
В XAML-разметку нашей формы добавим следующее
MainView.xaml
<ContentControl Grid.Column="1" Content="{Binding BooksList}"> <ContentControl.ContentTemplate> <DataTemplate> <Border BorderBrush="Bisque" BorderThickness="1" Margin="10"> <StackPanel Margin="10"> <TextBlock Text="Название:"/> <TextBox Text="{Binding Title, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,10"/> <TextBlock Text="Автор:"/> <TextBox Text="{Binding Author, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,10"/><StackPanel Orientation="Horizontal"> <Button Content="Выдать" Command="{Binding GiveItemCommand}" Margin="10,0" /> <Button Content="Забрать" Command="{Binding GetItemCommand}" Margin="10,0" /> </StackPanel></StackPanel> </Border> </DataTemplate> </ContentControl.ContentTemplate> </ContentControl>
Вот и все. Мы получили требуемую функциональность. Количество экземпляров книги увеличивается и уменьшается, а когда их становится 0, кнопка «Выдать» дизаблится (благодаря упомянутому CanGiveItem).

Итого
В приведенном приложении все данные и реализация логики вынесены в отдельное место. В C#-коде формы мы не добавили ни строчки. XAML понятен и прекрасен. Благодаря паттерну MVVM в коде легко разобраться и легко сопровождать.

26 коммент.:
Вы -- молодец!
Обрабатывать SelectionChanged не совсем правильно. На помощь приходит CollectionViewSorce с методом GetDefaultView. Он возвращает представление для коллекции, котоое реализует ICollectionView. А уж в ICollectionView есть все необходимое для работы (нотификация изменения текущей позиции, фильтры и прочие полезности).
Cлой ViewModel позволяет полностью описать бизнес-логику приложения (Мастер-детайлы, общие справочники и т.п.). Во View пишется только то управление для GUI, которое невозможно реализовать из ViewModel(т.к. VIewModel не должна ничего знать о своем представлении): управление фокусировками, хоткеи и т.п.
Спасибо за кейворды. Когда возникнет соответствующая задача, буду знать куда смотреть.
Еще бы знать, как на MVVM модальные окна правильно делать.
Один из вариантов - пишется общая обертка для показа View в модальном режиме. Обертка представляет собой команду, которая показывает модальную форму (которая может содержать кнопки Ок, Применить, Отмена в различных комбинациях), передавая для нее датаконтекст и шаблон представления. Также указывается команда, которая будет выполнятся в случае "положительного" результата обработки формы.
Команда биндится в XAMLе, в cs-файле ни единой строчки по прежнему нет.
Это в общих чертах, там много тонкостей :)
Я в общих чертах так и делаю.
Проблема в том, что, чтобы модальное окно закрылось, надо задать ему DialogResult. И для этого приходится обрабатывать OnClick кнопки и в обработчике писать DialogResult = true.
Хотя DialogResult тоже забиндить можно...
Некоторые вещи разумнее писать в коде. Стремление абсолютно все вынести в разметку может принести вред (наступал на эти грабли).
Кстати, OnClick кнопки - "невьюмодельно", нужно завести команду и прибиндить ее к кнопке.
Форму ведь необязательно по клику кнопки будут закрывать.
Вот это меня, собственно, и смущало. OnClick некрасиво и не вписывается в MVVM; биндить WindowResult можно, но он во ViewModel тоже не в тему совершенно, а окно по Ok закрыть хочется. Что же делать?
"Что же делать?"
Cinch
Если Вы имели в виду вот этот Cinch, то меня несколько смущает отсутствие релизов. Не хочу завязываться в рабочем приложении на нестабильный фреймворк.
Здравствуйте Дарья! Спасибо Вам за Вашу статью! У меня в процессе её чтения возник ряд вопросов, без которых не получается понять весь объём представленной Вами информации до конца. Был бы весьма признателен Вам за ответы на них:
В XAML-разметке вашего класса MainView, присутствуют элементы ListView и ContentControl.
Для ListView назначен источник данных: ItemsSource="{Binding BooksList}".
Для ContentControl этот же объект назначен в качестве содержимого: Content="{Binding BooksList}".
В подобных ситуациях для ListView я пишу такую разметку:
А для ContentControl пишу так:
Приведённая мною выше разметка мне понятна: в одном элементе (ListView) я указываю источником данных тип ObservableCollection, а во втором (ContentControl) - отслеживаю значение текущего выбора, произведённого в первом элементе.
Ваша разметка более лаконична и так же работает. Однако я никак не могу понять почему... В элементе ContentControl в качестве источника данных Вы указали Content="{Binding BooksList}". Судя по поведению ContentControl, он автоматически привязался к Items.CurrentItem? Искал в MSDN информацию, в которой бы упоминалось такое поведение - не нашел (к сожалению).
Правильно ли я понимаю - если свойству Content объекта ContentControl назначать класс, реализующий интерфейс ItemsControl (как это сделано в Вашем примере), то на самом деле объект ContentControl автоматически будет искать данные в ItemsControl.Items.CurrentItem? Если "да", то касается ли это только ItemsControl?
п.с. в предыдущем сообщении текстовый редактор сайта затёр мой код:
В подобных ситуациях для ListView я пишу такую разметку:
ListView Name="myList" ItemsSource="{Binding BooksList}"
А для ContentControl пишу так:
ContentControl Grid.Column="1" Content="{Binding ElementName=myList, Path=SelectedItem}"
2 Андрей:
Все дело в атрибуте IsSynchronizedWithCurrentItem. Почитайте про него.
я читал и помню, что он синхронизирует выбранный Item с источником - ItemsControl.Items.CurrentItem в случае Вашего примера. Но мой вопрос не в этом, а в том, что объект ContentControl, получив в качестве источника набор данных, определяет, что следует искать не абы какое свойство (для представления данных), а именно ItemsControl.Items.CurrentItem...
Ведь если в качестве источника в примере рассматривался бы string[], то в этом случае ContentControl не смог бы пройтись по иерархии свойств и обратиться к CurrentItem, поскольку в string[] этого свойства нет.
Дарья, этот Ваш текст с кусками кода можно отнести к категории интересного и полезного.
Спасибо Вам!
Спасибо, Дарья, за полезную шпаргалку!
Подскажите плиз, если кому не сложно, при такой модели как написать чтобы из MainViewModel закрывать приложение и вызывать стандартные команды окна (Close, Minimize, Move) ?
Спасибо вам. Если б не вы еще бы пару дней втыкал длинную, не очень понятно написанную статью с мсдн, а так за 5 минут разобрался
Дарья, огромное Вам спасибо за UpdateSourceTrigger=PropertyChanged. Я долго искал, как добиться такого поведения TextBox'а
Большое спасибо за шпаргалку!
Вопрос, как в рамках mvvm обработать следующий случай: в момент работы с вьюшкой произошли изменения с источником модели(допустим, у экземпляра Book по каким-то независимым причинам "изнутри" изменилось свойство Title). Как BookViewModel узнает об этом изменении?
Андрей,
>>Но мой вопрос не в этом, а в том, что объект ContentControl, получив в качестве источника набор данных, определяет, что следует искать не абы какое свойство (для представления данных), а именно ItemsControl.Items.CurrentItem
если объект одноэлементного множества (в данном случае ContentControl) связан с представлением коллекции, он автоматически привязывается к CurrentItem представления
источник: http://msdn.microsoft.com/ru-ru/library/ms752347.aspx#data_conversion
"Еще бы знать, как на MVVM модальные окна правильно делать." - посмотрите, может это как раз то что Вам нужно - http://www.silverlightshow.net/items/ModalDialogs-IEditableObject-and-MVVM-in-Silverlight-4.aspx
(правда, это SL, но нестрашно)
PS. Спасибо Вам за блог, очень приятно и полезно ))
Признаюсь, в голове некоторая каша.
Я вот пытаюсь понять как это применяется например во взаимодействии с EntityFramework. По идее сгенерированные объекты EntityFramework как-раз должны представлять прослойку Model, но как именно это реализуется пока не понимаю. Может есть какие-то примеры?
Добрый день Дарья!
На моем блоге есть похожая тема(реализация Mvvm, только в silverlight), предлагаю нам обменяться ссылками. Я разместил вашу ссылку у себя на странице статьи: http://rio900.com/archives/156/mvvm-silverlight-%D0%BF%D1%80%D0%B0%D0%BA%D1%82%D0%B8%D0%BA%D0%B5/
А у меня этот пример не работает ((
Сделал пример до редактирования данных (только просмотр)..
Объект viewModel имеет данные, а ListView.Items.Count равняется нулю.. и в окне не показывает список книг..
Отличная статья, но в корне не согласен с Вашим подходом к Model и ViewModel.
Model - действительно бизнес сущность системы и ее логика не зависит от ViewModel.
Т.е. если у книги, например, есть сводное служебное поле из фамилии автора и названия, то в Вашей версии в каждой из ViewModel (редакторов может быть несколько) Вам придется реализовать что-то вроде:
public string Author
{
set
{
Book.Author = value;
OnPropertyChanged("Author");
OnPropertyChanged("Summary"); // не забыть добавить во все
}
+ кроме того продублировать все эти свойства в каждой из ViewModel.
А это уже дублирование и явный повод для рефакторинга )
Итого, решение:
1. добавляем базовый класс для моделей с реализацией INotifyPropertyChanged.
2. Наследуем модель от п.1.
3. из ViewModel убираем все врапперы свойств модели с нотификациями.
4. из View биндимся Binding Book.Title. тем более что свойство Book у вас уже есть.
Аналогичная ситуация со внутренностями GiveItem() и CanGiveItem() расположенными во ViewModel. То что мы не можем выдать больше необходимого это опять же бизнес логика, а бизнес логике место в модели. В противном случае, если появится требование всегда оставлять в наличии книги Маркса в одном экземпляре, то есть вероятность что в одной из ViewModel можно этот ньюанс упустить.
в подкрепление моей версии ссылки на почитать и посмотреть:
http://archive.msdn.microsoft.com/mag200902MVVM
http://www.codeproject.com/KB/WPF/MVVMForDummies.aspx
Согласен с вами Alex, на себе уже проверено - так действительно лучше.
Отправить комментарий