Sunday, November 15, 2015

Event Aggregator in Caliburn.Micro

Здравствуйте, уважаемые читатели. Сегодня мы рассмотрим, как использовать Event Aggregator в Caliburn.Micro. Есть уже множество статей на эту тему. НапримерThe Event Aggregator или Caliburn Micro Part 4: The Event AggregatorВ этой статье мы разберем пример с использованием Event Aggregator, а также сравним его с реализованным в Prism 6. 
Для начала несколько слов о том, что собой являет Event Aggregator. Это сервис, который позволяет коммуницироваться между моделями представлений с помощью сообщений. Он работает на службе агрегации событий и шаблоне Наблюдатель (англ. Observer), который еще называют Publisher-Subscriber, и который позволяет издателям (publishers) и подписчикам (subscribers) взаимодействовать друг с другом, не имея явных ссылок друг на друга. Если глубже копнуть, то, в общей сложности, это паттерн Посредник (англ. Mediator), который работает совместно с наблюдателем. Основная причина, почему это паттерн наблюдатель, заключается в том, что он позволяет обеспечивать взаимодействие множества объектов, сохраняя при этом слабую связность. Я считаю, что не особо имеет значение то, как вы будете считать данный паттерн: как наблюдатель или как посредник; главное – чтобы вы знали, что название Event Aggregator – это просто альтернативное его название которое используется для паттернов уровня Enterprise. Например, вы можете почитать детально описание данного паттерна у Мартина Фаулера. 
А сейчас самое время перейти к практике. Для начала создадим новый WPF проект, который назовем “EventAggregatorSample”, чтобы особо не заморачиваться с названием.
Затем сразу с помощью NuGet Package Manager установим себе Caliburn.Micro.
Теперь приступим к реализации. Создадим простое окно со списком, в котором мы сможем удалять элементы с этого списка. Я также впервые отклонюсь от своего принципа с разделением проекта по папкам: отдельно – модель представления, отдельно – модель и отдельно – само представление (соответственно с тем, что у нас одна модель, одна модель представления и одно представление). Ничего мудреного. Давайте сходу переименуем название нашего главного окна с MainWindow в MainView, как показано на рисунке ниже. И сразу же создадим класс-пустышку MainViewModel, для того чтобы у нас заработало автоматическое связывание модели представления с представлением, которое работает в Prism.
Затем добавим модель, которая будет хранить информацию о списке. Ниже приведен пример данной модели.
public class TestModel : PropertyChangedBase
{
    private string _id;

    public string Id
    {
        get { return _id; }
        set
        {
            _id = value;
            NotifyOfPropertyChange();
        }
    }
}
Фактически для примера мы в ней просто будем хранить Guid-ы.
Теперь давайте приступим к реализации нашего загрузчика, который назовем ShellBootstrapper. Реализация его приведена ниже.
public class ShellBootstrapper : BootstrapperBase
{
    public ShellBootstrapper()
    {
        Initialize();
    }

    protected override void OnStartup(object sender, StartupEventArgs e)
    {
        DisplayRootViewFor<MainViewModel>();
    }

    private readonly SimpleContainer _container =
        new SimpleContainer();

    protected override object GetInstance(Type service, string key)
    {
        var instance = _container.GetInstance(service, key);
        if (instance != null)
            return instance;
        throw new InvalidOperationException("Could not locate any instances.");
    }

    protected override IEnumerable<object> GetAllInstances(Type service)
    {
        return _container.GetAllInstances(service);
    }

    protected override void BuildUp(object instance)
    {
        _container.BuildUp(instance);
    }

    protected override void Configure()
    {
        _container.Singleton<IEventAggregatorEventAggregator>();
        _container.Singleton<IWindowManagerWindowManager>();
        _container.PerRequest<MainViewModel>();
    }
}
Здесь нет ничего неординарного. Мы просто переопределили необходимые для нас методы и добавили регистрацию тех классов и интерфейсов, которые мы будем использовать.
Затем нам необходимо убрать с App.xaml запуск нашего приложения через StartupUri и добавить инициализацию плюс запуск нашего загрузчика, который и выполнит все требуемые действия.
<Application x:Class="EventAggregatorSample.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:eventAggregatorSample="clr-namespace:EventAggregatorSample">
  <Application.Resources>
    <ResourceDictionary>
      <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary>
          <eventAggregatorSample:ShellBootstrapper x:Key="Bootstrapper" />
        </ResourceDictionary>
      </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
  </Application.Resources>
</Application>
После проделанной работы вы можете запустить пример и посмотреть, что все работает. Но какой прок нам от пустого окна? Поэтому самое время приступить к реализации. Возьмемся за нашего класс-пустышку MainViewModel, который мы создали ранее, и заполним его необходимыми данными.
public class MainViewModel : IHandle<TestModel>
{
    private readonly IEventAggregator _eventAggregator;

    public MainViewModel(IEventAggregator eventAggregator)
    {
        _eventAggregator = eventAggregator;
        _eventAggregator.Subscribe(this);

        Items = new BindableCollection<TestModel>();
        for (int i = 0; i < 1000; i++)
        {
            Items.Add(new TestModel
            {
                Id = Guid.NewGuid().ToString()
            });
        }
    }

    public BindableCollection<TestModel> Items { getset; }

    public bool CanRemove(TestModel child)
    {
        return child != null;
    }

    public void Remove(TestModel child)
    {
        _eventAggregator.PublishOnUIThread(child);
    }

    public void Handle(TestModel message)
    {
        //Remove item with EventAggregator
        Items.Remove(message);
    }
}
Теперь я немного хотел бы объяснить, что же все-таки здесь происходит. Наша модель представления выступает одновременно как подписчиком, так и издателем. Для того чтобы получать уведомления об изменениях, необходимо имплементировать интерфейс IHandle<>, у которого есть только один метод Handle. В этом методе мы реализовали удаление нужного элемента со списка. Подписку на данное событие мы реализовали в конструкторе с помощью вызова метода Subscribe. Для того чтобы уведомить о том, что произошло какое-то событие, в интерфейсе IEventAggregator есть событие Publish.
void Publish(object message, Action<System.Action> marshal)
И несколько extension-методов, один из которых мы применили в нашем примере.
Всю работу с Event Aggregator можно свести к следующему виду:
  • создать или получить экземпляр Event Aggregator;
  • подписаться на событие;
  • отправить уведомление;
  • отписаться от события.
При этом часто эта логика разделена. Получатели только подписываются на изменения и обрабатывают их получения. А отправители в основном только отправляют сообщения нужного типа.
Теперь перейдем в наше окно MainView.xaml и перепишем его следующим образом:
<Window x:Class="EventAggregatorSample.MainView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:cal="http://www.caliburnproject.org"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition Height="*" />
      </Grid.RowDefinitions>
      <ListBox Grid.Row="0" x:Name="Items">
        <ListBox.ItemTemplate>
          <DataTemplate>
            <StackPanel Orientation="Horizontal">
              <Button Content="Remove"
                      cal:Message.Attach="Remove($dataContext)" />
              <TextBlock Text="{Binding Id}" />
            </StackPanel>
          </DataTemplate>
        </ListBox.ItemTemplate>
      </ListBox>
    </Grid>
</Window>
Здесь нет ничего запутанного: всего один список с кнопкой и текстом, поэтому мы заострять особо внимание на этом не будем и перейдем к запуску приложения, чтобы убедиться в том, что у нас все работает.
Это пример того, как выглядит мое окно после запуска, и после того как я нажимаю на кнопку Remove, элемент из списка благополучно удаляется.
Теперь поскольку мы вроде как закончили основную часть, приступим к принципу работы Event Aggregator в нестандартных случаях, когда у нас есть подписка на разные типы сообщений. Для этого добавим новый IHandle интерфейс с типом string.
public class MainViewModel : IHandle<TestModel>, IHandle<string>
После этого реализуем новый метод Handle следующим образом, а также сразу – метод Remove.
public void Remove(TestModel child)
{
    _eventAggregator.PublishOnUIThread(child.Id);
}

public void Handle(TestModel message)
{
    //Remove item with EventAggregator
    Items.Remove(message);
}

public void Handle(string message)
{
    //Call second methods
    var item = Items.FirstOrDefault(x => x.Id == message);
    if (item != null)
        Items.Remove(item);
}
Есть еще одна интересная возможность: ваш подписчик может проверять, есть ли подписанные обработчики конкретного типа, чтобы отправить, например, другое сообщение.
public void Handle(TestModel message)
{
    if (_eventAggregator.HandlerExistsFor(typeof(SpecialMessageEvent)))
    {
        _eventAggregator.PublishOnUIThread(new SpecialMessageEvent(message.Id));
    }
    //Remove item with EventAggregator
    Items.Remove(message);
}
Ни в коем случае не пытайтесь отправлять сообщение самим себе. Потому что, скажем, следующий код вызовет StackOverflowException.
public void Remove(TestModel child)
{
    _eventAggregator.PublishOnUIThread(child);
}

public void Handle(TestModel message)
{
    if (_eventAggregator.HandlerExistsFor(typeof(TestModel)))
    {
        _eventAggregator.PublishOnUIThread(new TestModel() { Id = message.Id});
    }
    //Remove item with EventAggregator
    Items.Remove(message);
}
Можно, конечно, добавлять разные проверки в списке и т.д. Но лучше этого избегать. В редких случаях это может быть оправдано, например, если у вас крупный проект и есть несколько уровней уведомлений, каждому из которых нужно сделать нотификацию. В этом плане фильтры в Prism для Event Aggregator работают лучше и позволяют игнорировать самого себя. Принцип здесь следующий: сначала срабатывает фильтр (можно или нельзя вызывать обновление для подписчика), а затем уже вызывается сам метод обновления.
Мне больше всего нравится подход с использованием тасков. В Caliburn.Micro есть интерфейс IHandleWithTask, который позволяет работать с Event Aggregator на уровне тасков (TPL). Давайте заменим нашу предыдущую логику с IHandle<string> на IHandleWithTask<string>.
После этого перепишем старую логику следующим образом:
public async void Remove(TestModel child)
{
    await _eventAggregator.PublishOnUIThreadAsync(child.Id);
}

public Task Handle(string message)
{
    //Call second methods
    return Task.Factory.StartNew(() =>
    {
        var item = Items.FirstOrDefault(x => x.Id == message);
        if (item != null)
            Items.Remove(item);
    });
}
Лично мне эта логика мне очень нравится, тем более, она очень хорошо смотрится при написании приложений  UWP- и Windows Phone-приложений.
Несколько слов об отписке. В Event Aggregator есть метод Unsubscribe, который позволяет убрать отписку необходимого нам подписчика. Самый простой способ, я считаю, – воспользоваться возможностями самого Caliburn.Micro. Для этого мы можем добавить для нашей модели представления наследование от класса Screen и переопределить метод OnActivate и OnDeactivate следующим образом:
protected override void OnActivate()
{
    _eventAggregator.Subscribe(this);
    base.OnActivate();
}

protected override void OnDeactivate(bool close)
{
    _eventAggregator.Unsubscribe(this);
    base.OnDeactivate(close);
}

Итоги
Сегодня мы рассмотрели, как можно использовать Event Aggregator в своих WPF-приложениях. В данном Event Aggregator намного больше возможностей, чем нам предоставляет аналогичный Event Aggregator с библиотеки Prism. Надеюсь, что данная статья вам будет кстати для изучения Caliburn.Micro и что вы нашли в этом материале что-то свежее для себя. 

No comments:

Post a Comment