Здравствуйте, уважаемые читатели. Сегодня мы
рассмотрим, как использовать 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<IEventAggregator, EventAggregator>();
_container.Singleton<IWindowManager, WindowManager>();
_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 { get; set; }
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