Monday, June 9, 2014

Введение в Prism 5. CompositeCommand with IActiveAware

В этой статье мы с вами рассмотрим, как работать с составными командами (класс CompositeCommand) на активной модели в библиотеке Prism 5. Использование класса CompositeCommand уже анализировалось в статьях "Введение в Prism 5. CompositeCommand. Часть первая" и "Введение в Prism 5. CompositeCommand. Часть вторая"Сегодня мы разберем использование интерфейса IActiveAware для активного представления, а также использование всего этого в комбинации с CompositeCommand.
Небольшое отступление и краткий ликбез по использованию команд в активных представлениях. Такие команды часто используются, чтобы можно было делать какие-то действия только в активном представлении. Примером таких действия может служить, например, команда Zoom на панели инструментов, которая заставляет масштабироваться только активный в настоящий момент элемент, как показано в следующей схеме.
Для поддержки этого сценария Prism предоставляет интерфейс IActiveAware. Интерфейс IActiveAware определяет свойство IsActive, которое возвращает true, когда элемент управления активен, и событие IsActiveChanged, которое генерируется всякий раз, когда активное состояние изменяется. Ниже приведена реализация этого интерфейса.
public interface IActiveAware
{
    bool IsActive { get; set; }
    event EventHandler IsActiveChanged;
}
Можно реализовать интерфейс IActiveAware на дочерних представлениях или моделях представления. Это, прежде всего, используется для того, чтобы отследить активное состояние дочернего представления в области. Является ли представление активным, определяется адаптером области, который контролирует представления в пределах определённого элемента управления областью. К примеру, для элемента TabControl, показанного ранее, есть адаптер региона, который устанавливает представление на выбранной в настоящий момент вкладке как активное. Этот адаптер называется RegionActiveAwareBehavior. Он наследуется от интерфейса IRegionBehavoir, у которого объявлено свойство Region и метод Attach, как показано в примере ниже.
public interface IRegionBehavior
{
    IRegion Region { get; set; }
    void Attach();
}
Поведение, которое контролирует объект IRegion и изменяет значение для IsActive, когда объект, который реализует интерфейс IActiveAware, добавляется или удаляется из коллекции. Регион — это класс, реализующий интерфейс IRegion. Регион является контейнером, содержащий контент, который будет отображаться в элементе управления. Следующий код показывает интерфейс IRegion.
public interface IRegion : INotifyPropertyChanged
{
    IViewsCollection Views { get; }
    IViewsCollection ActiveViews { get; }
    IRegionManager Add(object view);
    IRegionManager Add(object view, string viewName);
    IRegionManager Add(object view, string viewName, bool
                        createRegionManagerScope);
    void Remove(object view);
    void Activate(object view);
    void Deactivate(object view);
    object GetView(string viewName);
    IRegionManager RegionManager { get; set; }
    IRegionBehaviorCollection Behaviors { get; }
}
Один из подходов, который позволяет переключать на активное представление, называется View Injection. Что же это такое и как это использовать, распишем более детально, так как основная часть вопросов, которые будут здесь затронуты, есть только в документации по Prism 5 UI Composition, написанной на английском. Для того чтобы создать и отобразить View в регионе, есть два способа: View Discovery и View Injection. Если вас интересуют другие обработчики, поведение, связанное с регионами, то вы можете посмотреть перевод документации к Prism 4.1 на habrahabr Руководство разработчика Prism — часть 7, создание пользовательского интерфейса, которая не потерпела сильных изменений, по сравнению с документацией к Prism 5.  К сожалению, вопросы по View Discovery и View Injection там не затронуты. Если вы не стыкались плотно с регионами, рекомендую акцентировать свое внимание на информации, представленной ниже. Это поможет вам сэкономить много времени при написании своих приложений для работы с регионами.

Представления (Views) могут быть созданы и отображены в регионах либо автоматически, через "View Discovery", или программно, через "View Injection". Эти две технологии определить, как отдельные представления мапятся на именованные регионы в пользовательском интерфейсе приложения. (UI). При использовании view discovery вы устанавливаете отношение с помощью RegionViewRegistry между именем региона и типом представления (view). Когда область создается, регион смотрит для всех ViewTypes, связанных с регионом, и автоматически создает и загружает соответствующие представления. Таким образом, с использованием подхода view discovery у вас нет явного контроля над тем, когда соответствующие виды регионов загружаются и отображаются.

При использовании view injection ваш код получает ссылку на регион и программно добавляет в него представление (view). Как правило, это будет сделано, когда происходит инициализация модуля или в результате действий пользователя. Ваш код будет запрашивать RegionManager для конкретного региона по имени и затем применить соответствующее представление для него. С подходом, который базируется на view injection, у вас больше контроля над загрузкой и отображением представления; у вас также есть возможность удалить данное представление из региона.View Injection не позволяет создать представления для региона, если тот еще не создан.

Поскольку мы рассмотрели, чем отличается View Discovery от View Injectionдавайте внимательно посмотрим на пример UIComposition_Desktop, пример которого можно скачать с библиотекой Prism 5.
Там вы можете увидеть вот такой метод:
/// <summary>
/// Called when a new employee is selected. This method uses
/// view injection to programmatically
/// </summary>
private void EmployeeSelected(string id)
{
    if ( string.IsNullOrEmpty( id ) ) return;
           
    // Get the employee entity with the selected ID.
    Employee selectedEmployee = this.dataService.GetEmployees().FirstOrDefault(item => item.Id == id);

    // TODO: 05 - The MainRegionController displays the EmployeeSummaryView in the Main region when an employee is first selected.

    // Get a reference to the main region.
    IRegion mainRegion = this.regionManager.Regions[RegionNames.MainRegion];
    if (mainRegion == null) return;

    // Check to see if we need to create an instance of the view.
    EmployeeSummaryView view = mainRegion.GetView("EmployeeSummaryView") as EmployeeSummaryView;
    if (view == null)
    {
        // Create a new instance of the EmployeeDetailsView using the Unity container.
        view = this.container.Resolve<EmployeeSummaryView>();

        // Add the view to the main region. This automatically activates the view too.
        mainRegion.Add(view, "EmployeeSummaryView");
    }
    else
    {
        // The view has already been added to the region so just activate it.
        mainRegion.Activate(view);
    }

    // Set the current employee property on the view model.
    EmployeeSummaryViewModel viewModel = view.DataContext as EmployeeSummaryViewModel;
    if (viewModel != null)
    {
        viewModel.CurrentEmployee = selectedEmployee;
    }
}
В этом методе расписано действие каждой строчки. Так выглядит один из способов использования View Injection.

Перейдем к практике. Для этого создадим новый WPF проект и назовем его ActiveAwareSample, как показано на рисунке ниже.
Затем по старинке, как описано в статье "Введение в Prism 5. Bootstrapper", реализуем начальную структуру проекта. В этом примере я также решил добавить возможности по обработке действия пользователя, которые были рассмотрены в статье "Использование объектов запроса взаимодействия в Prism 5". После того как мы создали оболочку нашего главного окна и загрузчик, проект должен запуститься. Если у вас что-то не получилось, просто повторите пошагово действия, описанные в статье "Введение в Prism 5. Bootstrapper". Следующим этапом приведем структуру нашего проекта к следующему виду:
Сейчас вкратце постараюсь расписать эту структуру, чтобы у вас не возникало недопонимания, почему структура проекта построена именно так. Структура проекта основана на использовании паттерна MVVM и адаптации этого паттерна под возможности, которые нам предоставляет библиотека Prism 5.
  • Helpers – вспомогательные классы которые упрощают разработку нашей программной модели;
  • Models – использование моделей с паттерна MVVM;
  • Modules – разбиение проекта на модули с помощью Prism 5;
  • ViewModels – модели представления паттерна MVVM;
  • Views – представления паттерна MVVM (окна, контроли и другие компоненты дизайна);
  • Regions – разбиение на части называемые заполнителями или иными словами регионами с помощью библиотеки Prism 5.
Поскольку темой нашей статьи является использование класса CompositeCommand и то, как этот класс может взаимодействовать с интерфейсом IActiveAware, первым делом мы создадим новый класс ShowModelCommand в папке Helpers следующей структуры:
public static class ShowModelCommand
{
    public static CompositeCommand ShowActiveModelCommands = new CompositeCommand(true);
}

public class ShowModelCommandProxy
{
    public CompositeCommand ShowActiveModelCommand
    {
        get { return ShowModelCommand.ShowActiveModelCommands; }
    }
}
Если обратить внимание на статический класс ShowModelCommand, то можем увидеть, что переменная ShowActiveModelCommands создана с использованием конструктора с параметров, в который мы передаем значение true. Это необходимо для того чтобы иметь возможность создавать композитную команду, которая будет следить за изменением активной команды. Пожалуй, чтобы лучше понять, что имеется ввиду, посмотрим на класс DelegateCommandBase, от которого наследован класс DelegateCommand и который идет вместе с библиотекой Prism 5. 
public abstract class DelegateCommandBase : ICommand, IActiveAware
{
    protected readonly Func<object, bool> _canExecuteMethod;
    protected readonly Func<object, Task> _executeMethod;
    protected DelegateCommandBase(Action<object> executeMethod, Func<object, bool> canExecuteMethod);
    protected DelegateCommandBase(Func<object, Task> executeMethod, Func<object, bool> canExecuteMethod);
    public bool IsActive { get; set; }

    public virtual event EventHandler CanExecuteChanged;

    public virtual event EventHandler IsActiveChanged;

    protected bool CanExecute(object parameter);
    protected Task Execute(object parameter);
    protected virtual void OnCanExecuteChanged();
    protected virtual void OnIsActiveChanged();
    public void RaiseCanExecuteChanged();
}
Обратите внимание, что данный класс наследуется от интерфейса IActiveAware. Это значит, что мы можем отслеживать активность на уровне команд. Например, если у вас в модели данных есть несколько команд, то их выполнение вы можете настроить по такому принципу, что будет выполняться только активная команда. Это удобно делать в связке с классом CompositeCommand, что мы и постараемся продемонстрировать в данной статье. 
Пока вернемся к нашему классу ShowModelCommand и посмотрим, что рядом с ним реализован не статический класс ShowModelCommandProxy, который является, по сути, самой примитивной реализацией паттерна Proxy. Такой подход используется очень часто в примерах, которые идут в поставке с библиотекой Prism 5. В таком подходе есть несколько плюсов. Первым плюсом такого подхода является возможность протестировать данный класс с помощью Moq и какого-то фреймворка для тестирования xUnit, NUnit, MS Test и другие. Если вы используете для написания вашего кода и покрытия его тестами TDD или BDD, то такой подход с написанием оболочки или прокси для статического класса позволяет протестировать его поведение. Второй плюс такого подхода, заключается в использовании Dependency Injection, а именно Constructor Injection, который и будет продемонстрирован в нашей статье.
Давайте оставим наш класс ShowModelCommand в стороне и приступим к создании модели, которая будет использована в приложении. Поскольку я почти во всех своих приложениях создаю модель Book, в которой храню информацию о книгах на продажу, эта статья не станет исключением. Не знаю, плохо это или хорошо, что во многих моих статьях встречается данная модель, просто мне кажется, что пример с книгой позволяет проще понять преподнесенный материал и акцентировать внимание на более сложных составных деталях программы. Поэтому давайте по традиции приведем код данного класса Book, который добавим в папку Models, поскольку в этой папке должна храниться вся информация о моделях, которые будут использованы в данном проекте. 
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);
        }
    }
}
Данная модель ‒ это один из простых способов реализации классической модели с помощью паттерна MVVM и с использованием Призма. Давайте посмотрим, как будет выглядеть модель представления BookViewModel для созданной выше модели. 
public class BookViewModel : IActiveAware
{
    private Book _book;
    public BookViewModel(Book book)
    {
        _book = book;
        SaveCommand = new DelegateCommand(Save);
        NotificationRequest = new InteractionRequest<INotification>();
    }

    public Book Book
    { 
        get
        {
            return _book;
        }
    }

    public DelegateCommand SaveCommand { get; set; }

    public InteractionRequest<INotification> NotificationRequest { get; private set; }

    #region Private Methods
    private void Save()
    {
        NotificationRequest.Raise(
                new Notification { Content = Book.Id + " " + Book.Author, Title = "Notification" }
                );
    }
    #endregion

    public bool IsActive
    {
        get
        {
            return SaveCommand.IsActive;
        }
        set
        {
            SaveCommand.IsActive = value;
        }
    }

    public event EventHandler IsActiveChanged;
}
В этой модели представления мы уже видим использование интерфейса IActiveAware, с помощью которого мы проставляем для нашей команды SaveCommand свойство IsActive. Это сделано для того, чтобы мы могли отслеживать изменение активности команды с помощью класса CompositeCommand. Также в нашем примере мы использовали возможность показывать пользователям сообщения, которые доступны в Prism 5, через классы Notification и Confirmation. Как использовать данные классы, описано в статье "Использование объектов запроса взаимодействия в Prism 5". Если вкратце, то это один из способов взаимодействия с пользователем с помощью диалоговых окон, чтобы не нарушить использование паттерна MVVM. Если не углубляться в детали, то при выполнении команды SaveCommand у нас будет показано диалоговое окно, в котором будет отображена информация об активной книге.

Следующим делом добавим реализацию для нашей оболочки Shell.xaml, поскольку от реализации оболочки зависит реализация заполнителей (Regions). Ниже приведена реализация главного окна (Shell.xaml).
<Window x:Class="ActiveAwareSample.Shell"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:prism="clr-namespace:Microsoft.Practices.Prism.Regions;assembly=Microsoft.Practices.Prism.Composition"
        xmlns:inf="clr-namespace:ActiveAwareSample.Helpers"
        Title="IActiveAware Sample" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <ToolBar Grid.Row="0">
            <Button Command="{x:Static inf:ShowModelCommand.ShowActiveModelCommands}">Show Active Models</Button>
            <Separator />
        </ToolBar>
        <TabControl x:Name="mainTab" Grid.Row="1"
            prism:RegionManager.RegionName="TabRegion"></TabControl>
    </Grid>
</Window>
В данном окне мы видим один контрол ToolBar, который содержит единственную кнопку, которая связана с созданной ранее нами Composite Command (примечание: смотреть класс ShowModelCommand). Также для данного окна добавлен один элемент TabControl, в котором мы отобразим информацию о наших книгах (модель Book).  Следующим делом мы создадим модель представления ShellViewModel в папке ViewModels и свяжем с помощью этой модели представления использование класса BookViewModel с представлением. 
public class ShellViewModel
{
    private ShowModelCommandProxy _commandProxy;
    public ShellViewModel(ShowModelCommandProxy commandProxy)
    {
        _commandProxy = commandProxy;
        InitializeBooks();
    }

    private void InitializeBooks()
    {
        Books = new ObservableCollection<BookViewModel>();
        foreach(var book in GenerateBooks())
        {
            var viewModel = new BookViewModel(book);
            _commandProxy.ShowActiveModelCommand.RegisterCommand(viewModel.SaveCommand);
            Books.Add(viewModel);
        }
    }

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

    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) };
    }
}
Модель представления ShellViewModel связывает воедино модель Book с моделью представления BookViewModel. Теперь нам нужно создать представление, которое и отобразит нам информацию о книге. Для этого мы перейдем в папку Regions и добавим новый UserControl, который назовем BookView. Этот контрол будет заполнителем для нашего TabControl с представления Shell.xaml
<UserControl x:Class="ActiveAwareSample.Views.Regions.BookView"
             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"
             xmlns:prism="clr-namespace:Microsoft.Practices.Prism.Interactivity.InteractionRequest;assembly=Microsoft.Practices.Prism.Interactivity"
             xmlns:interactivity="clr-namespace:Microsoft.Practices.Prism.Interactivity;assembly=Microsoft.Practices.Prism.Interactivity"
             xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <i:Interaction.Triggers>
            <prism:InteractionRequestTrigger SourceObject="{Binding NotificationRequest, Mode=OneWay}">
                <interactivity:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True"/>
            </prism:InteractionRequestTrigger>
        </i:Interaction.Triggers>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <TextBlock Text="Author" Grid.Row="0" Grid.Column="0"/>
        <TextBlock Text="Title" Grid.Row="1" Grid.Column="0" />
        <TextBlock Text="Serial Number" Grid.Row="2" Grid.Column="0" />
        <TextBlock Text="Date" Grid.Row="3" Grid.Column="0" />
        <TextBlock Text="Price" Grid.Row="4" Grid.Column="0" />
        <TextBox Text="{Binding Book.Author}" Margin="2" Grid.Row="0" Grid.Column="1"/>
        <TextBox Text="{Binding Book.Title}" Margin="2" Grid.Row="1" Grid.Column="1"/>
        <DatePicker SelectedDate="{Binding Book.Year}" Margin="2" Grid.Row="2" Grid.Column="1"/>
        <TextBox Text="{Binding Book.SN}" Margin="2" Grid.Row="3" Grid.Column="1"/>
        <TextBox Text="{Binding Book.Price}" Margin="2" Grid.Row="4" Grid.Column="1"/>
        <Button Content="Save Price" Margin="2" Grid.Row="5" Grid.Column="0"
               Grid.ColumnSpan="2" HorizontalAlignment="Center"  Command="{Binding SaveCommand}" />
    </Grid>
</UserControl>
В дизайнере интерфейс данного контрола выглядит следующим образом:
Также в коде мы можем заметить использование таких строк:
<i:Interaction.Triggers>
            <prism:InteractionRequestTrigger SourceObject="{Binding NotificationRequest, Mode=OneWay}">
                <interactivity:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True"/>
            </prism:InteractionRequestTrigger>
        </i:Interaction.Triggers>
Если не углубляться в детали, то с помощью данной разметки мы сможем показать диалоговое окно Prism 5, которое доступно нам благодаря классу Notofication и классу PopupWindowAction, который, по сути, и отображает это окно. Теперь все эти модели представления и представления нужно связать воедино. Для этого я использую модульность, которую предоставляет нам для работы Prism. Для этого перейдем в папку Modules и создадим новый класс TabModule, который мы наследуем от интерфейса IModule. Реализация данного класса приведена ниже. 
public class TabModule : IModule
{
    private IRegionManager _regionManager;
    private IUnityContainer _container;

    public TabModule(IRegionManager regionManager, IUnityContainer container)
    {
        _regionManager = regionManager;
        _container = container;
    }

    public void Initialize()
    {
        IRegion tabRegion = _regionManager.Regions["TabRegion"];
        var mainViewModel = _container.Resolve<ShellViewModel>();
        BookView lastView = null;
        foreach (var bookViewModel in mainViewModel.Books)
        {
            var tabView = _container.Resolve<BookView>();
            tabView.DataContext = bookViewModel;
            tabRegion.Add(tabView);
            lastView = tabView;
        }

        tabRegion.Activate(lastView);
    }
}
В этом модуле мы используем подход, рассмотренный ранее с использованием view injection. Осталось теперь, для того чтобы это все "взлетело", добавить инициализацию данного модуля в нашем загрузчике Bootstrapper
public class Bootstrapper : UnityBootstrapper
{
    protected override System.Windows.DependencyObject CreateShell()
    {
        return Container.Resolve<Shell>();
    }

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

    protected override void InitializeModules()
    {
        IModule tabModule = Container.Resolve<TabModule>();
        tabModule.Initialize();
    }
}
После проделанной работы мы можем запустить наш проект и убедиться в том, что все работает.
Кнопка "Show Active Models" показывает информацию по активной в данный момент модели, чего мы, в принципе, и добивались. Теперь один нюанс, который я упустил в реализации, и из-за этого нюанса приложение выглядит не совсем таким, как ожидалось. В нашем контроле TabControl нет названия для каждой табы. Если вы вдруг захотите добавить какой-то текст в свой TabControl, вам нужно будет сделать небольшой хак, как описано в примере TabControl as a region. Это не сложно, потребует от вас некоторого извращения и грязных трюков, которые не приветствуются в WPF, но иногда без них просто не обойтись. Исходная структура проекта у меня получилась вот такой:
Исходники к статье вы можете скачать по ссылке ActiveAwareSample. Если у вас возникнут какие-то вопросы по статье, буду рад на них ответить. 

No comments:

Post a Comment