Статьи

MVVM Light — что такое Мессенджер?

MVVM Light предлагает класс, специфичный для каждого экземпляра ViewModel, который используется в контексте приложения — Messenger. Кто-то может спросить — что это такое и зачем это нужно? Ответ прост — в более сложном приложении экземпляры ViewModel не являются отдельными объектами. Скорее всего, вы хотели бы передавать конкретные данные из одной модели в другую, и Messenger облегчает этот процесс.

ПРИМЕЧАНИЕ. Я использую приложение Windows Phone 7 Silverlight для демонстрации.

Давайте посмотрим на конкретный исходный код, чтобы проиллюстрировать его деятельность. Изначально у меня есть стандартная модель представления теста (названная TestViewModel):

public class TestViewModel : ViewModelBase
{
    public TestViewModel()
    {
        
    }

    private string tS = "tS";
    public string TestString
    {
        get
        {
            return tS;
        }
        set
        {
            tS = value;
            RaisePropertyChanged("TestString");
        }
    }
}

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

public class MainViewModel : ViewModelBase
{
    public MainViewModel()
    {

    }

    private string pA = "pA";
    public string PropertyA
    {
        get
        {
            return pA;
        }
        set
        {
            RaisePropertyChanged("PropertyA");
        }
    }

}

Обе модели доступны через экземпляр ViewModelLocator (если вы не уверены, что это такое, посмотрите здесь ). Именно этот экземпляр объявлен в файле App.xaml как ресурс, который доступен по всему миру:

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

Теперь на моей главной странице я должен убедиться, что у меня установлен правильный DataContext, чтобы он указывал на правильный ViewModel в правильном экземпляре ViewModelLocator:

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

Чтобы показать, что модель связана, я создал TextBlock на самой странице, а также кнопку (ее назначение будет обсуждаться позже).

<StackPanel x:Name="LayoutRoot">
    <Button Height="100" Content="Test" Click="Button_Click"></Button>
    <TextBlock Text="{Binding Path=PropertyA}"></TextBlock>
</StackPanel>

Обратите внимание, что TextBlock имеет свойство Text, привязанное к PropertyA. Учитывая DataContext, он будет расположен в MainViewModel. Вот он, самый базовый скелет. Запустите приложение и убедитесь, что привязка работает правильно. Вы должны увидеть что-то похожее на это:

pA в TextBlock означает, что он был успешно связан с PropertyA.

Теперь я добавлю новую страницу и установлю ее DataContext для TestViewModel. Таким образом, у меня есть две отдельные страницы, которые извлекают данные через один и тот же ViewModelLocator, но из разных экземпляров ViewModel:

<phone:PhoneApplicationPage 
    x:Class="MVVMLight.SecondaryPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    SupportedOrientations="Portrait" Orientation="Portrait"
    mc:Ignorable="d" d:DesignHeight="800" d:DesignWidth="480"
    DataContext="{Binding Test, Source={StaticResource Locator}}">

    <TextBlock Text="{Binding TestString}"></TextBlock>
</phone:PhoneApplicationPage>

Я постарался сделать его максимально простым, поэтому вы просто видите TextBlock, привязанный к свойству TestString.

Теперь пришло время изменить MainViewModel, и именно здесь я начинаю всю историю Messenger. Как вы знаете, когда свойство изменяется, для его повторной привязки в MVVM Light необходимо вызвать RaisePropertyChanged («PROPERTY_NAME»); Этот метод перегружен (который унаследован от ViewModelBase), который может передавать (информировать другие экземпляры ViewModel), что свойство изменилось.

Вот что я изменил в MainViewModel.cs:

private string pA = "pA";
public string PropertyA
{
    get
    {
        return pA;
    }
    set
    {
        string oldValue = pA;
        pA = value;

        RaisePropertyChanged("PropertyA", oldValue,value,true);
    }
}

Обратите внимание, что я передаю и старое значение, и новое как часть трансляции. Переданные данные помещаются в PropertyChangedMessage.

Итак, давайте посмотрим, что произойдет, когда я изменю свою собственность. Для этого мне нужно получить доступ к экземпляру MainViewModel в используемом мной ViewModelLocator и установить значение PropertyA. Вот где я использую кнопку, которую я положил на странице выше:

private void Button_Click(object sender, System.Windows.RoutedEventArgs e)
{
    ViewModel.ViewModelLocator.MainStatic.PropertyA = "Hello, this changed!";
}

Запустите приложение и посмотрите, что происходит, когда вы нажимаете кнопку. Свойство успешно изменено, но это все. Что случилось с RaisePropertyChanged, который должен был передать сообщение другим экземплярам ViewModel?

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

В конструкторе TestViewModel я поместил эту строку:

Messenger.Default.Register<PropertyChangedMessage<string>>(this, (s) => MessageBox.Show(s.NewValue));

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

Это классическая служба обмена сообщениями, которая позволяет легко обмениваться данными между инициализированными экземплярами ViewModel. И по умолчанию в вашем ViewModelLocator зарегистрированные экземпляры ViewModel автоматически создаются при запуске (если не используется другая конфигурация).

Как вы видели из приведенного выше фрагмента (до моментального снимка Reflector), я в основном регистрирую прослушиватель для сообщений определенного типа. В этом случае я ищу экземпляр PropertyChangedMessage, который несет строку. это представляет получателя — очевидно, я могу установить его для другого экземпляра ViewModel, но на данный момент в этом нет необходимости.

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

ПРИМЕЧАНИЕ. Внутри анонимного метода у меня есть доступ к свойствам, доступным в получателе.

S параметр не строка — это PropertyChangedMessage <строка>. И, как я уже говорил, у меня есть доступ к старому значению свойства (которое было изменено), а также к новому:

Запустите приложение сейчас и посмотрите, что произойдет. В тот момент, когда вы нажмете кнопку и измените значение PropertyA, вы увидите всплывающее окно MessageBox с новым значением. Я могу передать любой объект, который я хочу передать в контексте PropertyChangedMessage, при условии, что свойство отправителя имеет к нему доступ.

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

Messenger.Default.Register<PropertyChangedMessage<string>>(ViewModel.ViewModelLocator.MainStatic, (s) => TestString = s.NewValue);

Поскольку у меня есть дополнительная страница, привязанная к экземпляру ViewModel (TestViewModel), я могу показать ее после изменения значения, поэтому в том же обработчике события Button_Click, который вызвал изменение свойства, я собираюсь добавить строку навигации:

NavigationService.Navigate(new Uri("/SecondaryPage.xaml", UriKind.Relative));

Как только я перейду туда, я должен увидеть такую ​​страницу:

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

  • NotificationMessage — используется для доставки чисто строковых сообщений.
  • DialogMessage — используется для запроса отображения MessageBox. Приведенный выше пример с MessageBox в обратном вызове можно легко изменить и воспользоваться преимуществами DialogMessage.
  • GenericMessage <T> — используется для передачи любого объекта.
  • NotificationMessageWithCallback — такой же, как NotificationMessage, но с обратным вызовом, который будет запущен после доступа к переданному уведомлению.

Отправка так же проста, как вызов метода Send для экземпляра по умолчанию. Например, я создал простое DialogMessage для передачи:

DialogMessage d = new DialogMessage("Sample dialog",(m) => { Debug.WriteLine(m.ToString()); });
d.Button = MessageBoxButton.OKCancel;
d.Caption = "Test";

Тогда все, что мне нужно сделать, это позвонить Отправить:

Messenger.Default.Send<DialogMessage>(d);

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

Messenger.Default.Register<DialogMessage>(this, (s) => {
MessageBoxResult m = MessageBox.Show(s.Content, s.Caption, s.Button);
s.ProcessCallback(m);
});

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

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

Например, приведенный выше вызов для отправки сообщения может быть модифицирован следующим образом:

Messenger.Default.Send<DialogMessage>(d,"TEST_TOKEN");

На принимающей стороне я бы запросил только этот токен:

Messenger.Default.Register<DialogMessage>(this, "TEST_TOKEN", (s) => {
                MessageBoxResult m = MessageBox.Show(s.Content, s.Caption, s.Button);
                s.ProcessCallback(m);     
            });

Кроме того, есть важный комментарий от Лорана Буньона (создателя MVVM Light), который следует упомянуть здесь (вы также можете увидеть его в разделе комментариев ниже):

  • Хотя использование Messenger.Default является распространенным явлением, также можно создавать экземпляры другого экземпляра Messenger для «частных трансляций». Также возможно передать экземпляр Messenger классу ViewModel (в конструкторе), если вы захотите использовать этот экземпляр вместо Default.
  • Вы можете отправлять действительно любой тип, от простых значений (строки и т. Д.) До более сложных объектов (например, PropertyChangedMessage или, например, SelectedCustomer и т. Д.).
  • Messenger полезен не только для ViewModels, но и в любое время, когда вам нужны два слабо связанных объекта для общения. Я знаю, что некоторые люди используют Messenger, например, в приложении WinForms.
  • И наконец, все показанное здесь также действительно в WPF и Silverlight (3 и 4) ?