Tuesday, May 13, 2014

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

Здравствуйте, уважаемые читатели. В этой статье мы рассмотрим пример использования паттерна EventAggregator с Prism 5, а также принцип работы SubEvent с библиотеки Prism 5. Пройдемся по терминам, чтобы понимать, для чего нужен паттерн EventAggregarot
Начнем с того, что такое агрегация событий (event aggregation). Для взаимодействия между слабо связанными событиями библиотека Prism предлагает механизм, который основан на паттерне Наблюдатель (в мире .NET чаще встречается второе название данного паттерна, Publisher-Subscriber). Этот паттерн позволяет взаимодействовать издателям (publishers) и подписчикам (subscribers), не имея явных ссылок друг на друга. Расскажу немного больше о данном паттерне. Паттерн наблюдатель использует так называемую модель проталкивания (push model), когда издатель посылает подписчикам (или часто можете встретить слово "наблюдатель") детальную информацию об изменении, независимо от того, нужно ли им это. Второй вариант реализации имеет название модель вытягивания (pull model), когда издатель посылает минимальное уведомление, а подписчики запрашивают детали позднее. По принципу pull model работает паттерн итератор (Iterator). Компания Microsoft добавила поддержку данного паттерна в mscorlib IObserver<T>
А теперь плохая новость. Компания Microsoft разделяет паттерны Observer и Publisher-Subscriber. Например, второй паттерн они рассматривают как реализация на событиях event. Более детально с работой этого паттерна вы можете ознакомиться в моей статье Паттерн "Observer" (Наблюдатель)
Возвратимся к нашему EventAggregator. EventAggregator дает функциональность для мультиадресной подписки/публикации и построен вокруг PubSubEvents с пространства имен Microsoft.Practices.Prism.PubSubEvents. Это означает, что может существовать несколько издателей, публикующих событие, и несколько подписчиков, подписанных на это событие. Реализация класса PubSubEvent приведена ниже.
public class PubSubEvent<TPayload> : EventBase
       {
             public PubSubEvent();

             public virtual bool Contains(Action<TPayload> subscriber);
             public virtual void Publish(TPayload payload);
             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);
             public virtual void Unsubscribe(Action<TPayload> subscriber);
       }
Не пугайтесь описания этого класса. Приведенный выше код является просто расширением классического паттерна наблюдатель, который вы можете увидеть в .NET Framework. Сам интерфейс IEventAggregator, используемый классом EventAggregator, тоже очень прост.
public interface IEventAggregator
       {
             TEventType GetEvent<TEventType>() where TEventType : EventBase, new();
       }
События, которые мы создаем с Prism, являются строго типизированными, то есть у вас будет работать проверка типов во время компиляции. Класс EventAggregator позволяет подписчикам или издателям обнаруживать конкретного наследника EventBase для определения типа события. Им могут пользоваться сразу несколько подписчиков и издателей, как показано на иллюстрации ниже (рисунок взят с официальной документации Developer's Guide to Microsoft Prism Library 5.0 for WPF).
Эту схему можно изобразить немного проще:
Так более понятно, в какую сторону и как происходит обмен. Теперь вернемся к рисунку по EventAggregator, взятому с официальной документации по Prism 5. Как видим, основная работа построена вокруг класса CompositePresentationEvent. В Prism 4.1 этот класс являлся единственной реализацией класса EventBase, включенной в данную библиотеку. В версии 5.0 разработчики провели рефакторинг и пометили этот класс как устаревший (атрибут Obsolete), а вместо него нужно использовать класс PubSubEvent, реализация которого приведена в статье выше.  
Примечание для разработчиков, которые работали раньше с Prism 4.1. Отличия CompositePresentationEvent от PubSubEvent вообще нет. Рефакторинг заключался только в том, чтобы перенести код с места на место. И как не печально признавать основные изменения, которые отличают Prism 5 от версии 4.1, это исправление некоторых багов и перенос некоторых классов в другие пространства имен.
Use the PubSubEvents class in the Microsoft.Practices.PubSubEvents portable class library instead of CompositePresentationEvents. The classes from the Events solution folder in the Prism assembly are marked obsolete.
Также класс EventAggregator поменял пространство имен и теперь находится в том же пространстве, что и класс PubSubEvent. Старый класс помечен атрибутом Obsolete и считается устаревшим.
The EventAggregator classes are marked obsolete in the Prism assembly.
Теперь класс EventAggregator обязан быть создан в UI потоке.
EventAggregator now must be constructed on the UI thread to properly acquire a reference to the UI thread’s SynchronizationContext.
После теоретического вступления приступим к созданию WPF-приложения. Назовем его "EventAggregatorSample".
Примечание. В этой статье, в отличие от предыдущих статей по библиотеке Prism 5, я не буду рассматривать, как построить архитектуру проекта, как установить Prism 5 и Prism.UnityExtensions (загрузчик для работы с IoC контейнером Unity), как реализовать загрузчик (Bootstrapper) и т.д. Все эти вопросы были затронуты раннее в статье по загрузчикам UnityBootstrapper и MefBootstrapper, по регионам и модульности есть материал в статье Введение в Prism 5. Работа с регионами. Поэтому пропустим эту часть и покажем, как нужно переписать взаимодействие на моделях, максимально приближенных к реальным условиям. Первым делом приведите свою структуру проекта к такому виду:
Если вы не знаете, как это сделать, вам поможет моя статья Введение в Prism 5. Boostrapper. В ней рассказано пошагово, что нужно делать, чтобы получить нужную форму. Следующим делом в нашу оболочку добавим два региона: ShopRegion, который будет хранить информацию о книгах и покупателях, и регион OrderRegion, который будет хранить информацию о заказах.  Пример разметки главного окна вы можете посмотреть ниже.
<Window x:Class="EventAggregatorSample.Views.Shell"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:cal="clr-namespace:Microsoft.Practices.Prism.Regions;assembly=Microsoft.Practices.Prism.Composition"
        Title="MainWindow" Height="350" Width="525">
    <Window.Background>
        <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
            <GradientStop Color="#FFFFFFFF" Offset="0"/>
            <GradientStop Color="#FCFFF5" Offset="0.992"/>
            <GradientStop Color="#3E606F" Offset="0.185"/>
        </LinearGradientBrush>
    </Window.Background>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="50"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Border Margin="10,5,10,10" Grid.Row="1" CornerRadius="4,4,4,4" BorderBrush="#193441" Background="#FFC8DDC5" BorderThickness="2,2,2,2">
            <Grid Width="Auto" Height="Auto" Margin="10,10,10,10">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width=".4*"/>
                    <ColumnDefinition  Width=".6*"/>
                </Grid.ColumnDefinitions>
                <Border Grid.Column="0" CornerRadius="4,4,4,4" BorderBrush="#193441" Background="#FCFFF5" BorderThickness="2,2,2,2" Margin="5" Padding="5">
                    <StackPanel Orientation="Vertical">
                        <ItemsControl cal:RegionManager.RegionName="ShopRegion"  />
                    </StackPanel>
                </Border>
                <Border Grid.Column="1" CornerRadius="4,4,4,4" BorderBrush="#193441" Background="#FCFFF5" BorderThickness="2,2,2,2" Margin="5" Padding="5">
                    <StackPanel Orientation="Vertical">
                        <ItemsControl cal:RegionManager.RegionName="OrderRegion"  />
                    </StackPanel>
                </Border>
            </Grid>
        </Border>
    </Grid>
</Window>
После запуска вашего примера вы должны получить такой прототип окна:
Примечание. В примерах, которые можно скачать вместе с Prism 5, есть пример EventAggregation_Desktop, в котором, по сути, раскрыта тема EventAggregation. В том примере есть несколько минусов. Один из таких минусов  это использование презентеров, реализация которых в примере сильно затронула UI интерфейс, что является недопустимым для подхода разработки с использованием паттерна MVVM. Но для того чтобы понять, как это все работает, этот пример очень даже сносный и неплохой.
Для нашего приложения нам понадобятся три модели. В модели Book мы будем хранить информацию о книге для продажи. Во второй модели Customer будем хранить информацию о покупателе. И напоследок модель Order, в которой будем хранить информацию о покупке. Добавить все эти модели необходимо в папку Models, предназначенную для хранения информации по моделям. Ниже приведена реализация класса Book.
public class Book : BindableBase
{
       private long _id;
       public long Id
       {
             get { return _id; }
             set
             {
                    _id = value;
                    OnPropertyChanged(() => Id);
             }
       }

       private string _author;
       public string Author
       {
             get { return _author; }
             set
             {
                    _author = value;
                    OnPropertyChanged(() => Author);
             }
       }

       private string _title;
       public string Title
       {
             get { return _title; }
             set
             {
                    _title = value;
                    OnPropertyChanged(() => Title);
             }
       }

       private DateTime _year;
       public DateTime Year
       {
             get { return _year; }
             set
             {
                    _year = value;
                    OnPropertyChanged(() => Year);
             }
       }

       private string _sn;
       public string SN
       {
             get { return _sn; }
             set
             {
                    _sn = value;
                    OnPropertyChanged(() => SN);
             }
       }

       private double _price;
       public double Price
       {
             get { return _price; }
             set
             {
                    _price = value;
                    OnPropertyChanged(() => Price);
             }
       }
}
В этом примере я воспользовался возможностью класса BindableBase указывать, какое свойство нужно обновить с помощью лямбда-выражения. Здесь нет никакой мистики. Если вы следите за обновлениями, которые вошли в фреймворк 4.5, то там можно увидеть один мистический атрибут CallerMemberName. Подробнее об этом вы можете ознакомится в статье  New feature in C# 5.0 – [CallerMemberName]. Следующим делом приведем реализацию класса Customer, в котором будет храниться информация о покупателе.  
public class Customer : BindableBase
{
       private int _id;
       public int Id
       {
             get { return _id; }
             set
             {
                    _id = value;
                    OnPropertyChanged(() => Id);
             }
       }

       private string _firstName;
       public string FirstName
       {
             get { return _firstName; }
             set
             {
                    _firstName = value;
                    OnPropertyChanged(() => FirstName);
             }
       }

       private string _lastName;
       public string LastName
       {
             get { return _lastName; }
             set
             {
                    _lastName = value;
                    OnPropertyChanged(() => LastName);
             }
       }

       private int _age;
       public int Age
       {
             get { return _age; }
             set
             {
                    _age = value;
                    OnPropertyChanged(() => Age);
             }
       }
}
И напоследок  самый большой класс Order, в котором хранится информация о покупателе, купленные книги и сума покупки.
public class Order : BindableBase
{
       private int _id;
       public int Id
       {
             get { return _id; }
             set
             {
                    _id = value;
                    OnPropertyChanged(() => Id);
             }
       }

       private Customer _customer;
       public Customer Customer
       {
             get { return _customer; }
             set
             {
                    _customer = value;
                    OnPropertyChanged(() => Customer);
             }
       }

    public void AddBook(Book newBook)
    {
        if (Books == null)
            Books = new ObservableCollection<Book>();
        Books.Add(newBook);
        Sum = Books.Sum(x => x.Price);
    }

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

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

       private double _sum;
       public double Sum
       {
             get { return _sum; }
             set
             {
                    _sum = value;
                    OnPropertyChanged(() => Sum);
             }
       }
}
Пока проблем с моделями у вас не должно возникнуть. Нам также будет нужен класс, который из моделей Book и Customer будет передавать информацию в модель Order. Для этого в проекте добавим новый каталог Helpers и класс ShopOrder для него. Реализация этого класса приведена ниже.
/// <summary>
/// Helper class for pass order for custom customer
/// </summary>
public class ShopOrder
{
       public Customer Customer { get; set; }
       public Book Book { get; set; }
}
Этот класс содержит в себе информацию о покупателе и о купленной книге. Этот же класс мы будем использовать для демонстрации обмена между разными модулями. Для того чтобы воспользоваться только что созданным классом, нужно создать новый класс, который наследовать от PubSubEvent.
public class ShopOrderEvent : PubSubEvent<ShopOrder>
{
}
Выше приведена реализация этого класса. Он по своей сути очень простой. Класс будет использоваться только в связке с AggregateEvent. И еще добавим вспомогательный класс, который сгенерит книги и покупателей для нашего примера. Реализация этого класса приведена ниже.
public static class ShopHelper
{
       public static IEnumerable<Book> GenerateBooks()
       {
             yield return new Book { Id = 1, Author = "Jon Skeet", Title = "C# in Depth", Price = 22.5, SN = "ISBN: 9781617291340", Year = new DateTime(2013, 9, 10) };
             yield return new Book { Id = 2, Author = "Martin Fowler", Title = "Refactoring: Improving the Design of Existing Code", Price = 41.52, SN = "ISBN-10: 0201485672", Year = new DateTime(1999, 7, 8) };
             yield return new Book { Id = 3, Author = "Jeffrey Richter", Title = "CLR via C# (Developer Reference)", Price = 35, SN = "ISBN-10: 0735667454", Year = new DateTime(2012, 12, 4) };
       }

       public static IEnumerable<Customer> GenerateCustomers()
       {
             yield return new Customer { Id = 1, Age = 21, FirstName = "Filip", LastName = "Morris" };
             yield return new Customer { Id = 2, Age = 35, FirstName = "Dunkan", LastName = "Maklaud" };
             yield return new Customer { Id = 3, Age = 34, FirstName = "Nikolas", LastName = "Petrol" };
       }
}
Этот класс сделан для того чтобы не добавлять лишний код в наши модели представления, которые будет сложно читать и модифицировать. После внесения всех моделей и вспомогательных классов мы должны получить следующую структуру проекта:

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

No comments:

Post a Comment