Поиск на сайте: Расширенный поиск


Новые программы oszone.net Читать ленту новостей RSS
CheckBootSpeed - это диагностический пакет на основе скриптов PowerShell, создающий отчет о скорости загрузки Windows 7 ...
Вы когда-нибудь хотели создать установочный диск Windows, который бы автоматически установил систему, не задавая вопросо...
Если после установки Windows XP у вас перестала загружаться Windows Vista или Windows 7, вам необходимо восстановить заг...
Программа подготовки документов и ведения учетных и отчетных данных по командировкам. Используются формы, утвержденные п...
Red Button – это мощная утилита для оптимизации и очистки всех актуальных клиентских версий операционной системы Windows...
OSzone.net Microsoft Разработка приложений .NET Framework Компонент Messenger в MVVM Light Toolkit RSS

Компонент Messenger в MVVM Light Toolkit

Текущий рейтинг: 3 (проголосовало 2)
 Посетителей: 900 | Просмотров: 1112 (сегодня 0)  Шрифт: - +

В этой серии статей о шаблоне Model-View-ViewModel (MVVM) и MVVM Light Toolkit уже рассказано довольно многое с тех пор, как я начал ее почти год назад, — от использования IOC-контейнеров в MVVM-приложениях до способов обработки обращений между потоками и компонента DispatcherHelper в MVVM Light. Я также рассказал о создании команд (с RelayCommand и EventToCommand), сервисах представлений, таких как сервисы Navigation и Dialog, а также кратко рассмотрел компонент Messenger.

Компонент Messenger на самом деле является весьма мощным элементом MVVM Light Toolkit, часто прельщающим разработчиков своей простотой в использовании и в то же время вызывающим жаркие дискуссии из-за рисков, которые он может создать при неправильном применении. Этот компонент заслуживает отдельной статьи, поясняющей, как он работает, каковы риски, связанные с его использованием, и для каких сценариев он подходит больше всего.

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

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

Агрегация событий и упрощения в Messenger

Системы, подобные Messenger, иногда называют шинами событий или агрегаторами событий. Такие компоненты соединяют отправителя и получателя (также называемых публикатором и подписчиком соответственно). Когда MVVM Light создавалась, многие системы обмена сообщениями требовали, чтобы получатель или отправитель реализовали специфические методы. Например, мог существовать интерфейс IReceiver, который определял метод Receive, и для регистрации в системе обмена сообщениями объект должен был реализовать этот интерфейс. Ограничение такого рода раздражало, поскольку сужало круг тех, кто реально мог бы использовать систему обмена сообщениями. В частности, если вы применяли какую-то стороннюю сборку, то не могли зарегистрировать тот или иной экземпляр из этой библиотеки в системе обмена сообщениями, поскольку у вас не было доступа к коду и вы не могли модифицировать сторонний класс для реализации IReceiver.

MVVM Light Messenger создавали с намерением упростить этот сценарий: любой объект может быть получателем, любой объект может быть отправителем, любой объект может быть сообщением.

Одна вещь, которую не делает Messenger, — отслеживание того, из какого потока отправлено сообщение.

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

Рассмотрим, к примеру, код на рис. 1. Как видите, MVVM Light Messenger используется в двух разных объектах. Объект Registration посылает сообщение всем экземплярам RegisteredUser. Сценарий этого рода можно реализовать множеством способов, и Messenger не всегда является лучшим решением. Но в зависимости от вашей архитектуры он может оказаться очень хорошим средством в реализации этой функциональности, особенно если отправитель и получатель являются частями приложения, которое должно оставаться свободно связанным. Заметьте, что экземпляр Registration ничего не отправляет экземплярам RegisteredUser явным образом. Вместо этого он широковещательно посылает сообщение через Messenger. Любой экземпляр может зарегистрироваться на прием этого типа сообщения и получать уведомление о его отправке. В данном примере отправленное сообщение является экземпляром RegistrationInfo. Однако отправить можно любой тип сообщения — от простых значений (int, bool и т. д.) до специализированных объектов-сообщений. Позже мы рассмотрим использование сообщений и проанализируем некоторые из встроенных типов сообщений в MVVM Light.

Рис. 1. Отправка и получение сообщения

public class Registration
{
  public void SendUpdate()
  {
    var info = new RegistrationInfo
    {
      // Какие-то свойства...
    };
    Messenger.Default.Send(info);
  }
}
public class RegisteredUser
{
  public RegisteredUser()
  {
    Messenger.Default.Register<RegistrationInfo>(
      this,
      HandleRegistrationInfo);
  }
  private void HandleRegistrationInfo(RegistrationInfo info)
  {
    // Обновление информации о зарегистрированном пользователе
  }
}
public class RegistrationInfo
{
  // Какие-то свойства...
}

Код на рис. 1 показывает, что регистрация на тип сообщения (RegistrationInfo) осуществляется через делегат (HandleRegistrationInfo). Это популярный механизм в Microsoft .NET Framework. Например, обработчик событий в C# тоже регистрируется передачей событию делегата, которым может быть либо именованный метод, либо анонимное лямбда-выражение. Аналогично вы можете использовать именованные методы или анонимные лямбды для регистрации получателя в Messenger, как демонстрирует рис. 2.

Рис. 2. Регистрация с помощью именованных методов или лямбд

public UserControl()
{
  InitializeComponent();
  // Регистрация с помощью именованного метода ----
  Loaded += Figure2ControlLoaded;
  Messenger.Default.Register<AnyMessage>(
    this,
    HandleAnyMessage);
 // Регистрация с помощью анонимных лямбд ----
  Loaded += (s, e) =>
  {
    // Здесь что-то делаем
  };
  Messenger.Default.Register<AnyMessage>(
    this,
    message =>
    {
      // Здесь что-то делаем
    });
}
private void HandleAnyMessage(AnyMessage message)
{
  // Здесь что-то делаем
}
private void Figure2ControlLoaded (object sender, RoutedEventArgs e)
{
  // Здесь что-то делаем
}

Обращение между потоками

Одна вещь, которую не делает Messenger, — отслеживание того, из какого потока отправлено сообщение. Если вы читали мою прошлую статью, «Multithreading and Dispatching in MVVM Applications» (bit.ly/1mgZ0Cb), то знаете, что нужно предпринимать некоторые меры предосторожности, когда объект, выполняемый в одном из потоков, пытается обратиться к объекту, принадлежащему другому потоку. Эта проблема часто возникает между фоновым потоком и элементом управления, принадлежащим UI-потоку. В предыдущей статье вы видели, как с помощью MVVM Light DispatcherHelper можно «перенаправить» операцию в UI-поток и тем самым избежать исключение из-за попытки доступа между потоками.

Некоторые агрегаторы событий позволяют автоматически перенаправлять сообщения, посылаемые UI-потоку. Однако MVVM Light Messenger никогда не делает этого из-за стремления его разработчиков упростить Messenger API. Добавление возможности автоматического перенаправления сообщений UI-потоку потребовало бы добавить больше параметров в методы регистрации. Более того, это сделало бы перенаправление менее явным и, возможно, более трудным в понимании не очень опытными разработчиками того, что именно происходит «за кулисами».

Вместо этого вы должны явным образом перенаправлять сообщения UI-потоку, если в этом есть потребность. Лучший способ сделать это — задействовать MVVM Light DispatcherHelper. Как было показано в предыдущей статье, метод CheckBeginInvokeOnUI будет перенаправлять операцию только при необходимости. Если Messenger уже выполняется в UI-потоке, сообщение можно немедленно распространять без перенаправления:

public void RunOnBackgroundThread()
{
 // Выполняем какую-то фоновую операцию
  DispatcherHelper.CheckBeginInvokeOnUI(
    () =>
    {
      Messenger.Default.Send(new ConfirmationMessage());
    });
}

Обработка памяти

В любой системе, которая разрешает взаимодействие объектов, ничего не знающих друг о друге, возникает трудная задача сохранения ссылки на принимающих сообщение. Возьмем, к примеру, систему обработки событий в .NET. Она может создавать жесткие ссылки (strong references) между объектом, генерирующим событие, и объектом — подписчиком на это событие. Код на рис. 3 создает жесткую связь между _first и _second. То есть, если вызывается метод CleanUp и _second равен null, сборщик мусора не может удалить его из памяти, потому на _first все еще существует ссылка. Сборщик мусора полагается на учет ссылок на объект, чтобы определить, можно ли удалить его из памяти; экземпляр Second нельзя удалить, и так создается утечка памяти. Со временем это может вызвать массу проблем: приложение может существенно замедлить скорость своей работы, а в конечном счете просто рухнуть.

Рис. 3. Жесткие ссылки между экземплярами

public class Setup
{
  private First _first = new First();
  private Second _second = new Second();
  public void InitializeObjects()
  {
    _first.AddRelationTo(_second);
  }
  public void Cleanup()
  {
    _second = null;
    // Даже если экземпляр Second равен null, он все равно
    // остается в памяти, так как счетчик ссылок не обнулен
    // (ссылка на _first все еще существует)
  }
}
public class First
{
  private object _another;
  public void AddRelationTo(object another)
  {
    _another = another;
  }
}
public class Second
{
}

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

Чтобы смягчить остроту этой проблемы, в .NET появился объект WeakReference. Этот класс позволяет хранить ссылку на объекту в нежесткой форме (weak). Если все остальные ссылки на этот объект установлены в null, сборщик мусора сможет убрать этот объект, хотя его использует WeakReference. Это очень удобно и при мудром использовании может практически исключить утечки памяти, однако он не всегда решает все проблемы. Чтобы проиллюстрировать это, на рис. 4 показана простая коммуникационная система, где объект SimpleMessenger хранит ссылку на Receiver в WeakReference. Обратите внимание на проверку свойства IsAlive перед обработкой сообщения. Если Receiver удален в ходе предыдущего сбора мусора, свойство IsAlive будет содержать false. Это признак того, что WeakReference больше не действителен и должен быть удален.

Рис. 4. Использование экземпляров WeakReference

public class SuperSimpleMessenger
{
  private readonly List<WeakReference> _receivers
    = new List<WeakReference>();
  public void Register(IReceiver receiver)
  {
    _receivers.Add(new WeakReference(receiver));
  }
  public void Send(object message)
  {
   // Блокировка получателей, чтобы избежать проблем
    // с синхронизацией в многопоточной среде
    lock (_receivers)
    {
      var toRemove = new List<WeakReference>();
      foreach (var reference in _receivers.ToList())
      {
        if (reference.IsAlive)
        {
          ((IReceiver)reference.Target).Receive(message);
        }
        else
        {
          toRemove.Add(reference);
        }
      }
      // Удаление мертвых ссылок.
      // Делайте это в другом цикле, чтобы избежать исключения
      // при модификации текущего перебираемого набора.
      foreach (var dead in toRemove)
      {
        _receivers.Remove(dead);
      }
    }
  }
}

MVVM Light Messenger построен приблизительно на том же принципе, хотя, конечно, он гораздо сложнее! В частности, поскольку Messenger не требует, чтобы Receiver реализовал какой-то определенный интерфейс, ему нужно хранить ссылку на метод (обратный вызов), который будет использоваться при передаче сообщения. В Windows Presentation Foundation (WPF) и Windows Runtime это не проблема. Однако в Silverlight и Windows Phone инфраструктура более защищенная, и ее API запрещают некоторые операции. Одно из таких ограничений в ряде случаев затрагивает и систему Messenger.

Чтобы разобраться в этом, вы должны знать, какого рода методы можно регистрировать для обработки сообщений. Если в двух словах, то метод-получатель может быть статическим (это вообще никогда не создает проблем) или методом экземпляра, в каковом случае возможны вариации: public (открытый), internal (внутренний) и private (закрытый). Во многих случаях метод-получатель является анонимным лямбда-выражением, что эквивалентно закрытому методу.

Когда метод статический или открытый, риска появления утечки памяти нет. Когда метод-обработчик является внутренним или закрытым (или анонимной лямбдой), такой риск есть в Silverlight и Windows Phone. К сожалению, в этих случаях Messenger никак не может использовать WeakReference. И вновь это не проблема в WPF или Windows Runtime. Эта информация сведена в табл. 1.

Табл. 1. Риск утечки памяти без отмены регистрации

ВидимостьWPFSilverlightWindows Phone 8Windows Runtime
StaticНет рискаНет рискаНет рискаНет риска
PublicНет рискаНет рискаНет рискаНет риска
InternalНет рискаРискРискНет риска
PrivateНет рискаРискРискНет риска
Анонимная лямбдаНет рискаРискРискНет риска

Хотя MVVM Light Messenger — очень мощный и универсальный компонент, важно помнить, что в его использование связано с некоторым риском.

Важно отметить: даже если в табл. 1 конкретный случай помечен как рискованный, промах с отменой регистрации не всегда создает утечку памяти. Но несмотря на это, хороший стиль — явным образом отменять регистрацию получателей сообщений от Messenger, когда они больше не нужны, и тогда никаких утечек памяти не будет. Это делается методом Unregister. Заметьте, что у него есть несколько перегруженных версий. Получателя можно полностью отключить от Messenger или отменить регистрацию только одного конкретного метода, а остальные сохранить активными.

Другие риски при использовании Messenger

Как я упоминал, хотя MVVM Light Messenger — очень мощный и универсальный компонент, важно помнить, что в его использование связано с некоторым риском. Я уже говорил о потенциальных утечках памяти в Silverlight и Windows Phone. Другой риск: использование Messenger отсоединяет объект настолько, что может оказаться затруднительным точно понять, что происходит при отправке и приеме сообщения. Менее опытным разработчикам, никогда не имевшим дело с шиной событий, может быть сложно проследить поток операций. Например, если при отладке вы заходите в вызов какого-то метода, а он вызывает метод Messenger.Send, то поток отладки теряется, если только вы не знаете, что следует искать соответствующий метод Messenger.Receive и ставить точку прерывания там. С другой стороны, операции Messenger синхронны, и, если вы понимаете, как работает Messenger, отладить этот поток можно без особых проблем.

Я стараюсь использовать Messenger как «последнее прибежище», когда применение более традиционных приемов программирования либо невозможно, либо приводит к появлению чрезмерно большого количества зависимостей между частями приложения, которое я хочу поддерживать как можно более разъединенным. Однако иногда предпочтительнее использовать другие средства, такие как IOC-контейнер и сервисы, чтобы добиться тех же целей в более явном стиле. Об IOC и сервисах представлений я рассказывал в первой статье из этой серии (bit.ly/1m9HTBX).

Один или несколько компонентов Messenger

Одно из преимуществ таких систем обмена сообщениями, как MVVM Light Messenger, заключается в том, что их можно использовать даже между сборками, например в плагинах. Это распространенная архитектура больших приложений, особенно в WPF. Но система плагинов может быть полезна и в менее крупных приложениях для более простого добавления новых средств без перекомпиляции, скажем, основной части. Как только DLL загружается в AppDomain приложения, содержащиеся в ней классы могут использовать MVVM Light Messenger для взаимодействия с любым другим компонентом этого приложения. Это очень мощная возможность, особенно когда основному приложению не известно, сколько будет загружено вторичных компонентов, что обычно и происходит в приложении на основе плагинов.

Как правило, приложению нужен лишь один экземпляр Messenger для всех взаимодействий. Статический экземпляр, хранящийся в свойстве Messenger.Default, — это, по-видимому, все, что вам нужно. Однако при необходимости вы можете создавать новые экземпляры Messenger. В таких случаях каждый Messenger действует как отдельный коммуникационный канал. Это может быть полезно, если вы хотите быть уверенными в том, что данный объект никогда не получит сообщение, которое ему не предназначено. Так, в коде на рис. 5 два класса регистрируются на получение одного и того же типа сообщения. Когда сообщение принимается, обоим экземплярам нужно выполнить некие проверки, чтобы определить, что делает данное сообщение.

Рис. 5. Использование Messenger по умолчанию и проверка отправителя

public class FirstViewModel
{
  public FirstViewModel()
  {
    Messenger.Default.Register<NotificationMessage>(
      this,
      message =>
      {
        if (message.Sender is MainViewModel)
        {
          // Это сообщение адресовано мне
        }
      });
  }
}
public class SecondViewModel
{
  public SecondViewModel()
  {
    Messenger.Default.Register<NotificationMessage>(
      this,
      message =>
      {
        if (message.Sender is SettingsViewModel)
        {
          // Это сообщение адресовано мне
        }
      });
  }
}

Одно из преимуществ таких систем обмена сообщениями, как MVVM Light Messenger, заключается в том, что их можно использовать даже между сборками, например в плагинах.

На рис. 6 показана реализация с закрытым экземпляром Messenger. В этом случае SecondViewModel никогда не получит сообщение, потому что он подписан на другой экземпляр Messenger и слушает другой канал.

Рис. 6. Использование закрытого Messenger

public class MainViewModel
{
  private Messenger _privateMessenger;
  public MainViewModel()
  {
    _privateMessenger = new Messenger();
    SimpleIoc.Default.Register(() => _privateMessenger,
      "PrivateMessenger");
  }
  public void Update()
  {
    _privateMessenger.Send(new NotificationMessage("DoSomething"));
  }
}
public class FirstViewModel
{
  public FirstViewModel()
  {
    var messenger
      = SimpleIoc.Default.GetInstance<Messenger>("PrivateMessenger");
    messenger.Register<NotificationMessage>(
      this,
      message =>
      {
        // Это сообщение адресовано мне
      });
  }
}

Другой способ избежать отправки конкретного сообщения конкретному получателю — использовать маркеры, как на рис. 7. Это своего рода контракт между отправителем и получателем. Обычно маркер является уникальным идентификатором, таким как GUID, но он может быть любым объектом. Если и отправитель, и получатель используют одинаковый маркер, между двумя объектами открывается закрытый коммуникационный канал. В этом сценарии SecondViewModel, который не использовал маркер, никогда не будет оповещен об отправке сообщения. Основное преимущество в том, что в получателе не требуется писать логику для проверки того, что сообщение действительно было адресовано ему. Вместо этого Messenger отфильтровывает сообщения на основе маркера.

Рис. 7. Разные коммуникационные каналы с применением маркеров

public class MainViewModel
{
  public static readonly Guid Token = Guid.NewGuid();
  public void Update()
  {
    Messenger.Default.Send(new NotificationMessage("DoSomething"),
      Token);
  }
}
public class FirstViewModel
{
  public FirstViewModel()
  {
    Messenger.Default.Register<NotificationMessage>(
      this,
      MainViewModel.Token,
      message =>
      {
        // Это сообщение адресовано мне
      });
  }
}

Использование сообщений

Маркеры — хороший способ фильтрации сообщений, но это не меняет того факта, что сообщение должно нести некий контекст, чтобы быть понятым. Например, вы можете использовать методы Send и Receive с булевым контентом, как на рис. 8. Но если несколько отправителей посылают булевы сообщения, как получатель поймет, кому адресовано сообщение и что с ним делать? Вот почему лучше использовать специализированный тип сообщения, чтобы сделать контекст яснее.

Рис. 8. Определение контекста с помощью типа сообщения

public class Sender
{
  public void SendBoolean()
  {
    Messenger.Default.Send(true);
  }
  public void SendNotification()
  {
    Messenger.Default.Send(
      new NotificationMessage<bool>(true, Notifications.PlayPause));
  }
}
public class Receiver
{
  public Receiver()
  {
    Messenger.Default.Register<bool>(
      this,
      b =>
      {
        // Не совсем понятно, что делать
        // с этим булевым сообщением
      });
    Messenger.Default.Register<NotificationMessage<bool>>(
      this,
      message =>
      {
        if (message.Notification == Notifications.PlayPause)
        {
          // Что-то делаем с message.Content
          Debug.WriteLine(message.Notification + ":" +
            message.Content);
        }
      });
  }
}

На рис. 8 также показано использование специфического типа сообщения. NotificationMessage<T> — один из самых распространенных типов сообщений, встроенных в MVVM Light Toolkit, и он позволяет передавать любой контент (в данном случае — булев) вместе со строкой уведомления. Обычно уведомление — это уникальная строка, определенная в статическом классе Notifications. Это дает возможность отправлять вместе с сообщением какие-то инструкции.

Кроме того, конечно, можно наследовать и от NotificationMessage<T>, использовать другой встроенный тип сообщения или реализовать собственные типы сообщений. MVVM Light Toolkit содержит класс MessageBase, от которого можно наследовать для этих целей, хотя вы вовсе не обязаны использовать его в своем коде.

Как правило, приложению нужен лишь один экземпляр Messenger для всех взаимодействий.

Другой встроенный тип сообщения — PropertyChangedMessage<T>. Он особенно полезен применительно к ObservableObject и классу ViewModelBase, который обычно используется как базовый класс для объектов, участвующих в операциях привязки. Эти классы являются реализациями интерфейса INotifyPropertyChanged, который критически важен в MVVM-приложениях со связыванием с данными. Например, в коде на рис. 9 BankAccountViewModel определяет наблюдаемое свойство Balance. Когда это свойство изменяется, метод RaisePropertyChanged принимает булев параметр, который заставляет класс ViewModelBase «широковещательно» посылать PropertyChangedMessage с информацией об этом свойстве, например его имя, старое и новое значения. Другой объект может подписаться на этот тип сообщения и соответственно реагировать на него.

Рис. 9. Отправка PropertyChangedMessage

public class BankViewModel : ViewModelBase
{
  public const string BalancePropertyName = "Balance";
  private double _balance;
  public double Balance
  {
    get
    {
      return _balance;
    }
    set
    {
      if (Math.Abs(_balance - value) < 0.001)
      {
        return;
      }
      var oldValue = _balance;
      _balance = value;
      RaisePropertyChanged(BalancePropertyName, oldValue, value, true);
    }
  }
}
public class Receiver
{
  public Receiver()
  {
    Messenger.Default.Register<PropertyChangedMessage<double>>(
      this,
      message =>
      {
        if (message.PropertyName == BankViewModel.BalancePropertyName)
        {
          Debug.WriteLine(
            message.OldValue + " --> " + message.NewValue);
        }
      });
  }
}

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

Заключение

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

Этой статьей я завершаю представление компонентов MVVM Light Toolkit. Сейчас очень интересное время для .NET-разработчиков, которые получили возможность использовать одни и те же инструменты и методики на нескольких платформах, основанных на XAML. С помощью MVVM Light вы можете использовать один и тот же код для WPF, Windows Runtime, Windows Phone, Silverlight — и даже на платформах Xamarin для Android и iOS. Надеюсь, вы сочли эту серию статей полезной для понимания того, как MVVM Light может помочь вам в эффективной разработке приложений, в то же время упростив их проектирование, тестирование и сопровождение.

Автор: Лёро Буньон  •  Иcточник: msdn.microsoft.com  •  Опубликована: 23.12.2014
Нашли ошибку в тексте? Сообщите о ней автору: выделите мышкой и нажмите CTRL + ENTER
Теги:   MVVM Light Toolkit.


Оценить статью:
Вверх
Комментарии посетителей
Комментарии отключены. С вопросами по статьям обращайтесь в форум.