MVVM Light Toolkit - Команды и сообщения

Введение

В этой статье мы рассмотрим основные возможности фреймворка MVVM Light Toolkit, такие как использование команд и взаимодействие на основе сообщений.

В качестве примера мы разработаем приложение для Windows Phone 7, которое должно отображать список книг. Кроме списка книг, на интерфейсе должны быть расположены две кнопки – «Юмор» и «Драма», нажатие на которые должно отмечать «галкой» книги соответствующего жанра.

Приложение будет реализовано с использованием паттерна MVVM.

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

Пререквизиты

Для использования материала необходимо установить сборки и шаблоны MVVM Light V3 (stable). Их можно скачать по ссылкам на странице с описанием процесса установки.

Создаем каркас приложения

Создадим новый проект. В качестве шаблона проекта используем шаблон MVVM Light.

New Project Dialog in Visual Studio 2010

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

Составные части MVVM

Как вы уже знаете, аббревиатура MVVM расшифровывается как Model (Модель) – View (Представление) – ViewModel (Модель представления). Для реализации приложения, основанного на этом шаблоне проектирования нам необходимо реализовать каждый из этих компонентов как минимум один раз.

Модель

В нашем приложении мы будем работать с книгами и жанрами. Соответственно, нам потребуется создать типы Book и BookGenre.

namespace MvvmLightDemo.Model  
{
    public class Book
    {
        public string Title { get; set; }

        public string Author { get; set; }

        public BookGenre Genre { get; set; }
    }
}

Приведенный выше код с определением сущностей мы помещаем в папку Model. Обратите внимание, что класс Book не реализует интерфейс INotifyPropertyChanged. По идее, класс Книга не должен знать ничего о специфике реализации среды, где он будет использоваться. Этот класс предоставляет информацию только о книге и не пытается казаться тем, чем он на самом деле не является.

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

Для реализации представления мы будем использовать файл MainPage.xaml, созданный из шаблона проекта. Для доведения его до соответствия требованиям, нам необходимо добавить два элемента Button и один ListBox. Давайте представим, что мы дизайнеры и откроем этот файл в Expression Blend.

Обратите внимание, что мы работаем только с разметкой и ничего не добавляем в code behind (мы – дизайнеры!).

Мы решаем, что в списке книг нам необходимо показывать чекбокс слева от строки с названием и автором книги. Причем название и автор у нас – одна строка.

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

Разместив элементы управления по своему вкусу, мы можем снять с себя полномочия дизайнера и вернуться в Visual Studio 2010 к роли разработчика.

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

Прежде чем продвинуться дальше, давайте подведем промежуточный итог.

Что мы имеем?

Мы имеем структуру хранения информации о книгах, которая описана типом Book. Мы знаем, какие жанры книг нам доступны. Дизайнер спроектировал интерфейс нашего приложения.

Что нам осталось сделать?

Нам необходимо сделать так, чтобы список книг можно было отобразить на интерфейсе, что бы нажатие кнопок приводило к изменению отображения галок в чекбоксах. Иными словами, нам необходимо связать Модель и Представление. Эта роль в шаблоне MVVM отведена последнему компоненту – модели представления. Задача модели представления – преобразовать модель таким образом, чтобы представление могло отобразить необходимую информацию. Соответственно, если представление отображает книги в виде списка, то модель представления должна предоставить этот список. Так же модель представления отвечает за отработку взаимодействия с пользователем, который работает с представлением. Если пользователь отдает команду (нажимает кнопку, выбирает элемент в списке, и т.п.), то представление должно знать, к какому методу модели представления обратиться, что бы действие пользователя могло быть выполнено.

Таким образом, наша модель представления должна обеспечить для представления:

  1. Список книг
  2. Команду выбора книг предпочтительного жанра

Учитывая, что дизайнеры не ожидают от наших данных о книгах поле типа Boolean, которое бы отображало предпочтение пользователя, мы создадим еще одну модель представления для взаимодействия с неочевидным на первый взгляд представлением, которое «затаилось» внутри DataTemplate списка книг.

public class BookViewModel : ViewModelBase  
{
    private readonly Book _book;
    private BookGenre _favorite;

    public BookViewModel(Book book)
    {
        _book = book;
    }

    public string Title
    {
        get { return string.Format("{0} by {1}", _book.Title, _book.Author); }
    }

    public bool IsFavorite
    {
        get { return _book.Genre == _favorite; }
    }

    public override void Cleanup()
    {
        base.Cleanup();
    }
}

Определившись с BookViewModel, мы можем продолжить реализацию модели представления основного пользовательского интерфейса приложения.

Добавим демо данные в виде массива книг и коллекцию для хранения BookViewModel. Ниже мы рассмотрим подробнее каждый из добавленных элементов.

public class MainViewModel : ViewModelBase  
{
    //demo data source
    private Book[] DemoBooks = {
        new Book {Title = "Book 1", Author = "Pushkin A.S.", Genre = BookGenre.Drama},
        new Book {Title = "Book 2", Author = "Pushkin A.S.", Genre = BookGenre.Drama},
        new Book {Title = "Book 3", Author = "Pushkin A.S.", Genre = BookGenre.Drama},
        new Book {Title = "Book 4", Author = "Pushkin A.S.", Genre = BookGenre.Drama},
        new Book {Title = "Book 5", Author = "Chekhov A.P.", Genre = BookGenre.Humor},
        new Book {Title = "Book 6", Author = "Chekhov A.P.", Genre = BookGenre.Humor},
        new Book {Title = "Book 7", Author = "Chekhov A.P.", Genre = BookGenre.Humor},
        new Book {Title = "Book 8", Author = "Chekhov A.P.", Genre = BookGenre.Humor},
    };

    public ObservableCollection Books { get; set; }

    public MainViewModel()
    {
        Books = new ObservableCollection();
        DemoBooks.ToList().ForEach(book => Books.Add(new BookViewModel(book)));
    }
…
}

Обработка события нажатия на кнопку

В нашем представлении есть две кнопки, нажатие на которые необходимо обрабатывать.

Самый простой способ - повесить обработчик на событие Click и в нем вызывать методы модели представления. Этот подход порой хорошо работает на небольших проектах, в которых не используются MVC-подобные паттерны проектирования, или на проектах с коротким жизненным циклом.

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

Если мы пойдем по пути, описанному выше, наши модель представления и представление будут вынуждены знать о деталях реализации, и будут зависеть друг от друга, что в итоге приведет к тому, что в коде будут появляться «костыли» и «баги». Любое незначительное изменение в одном из связанных компонентов может привести к нарушению работы другого.

Использование MVVM Light Toolkit для обработки события нажатия на кнопку

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

В нашем случае, если пользователь нажал на кнопку «Юмор» мы должны показать ему, какие книги из списка относятся к этому жанру – каждая книга выбранного жанра должна быть отмечена галкой.

Для реализации мы воспользуемся следующими компонентами из MVVM Light Toolkit:

  • Класс RelayCommand<T>
  • EventToCommand behavior

Добавим в определение класса MainViewModel свойство и инициализируем его в конструкторе

public RelayCommand SetFavoriteCommand { get; set; }

SetFavoriteCommand = new RelayCommand(genre =>MessageBox.Show(genre), _ => Books.Count > 0);  

Конструктор RelayCommand принимает типизированный инстанс класса Action<T>, который и берет на себя работу по выполнению команды. Кроме того, RelayCommand предоставляет второй конструктор, во втором параметре которого можно передать условие доступности команды. В нашем случае мы используем именно этот конструктор. Критерием является необходимость наличия хотя бы одной книги в коллекции.

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

<Button Content="Humor"  
        Height="72"
        VerticalAlignment="Top"
        Margin="0,0,82,0">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <GalaSoft_MvvmLight_Command:EventToCommand Command="{Binding SetFavoriteCommand}"
                                                        CommandParameterValue="humor" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>  
<Button Content="Drama"  
        Height="72"
        VerticalAlignment="Top"
        Margin="0,0,0,0">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <GalaSoft_MvvmLight_Command:EventToCommand Command="{Binding SetFavoriteCommand}"
                                                        CommandParameterValue="drama" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>  

В коде выше следует обратить внимание на триггер и использование EventToCommand. Мы указываем тип события, команду, которую необходимо вызвать при возникновении события, и указываем параметр для команды.

Данную разметку можно добавить и через Expression Blend. Достаточно выбрать EventToCommand из списка Assets и перетащить его на нужный элемент интерфейса:

Настраиваем триггер:

Связываем с командой из дата-контекста:

Можно собрать приложение, поставить брейкпоинт на коде команды в конструкторе и проверить работу связки EventToCommand и RelayCommand в действии.

Использование MVVM Light Toolkit организации общения между компонентами

Мы только что успешно реализовали реагирование на пользовательский ввод. Давайте рассмотрим, каким образом MVVM Light Toolkit может помочь нам в организации взаимодействия между компонентами.

В нашем упрощенном примере мы хотим устанавливать галку в чекбоксах в зависимости от того какую кнопку нажмет пользователь.

Как и в случае с обработкой нажатия кнопки, мы можем просто изменить поле у каждой подходящей книги (у BookViewModel) и закончить с этим, через механизмы биндинга и нотификации об изменениях в полях данных мы увидим наши галки на своих местах.

Здесь опять возникает ситуация когда MainViewModel должна знать больше чем нужно об устройстве BookViewModel и становится зависима от деталей реализации этого класса.

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

В итоге, код обработчика команды будет представлять собой, в лучшем случае, список методов, а в худшем – один God метод длинной в тысячу строк. Нетрудно догадаться, что это увеличивает шансы на появление ошибок и приводит к снижению качества конечного продукта и усложняет его сопровождение.

Для того что бы избавиться от зависимостей, воспользуемся классом Messenger из MVVM Light Toolkit.

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

Для реализации этого подхода в нашем приложении внесем ряд изменений. Команда SetFavoriteCommand должна отправлять выбранный жанр в Messenger, а в конструкторе BookViewModel должна производиться подписка модели представления на сообщение о выборе жанра.

MainViewModel.cs:

SetFavoriteCommand = new RelayCommand(  
    genre => Messenger.Default.Send(new GenericMessage(
            genre == "humor"
                ? BookGenre.Humor
                : BookGenre.Drama)),
    _ => Books.Count > 0);

BookViewModel.cs:

public BookViewModel(Book book)  
{
    _book = book;
    Messenger.Default.Register< GenericMessage >(this, OnFavoriteGenreChanged);
}

private void OnFavoriteGenreChanged(GenericMessage msg)  
{
    _favorite = msg.Content;
    RaisePropertyChanged("Title");
    RaisePropertyChanged("IsFavorite");
}

Если через какое то время потребуется добавить новое действие в ответ на нажатие кнопок, то вся модификация сведётся к добавлению нового обработчика и подписке его на сообщение через Messenger.

В реальном приложении не следует использовать GenericMessage в явном виде, а объявить наследника и использовать уже его.

Заключение

MVVM Light Toolkit на мой взгляд весьма полезный набор классов, особенно хорошо подходящий для небольших задач, не требующих модульности или необходимости использовать IoC. Даже для сложных приложений, в которых я бы использовал Prism/Unity/MEF можно найти применение и для MVVM Light Toolkit.

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

Удачи!

Исходный код MvvmLightDemo.zip