Sunday, May 18, 2014

Введение в Prism 5. Использование EventAggregator. Часть вторая

Эта статья является продолжением статьи по использованию EventAggregator в своих проектах. В первой статье мы остановились на том, что наша структура проекта должна стать похожей на приведенную на рисунке ниже.
Теперь перейдем к основной части нашего приложения: к написанию модели представления для наших моделей. Первую модель мы сделаем для работы с клиентами и книгами, назовем ее ShopViewModel.
public class ShopViewModel : BindableBase
{
    private IEventAggregator _eventAggregator;

    public ShopViewModel(IEventAggregator eventAggregator)
       {
        //Generate books
        Books = new ObservableCollection<Book>(ShopHelper.GenerateBooks());
        //Generate customers
        Customers = new ObservableCollection<Customer>(ShopHelper.GenerateCustomers());

        BuyBookCommand = new DelegateCommand(BuyBook, CanBuyBook);

        _eventAggregator = eventAggregator;
       }

       public ObservableCollection<Customer> Customers { get; set; }
       public ObservableCollection<Book> Books { get; set; }

    private Book _selectedBook;
    public Book SelectedBook
    {
        get { return _selectedBook; }
        set
        {
            _selectedBook = value;
            OnPropertyChanged(() => SelectedBook);
            BuyBookCommand.RaiseCanExecuteChanged();
        }
    }

    private Customer _selectedCustomer;
    public Customer SelectedCustomer
    {
        get { return _selectedCustomer; }
        set
        {
            _selectedCustomer = value;
            OnPropertyChanged(() => SelectedCustomer);
            BuyBookCommand.RaiseCanExecuteChanged();
        }
    }

       #region Command
       public DelegateCommand BuyBookCommand {get; set; }
            
       #endregion

    #region Private Methods
    private bool CanBuyBook()
    {
        return SelectedBook != null && SelectedCustomer != null;
    }

    private void BuyBook()
    {
        var shopOrder = new ShopOrder();
        shopOrder.Book = SelectedBook;
        shopOrder.Customer = SelectedCustomer;
        _eventAggregator.GetEvent<ShopOrderEvent>().Publish(shopOrder);
    }
    #endregion
}
В этой модели мы используем EventAggregator для того чтобы отправить объект класса ShopOrder всем подписчикам, которые подписаны на событие ShopOrderEvent. О том, как использовать паттерн команда (интерфейс ICommand (DelegateCoomand в Prism), мы рассматривали в статье "Введениев Prism 5". Теперь необходимо добавить регион, в котором мы отобразим наши данные. Для этого перейдем в папку Views/Regions и добавим новый контрол ShopOrderView.xaml. Ниже вы можете увидеть мой вариант реализации данного контрола.
<UserControl x:Class="EventAggregatorSample.Views.Regions.ShopOrderView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d"
             >
    <Grid>
        <StackPanel>
            <Label>Customer:</Label>
            <ComboBox Name="CustomerCbx" Margin="5"  Width="Auto" HorizontalAlignment="Stretch"
                      ItemsSource="{Binding Customers}" SelectedItem="{Binding SelectedCustomer}">
                <ComboBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="{Binding FirstName}" Width="50"/>
                            <TextBlock Text="{Binding LastName}" />
                        </StackPanel>
                    </DataTemplate>
                </ComboBox.ItemTemplate>
            </ComboBox>
            <Label>Book:</Label>
            <ComboBox Name="BooksCbx" Margin="5"  Width="Auto" HorizontalAlignment="Stretch"
                      ItemsSource="{Binding Books}" SelectedItem="{Binding SelectedBook}">
                <ComboBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel>
                            <TextBlock Text="{Binding Author}" />
                            <TextBlock Text="{Binding Title}" />
                            <TextBlock Text="{Binding Price, StringFormat={}{0:C}}" FontWeight="Bold" />
                        </StackPanel>
                    </DataTemplate>
                </ComboBox.ItemTemplate>
            </ComboBox>
            <Button Name="AddButton" Margin="5" Width="75" Height="25" HorizontalAlignment="Left"
                    Command="{Binding BuyBookCommand}">Add</Button>
        </StackPanel>
    </Grid>
</UserControl>
В контроле, по сути, два элемента ComboBox, вокруг которых построена логика, и одна кнопка, которая добавляет новый заказ. Теперь для того чтобы наш контрол, модели и модели представления соединить в одно целое, добавим новый модуль в папку Modules и назовем его ShopModule. Как использовать модули, вы можете посмотреть в статье "Введениев Prism 5. Работа с регионами".
public class ShopModule : IModule
{
    public ShopModule(IUnityContainer container, IRegionManager regionManager)
    {
        Container = container;
        RegionManager = regionManager;
    }
    public void Initialize()
    {
        var viewModel = Container.Resolve<ShopViewModel>();
        var view = Container.Resolve<ShopOrderView>();
        view.DataContext = viewModel;
        RegionManager.Regions["ShopRegion"].Add(view);
    }

    public IUnityContainer Container { get; private set; }
    public IRegionManager RegionManager { get; private set; }
}
Теперь осталось добавить вызов данного модуля в наш загрузчик (класс Bootstrapper).
protected override void InitializeModules()
{
    IModule shopModule = Container.Resolve<ShopModule>();
    shopModule.Initialize();
}
После этого запустим наш пример, чтобы проверить что левая часть нашего приложения работает.
С издателем мы, по сути, разобрались и реализовали его (роль издателя на себя взяла модель представления ShopViewModel). Теперь приступим к реализации второй части, без которой использование EventAggregator будет неполным, а именно: начнем реализовывать наш подписчик. Модель представления, которая отвечает за работу с заказами, мы назовем OrderViewModel и поместим в папку ViewModels. Один из способов реализации данной модели мы можем увидеть ниже.
public class OrderViewModel
{
    private IEventAggregator _eventAggregator;
    private SubscriptionToken subscriptionToken;
    public OrderViewModel(IEventAggregator eventAggregator)
    {
        _eventAggregator = eventAggregator;
        Orders = new ObservableCollection<Order>();
        ShopOrderEvent fundAddedEvent = eventAggregator.GetEvent<ShopOrderEvent>();
        subscriptionToken = fundAddedEvent.Subscribe(AddNewOrderEventHandler, ThreadOption.UIThread, false, (shopOrder) => true);
    }

    public void AddNewOrderEventHandler(ShopOrder shopOrder)
    {
        var order = Orders.FirstOrDefault(x => x.Customer.Id == shopOrder.Customer.Id);
        if(order != null)
        {
            order.AddBook(shopOrder.Book);
        }
        else
        {
            var newOrder = new Order
                {
                    Customer = shopOrder.Customer
                };
            newOrder.AddBook(shopOrder.Book);

            Orders.Add(newOrder);
        }
    }

    public ObservableCollection<Order> Orders { get; set; }
}
Эта модель представления выступает у нас подписчиком. Для того чтобы подписаться на какое-то событие с помощью EventAggregator, нам нужно обратиться за помощью к классу SubscriptionToken. Пройдемся внимательно по подписке с помощью класса EventAggreagtor.
ShopOrderEvent fundAddedEvent = eventAggregator.GetEvent<ShopOrderEvent>();
subscriptionToken = fundAddedEvent.Subscribe(AddNewOrderEventHandler, ThreadOption.UIThread, false, (shopOrder) => true);
В данном примере всего две строчки кода, поэтому разберем их детально. Первая строка кода позволяет получить объект события. EventAggregator создаёт объект события при первом запросе, если он ещё не был создан. Он освобождает издателя или подписчика от необходимости определять, доступно ли событие. А основная работа ложится на класс PubSubEvent. В нашем случае это класс ShopOrderEvent. Существует несколько вариантов подписки на данное событие. В первой части статьи я уже приводил эти перегруженные методы, для того чтобы освежить их в памяти, посмотрим еще раз:
public SubscriptionToken Subscribe(Action<TPayload> action);
        public SubscriptionToken Subscribe(Action<TPayload> action, bool keepSubscriberReferenceAlive);
        public SubscriptionToken Subscribe(Action<TPayload> action, ThreadOption threadOption);
        public SubscriptionToken Subscribe(Action<TPayload> action, ThreadOption threadOption, bool keepSubscriberReferenceAlive);
        public virtual SubscriptionToken Subscribe(Action<TPayload> action, ThreadOption threadOption, bool keepSubscriberReferenceAlive, Predicate<TPayload> filter);
Рассмотрим, как работают эти способы подписки.
  • Если вам нужно обновлять элементы управления в пользовательском интерфейсе при получении события, подписывайтесь на получение события в потоке UI.
  • Если вам нужно фильтровать события, предоставьте при подписке делегат для фильтрации.
  • Если для вас критична производительность, рассмотрите использование сильных ссылок при подписке и последующее ручную отписку.
  • Если ни один из пунктов выше вам не подходит, используйте стандартную подписку.
О режиме, в котором будет работать наш подписчик, указывает второй параметр: перечисление ThreadOption. Это перечисление содержит следующие члены:
  • PublisherThread. События будут получены в потоке издателя. Это является стандартным поведением.
  • BackgroundThread. Событие будет получено асинхронно в потоке из пула потоков .NET Framework.
  • UIThread. Событие будет получено в потоке UI.
Если нам нужно фильтровать данные, которые нам поступают, например, некоторые виды заказов нас не интересуют, нам необходимо установить предикат
Predicate<TPayload> filter
Для нашего примера я сделал это последним параметром через лямбда-выражение.
(shopOrder) => true
Это означает, что меня удовлетворяют все заказы. Напоследок остался последний параметр, который мы не рассмотрели: keepSubscriberReferenceAlive типа bool.
  • При установке в true экземпляр события хранит сильную ссылку на экземпляр подписчика, не позволяя ему тем самым стать достижимым для сборщика мусора. Про отписку от события смотрите раздел далее в статье.
  • При установке в false (что является значением по умолчанию, если не задать этот параметр), экземпляр события создаёт слабую ссылку на подписчика, позволяя ему стать достижимым для сборщика мусора при отсутствии на него других сильных ссылок. При сборке мусора происходит автоматическая отписка от этого события.
После того как мы реализовали нашу модель представления для заказов, нужно реализовать контрол, который будет отображать всю модель  в нашем регионе. Для этого перейдем в папку Regions и добавим контрол OrderView.xaml. Реализация данного контрола может быть подобная, как на рисунке ниже.
<UserControl x:Class="EventAggregatorSample.Views.Regions.OrderView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d"
             Height="Auto" Width="Auto">
    <Grid>
        <ListBox ItemsSource="{Binding Orders}" Width="Auto" HorizontalContentAlignment="Stretch">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Border Margin="10,5,10,10" Grid.Row="1" CornerRadius="4,4,4,4" BorderBrush="#193441" Background="#FFC8DDC5" BorderThickness="2,2,2,2">
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto" />
                                <RowDefinition Height="Auto" />
                                <RowDefinition Height="Auto" />
                            </Grid.RowDefinitions>
                            <Border Grid.Row="0">
                                <StackPanel Orientation="Horizontal">
                                    <TextBlock Text="{Binding Customer.FirstName}" Width="50"/>
                                    <TextBlock Text="{Binding Customer.LastName}" />
                                </StackPanel>
                            </Border>
                            <ListBox Grid.Row="1" ItemsSource="{Binding Books}">
                                <ListBox.ItemTemplate>
                                    <DataTemplate>
                                        <StackPanel Orientation="Horizontal">
                                            <TextBlock Text="{Binding Author}" Width="100"/>
                                            <TextBlock Text="{Binding Price, StringFormat={}{0:C}}" FontWeight="Bold" />
                                        </StackPanel>
                                    </DataTemplate>
                                </ListBox.ItemTemplate>
                            </ListBox>
                            <Border Grid.Row="2">
                                <StackPanel Orientation="Horizontal">
                                    <TextBlock Text="Sum:" Width="50"/>
                                    <TextBlock Text="{Binding Sum}" />
                                </StackPanel>
                            </Border>
                        </Grid>
                    </Border>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</UserControl>
Прошу начинающих разработчиков при виде данного кода не теряться. Я экспериментировал, как лучше отобразить заказы на UI интерфейсе, и  пришел к такому интерфейсу. Осталось связать информацию, которая будет отображаться с правой стороны нашей оболочки, с помощью модуля OrderModule. Этот модуль необходимо добавить в нашу папку Modules.
public class OrderModule : IModule
{
    public OrderModule(IUnityContainer container, IRegionManager regionManager)
    {
        Container = container;
        RegionManager = regionManager;
    }
    public void Initialize()
    {
        var viewModel = Container.Resolve<OrderViewModel>();
        var view = Container.Resolve<OrderView>();
        view.DataContext = viewModel;
        RegionManager.Regions["OrderRegion"].Add(view);
    }

    public IUnityContainer Container { get; private set; }
    public IRegionManager RegionManager { get; private set; }
}
И последний штрих нашего приложения: добавить наш модуль в наш загрузчик.
protected override void InitializeModules()
{
    IModule shopModule = Container.Resolve<ShopModule>();
    IModule orderModule = Container.Resolve<OrderModule>();

    shopModule.Initialize();
    orderModule.Initialize();
}
Полный исходный код загрузчика будет выглядеть следующим образом:
public class Bootstrapper : UnityBootstrapper
{
       protected override DependencyObject CreateShell()
       {
             return Container.Resolve<Shell>();
       }

       protected override void InitializeShell()
       {
             Application.Current.MainWindow = (Window)Shell;
             Application.Current.MainWindow.Show();
       }

    protected override void InitializeModules()
    {
        IModule shopModule = Container.Resolve<ShopModule>();
        IModule orderModule = Container.Resolve<OrderModule>();

        shopModule.Initialize();
        orderModule.Initialize();
    }
}
Теперь еще раз посмотрим на нашу структуру проекта. Она должна быть подобной той архитектуре, которая приведена на рисунке ниже.
Если вы все сделали правильно, то осталось запустить наш пример и посмотреть на результат.
Вот такой вид оболочки вышел у меня. Если развернуть созданный пример на весь экран, то внешний вид будет более привлекательным. А так имеем приложение, которое я старался максимально подогнать к примерам, которые используются для серии Line Of Business (LOB) приложений. Я решил пример с использованием CompositeCommand оставить после примера с EventAggregator, так как данный пример можно немножко модифицировать и добавить возможность отмены заказать или сформировать заказ с возможностью его контроля.

No comments:

Post a Comment