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
            });
    }
);

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

3 comments:

  1. Здравствуйте.
    Начал новый проект с использованием MVVM Light Toolkit. Сильно досаждает полное отсутствие документации... Расскажите подробнее про ViewModelLocator. В частности интересует где нужно создавать View и соответствующие ViewModels в рамках тулкита.
    Например на главной форме есть кнопка, при клике на которую нужно показать новое окно. При этом ViewModel этого окна нужны начальные данные для инициализации.
    Сейчас сделано так: на клик повешена команда, реализация которой находится в MainViewModel. В этом обработчике создаю View и ViewModel (со всеми нужными параметрами в конструкторе), а потом показываю окно. Что-то подсказывает что это неправильный подход...

    ReplyDelete
  2. Danila, на этих выходных напишу про ViewModelLocator и постараюсь ответить на Ваш вопрос

    ReplyDelete
  3. Очень познавательно, но хотелось бы больше информации про варианты открытия новых окон из ViewModel в том числе диалоговых. А также контроль закрытия View внутри ViewModel и наоборот - закрытие View через ViewModel. Очень кратко написано про Messanger хочется разобраться более детально. Спасибо.

    ReplyDelete