Tuesday, May 27, 2014

Введение в Prism 5. CompositeCommand. Часть первая

В этой статье мы рассмотрим использование составных команд (класс CompositeCommand) при построении WPF приложений, с использованием библиотеки Prism 5. Если вам нужен простенький пример чтобы понять, как работает CompositeCommand вы можете посмотреть пример с моей статьи Interaction Triggers and Commands in Prism 5.  В той статье есть краткое упоминание об использовании CompositeCommand. Поэтому если вы просто хотите ознакомится с данной темой посмотрите предыдущую статью, так как в этой теме я буду рассматривать и объяснять нюансы которые встречаются в приложениях, которые выходят в поставку.
Во многих случаях команда, определенная моделью представления, будет связана с элементами управления в связанном представлении так, чтобы пользователь мог непосредственно вызвать её изнутри представления. Однако в некоторых случаях может понадобиться вызвать команды от одной или более моделей представления из элемента управления в родительском представлении. Например, если ваше приложение позволяет пользователю редактировать разнообразные элементы одновременно, можно позволить пользователю сохранять все элементы, используя единственную команду, представленную кнопкой на панели инструментов. В этом случае команда Save All вызовет каждую из команд Save, реализованную экземпляром модели представления каждого элемента, как показано на следующей иллюстрации.
Примечание. Картинка взята с документации по Prism. Такой сценарий Prism поддерживает через класс CompositeCommand. Класс CompositeCommand представляет команду, которая складывается из разнообразных дочерних команд. Когда вызывается составная команда, каждая из её дочерних команд вызывается поочередно.
Составные команды по своему использованию могут использоваться у двоих сценариях. Одним из популярных использований составных команд является использование этих комманд на родительском уровне представления, чтобы скоординировать то, как вызываются команды на дочернем уровне представления. Например, нам может захотеть, чтобы команды выполнялись по цепочке как в приведенной картинку выше с использование команды SaveAll. В других случаях вы захотите, чтобы команда была выполнена только на активном представлении. В этом случае, составная команда выполнит дочерние команды только на представлениях, которые являются активными. Например, можно захотеть реализовать команду Zoom на панели инструментов, которая заставляет масштабироваться только активный в настоящий момент элемент, как показано в следующей схеме.
Такой сценарий реализуется в Prism 5 благодаря интерфейсу IActiveAware. Интерфейс IActiveAware определяет свойство IsActive, которое возвращает true, когда элемент управления активен, и событие IsActiveChanged, которое генерируется всякий раз, когда активное состояние изменяется. К сожалению, я не смог показать в данной теме эти два сценария, поэтому мы рассмотрим реализацию CompositeCommand с которой вы будете часто встречаться, отображая коллекцию элементов в представлении — когда вы нуждаетесь в UI для каждого элемента в коллекции, который будет связан с командой на родительском уровне представления (вместо уровня элемента). Ниже на рисунке представлен пример окна, которое мы реализуем в данной статье.
Реализация данного окна берет свое начала со статьи Введение в Prism 5. Использование EventAggregator. Часть первая, и будет включать в себя использование таких элементов Prism 5 как модульность, использование загрузчиков, работа с регионами, EventAggreagator и CompositeCommand. Если вы не знакомы с этими терминами, то возможно вам нужно почитать более ранние статьи по этому поводу, чтобы понимать, о чем идет речь. Я буду давать краткое объяснение почему я использовал в той или иной ситуации такой подход, но он не будет описывать саму суть этого подхода, только причину.  Ну что ж приступим. Поскольку для написания проектов я использую из недавних пор Visual Studio 2013 я создам приложением .NET Framework 4.5.1. Вы же можете использовать по желанию .NET Framework 4.5, так как на более ранних версиях не работает Prism 5. Создадим новое WPF приложение и назовем его EventAggregatorSample.
Далее через NuGet Packages поставим саму библиотеку Prism, а также библиотеку для работы с загрузчиком, который работает с IoC контейнером Unity.
Сначала необходимо создать загрузчик и сделать стартовую страничку термином Prism это оболочка (Shell). Создать начальный загрузчик можно, проделав все этапы со статьи "Введение в Prism 5. Bootstrapper". Если вам не приходилось писать свой загрузчик, то просто перейдите по ссылке выше и проделайте те этапы, которые описаны в указанной статье.
Структура проекта имеет следующий вид:
Кратко об этой структуре которою я частенько использую, если приложение не очень сложное:
  • Assets – стили, ресурсы, словари;
  • Helpers – вспомогательные классы, extensions methods, базовая логика;
  • Models – модели данных которые используются в паттерне MVVM;
  • Modules – разбиение проектов на модули с помощью интерфейса IModule с Prism 5;
  • Presenters – презентеры для добавления логики обработки пользователей;
  • ViewModels – модель представления паттерна MVVM которая связывает модель с представлением;
  • Views – графическое представление (Views) паттерна MVVM – окна, контролы и т.д;
  • Regions – подвид представления которое в Prism 5 носит название регионов (заполнителей)  и используется для разделения UI представления на части.

Небольшое пояснение к слою Presenters который приведен в списке выше.  В оригинале слой Presenters используется для внедрения в свой проект паттерна Model-View-Presenter-ViewModel (MVPVM). Но в приведенном примере я туда добавлю по сути модель представления для некоторых моделей и обработку команд. Поэтому пусть не пугает вас слово Presenter в названиях классов, воспринимайте это как модель представления и у вас не будет проблем.
Первым делом перейдем в нашу оболочку и добавим необходимые регионы для отображения наших данных.
Добавим код который приведен ниже.
<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="400" Width="650">
    <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>
Следующим этапом нам необходимо добавить модели который будут использоваться в программе. Поскольку у нас программа предназначена для продажи книг то для того чтобы не раздувать логику приложения будет использоваться три модели: Book – модель для хранения информации о книге и ее автора, Customer – модель в которой хранится информация о покупателе и модель Order в которой будут информация о покупке.
Модель 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);
             }
       }
}
Модель 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 ObservableCollection<Book> Books { get; set; }

       private double _sum;
       public double Sum
       {
             get { return _sum; }
             set
             {
                    _sum = value;
                    OnPropertyChanged(() => Sum);
             }
    }
}
Запомните один важный факт: у ваших моделях данных не должно быть никакой обработки действий пользователя. Только голые данные и уведомление о том, что они изменились (интерфейс INotifyPropertyChanged).
Для передачи данных о заказе между регионами мы воспользуемся классом EventAggregator. Для этого в папке 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 библиотеки Prism 5. Ниже приведен код реализации класса ShopOrderEvent который наследуется от класса PubSubEvent.
public class ShopOrderEvent : PubSubEvent<ShopOrder>
{
}
Также для генерации тестовых данных я добавил класс ShopHelper.
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" };
       }
}
В реальном же приложении вам можно будет воспользоваться поддержкой сервисов в Prism 5, для того чтобы выгрузить данные с БД. Или реализовать например паттерн Репозиторий (анг. Repository).
Следующим этапом приступим к реализации логики для региона ShopRegion. Для этого перейдем в папку ViewModels и добавим модель представления 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
}
Эта модель представления очень проста. В ней храниться коллекция книг Books, коллекция покупателей Customers команда BuyBookCommand которая с помощью EventAggregator уведомит подписчиков о том, что такой то покупатель купил такую то книгу. Если вы знакомы с паттерном MVVM то данный код вас не должен напугать. Для данной модели представления нужен добавить контрол, который будет отображать информацию из наших моделей Book и Customer. Для этого перейдем в папку Views\Regions и добавим новый UserControl который назовем ShopOrderView.
<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}">Купить</Button>
        </StackPanel>
    </Grid>
</UserControl>
Этот контрол представляет собой два выпадающих списка для выбора и одну кнопку “Купить”. Ниже представлен внешний вид данного контрола в дизайнере.
Следующим этапом нам нужно добавить в папку Modules новый модуль ShopModule, который необходимо наследовать от интерфейса IModule библиотеки 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 и переопределить метод InitializeModules
protected override void InitializeModules()
{
    IModule shopModule = Container.Resolve<ShopModule>();
    shopModule.Initialize();
}
После этого вы можете запустить приложение и сможете увидеть результат с работающей левой частью (регион ShopRegion)

Продолжение статьи CompositeCommand in Prism 5. Part 2

No comments:

Post a Comment