В
этой статье мы рассмотрим поведение с привязкой команд в библиотеке Prism 5. Мы также рассмотрим, как связываются триггеры с
командами в этой библиотеке. Надеюсь,
вам уже приходилось работать с интерфейсом ICommand в WPF, поэтому как минимум первая часть этой статьи не должна
вызвать у вас недопонимание. Эту статью я решил написать в перерыве между подготовкой статьи об использовании CompositeCommand в Prism 5, в которой я затронул тему построения модульного приложения,
используя паттерн EventAggregator и такие возможности призма, как модульность и работу с
регионами. Поэтому в этой теме мы затронем CompositeCommand разве что мельком,
так как в ближайшее время по данной теме я выложу целую статью.
Приступим к реализации. Первым делом создадим новое WPF-приложение и выберем .NET Framework 4.5, так как Prism 5 не работает с фреймфорком ниже этой версии.
Приступим к реализации. Первым делом создадим новое WPF-приложение и выберем .NET Framework 4.5, так как Prism 5 не работает с фреймфорком ниже этой версии.
Далее
через NuGet Packages поставим саму
библиотеку Prism,
а также библиотеку для работы с загрузчиком, который работает с IoC контейнером Unity.
Сначала необходимо создать загрузчик и сделать стартовую страничку
(термином Prism это оболочка (Shell). Создать начальный загрузчик можно, проделав все этапы
со статьи "Введение в Prism 5. Bootstrapper". Если вам не приходилось писать свой загрузчик, то
просто перейдите по ссылке выше и проделайте те этапы, которые описаны в указанной
статье. Первым делом создадим модель 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);
}
}
}
Примечание. Одна из основных проблем начинающих разработчиков состоит в том, что
они часто путают понятие модель (Model) и модель представления (ViewModel). Основное отличие двух понятий заключается в том, что модель
не должна обрабатывать хоть каким-то образом логику пользователя. Модель может только
отображать некоторые данные с БД, с какого-то сервиса и т.д. Если у вас
появятся события обработки, или команды, или действия пользователя, которые
потребуют обработки, то это уже модель представления.
Следующим
этапом нам необходимо для нашей модели Book добавить модель представления BookViewModel, которая будет
отвечать за работу с данной моделью и реагировать на команды, которые нам будет
присылать наше представление (View). Я старался по
максимуму упростить логику, чтобы было понятно, как это все работает.
public class BookViewModel : BindableBase
{
#region Constructors
public
BookViewModel()
{
Books = new ObservableCollection<Book>(GenerateBooks());
}
#endregion
#region Static Methods
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) };
}
#endregion
#region Public Properties
public ObservableCollection<Book> Books { get; set; }
private Book _currentBook;
public Book CurrentBook
{
get { return
_currentBook; }
set
{
_currentBook
= value;
OnPropertyChanged(()
=> CurrentBook);
RemoveBookCommand.RaiseCanExecuteChanged();
}
}
#endregion
#region Commands
private DelegateCommand _removeBookCommand;
public DelegateCommand RemoveBookCommand
{
get { return
_removeBookCommand ?? (_removeBookCommand = new DelegateCommand(RemoveBook, CanRemoveBook)); }
}
private bool
CanRemoveBook()
{
return
CurrentBook != null;
}
private void
RemoveBook()
{
Books.Remove(CurrentBook);
}
#endregion
}
В данной модели представления добавлена одна команда RemoveBookCommand типа DelegateCommand (DelegateCommand является реализацией интерфейса ICommand, которую нам предоставляет библиотека Prism), с ее помощью мы можем удалить с нашей коллекции книг выбранную нами книгу. Теперь немного подправим нашу оболочку Shell.xaml, в которой отобразим нашу кнопку для удаления
книги, и отображение всех книг, которые у нас есть. Реализация этой оболочки
приведена ниже.
<Window x:Class="PrismCommandSample.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Command Sample" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"
/>
<RowDefinition Height="Auto"
/>
</Grid.RowDefinitions>
<ListBox Grid.Row="0" ItemsSource="{Binding Books}" SelectedItem="{Binding CurrentBook}">
<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>
<Button Content="Удалить
книгу" Grid.Row="1" Width="120" Command="{Binding RemoveBookCommand}"></Button>
</Grid>
</Window>
Для того чтобы установить свойство DataContext для нашей оболочки модель представления BookViewModel, нам
нужно подправить наш загрузчик Bootstrapper например как показано ниже.
public class Bootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
return
Container.Resolve<Shell>();
}
protected override void
InitializeShell()
{
Application.Current.MainWindow = (Window)Shell;
var
viewModel = Container.Resolve<BookViewModel>();
Application.Current.MainWindow.DataContext = viewModel;
Application.Current.MainWindow.Show();
}
}
После проделанных действий попробуем запустить наш проект и посмотреть на
результат.
Такой
пример использования команд вы, наверное, уже встречали ранее. Теперь
пройдемся по новинкам, которые стали доступны в Prism 5. Одним из таких нововведений являются так званные Task-Based Delegate Command. Существует множество сценариев, когда команда будет
выполняться продолжительное время. Примером такой команды может быть транзакция до базы данных (БД), при этом интерфейс пользователя должен отвечать
на действия пользователя. Реализуется все это с помощью метода FromAsyncHandler класса DelegateCommand, который позволяет создать новый экземпляр класса DelegateCommand с асинхронного метода.
public static DelegateCommand FromAsyncHandler(Func<Task> executeMethod);
public static DelegateCommand FromAsyncHandler(Func<Task> executeMethod, Func<bool> canExecuteMethod);
Интересная новость состоит также в том, что метод Execute() класса
DelegateCommand
возвращает объект класса Task. Использовать метод FromAsyncHandler можно так:
public DelegateCommand SaveBookCommand { get; private set; }
SaveBookCommand = DelegateCommand.FromAsyncHandler(SaveBook,
CanSaveBook);
private bool
CanSaveBook()
{
return true;
}
private Task SaveBook()
{
return Task.FromResult(10000);
}
Следующее важное отличие Prism 5 от Prism 4.1 состоит
в том, что с Prism 5 убрали пространство имен Microsoft.Practices.Prism.Commands со всеми классами, которые там были. Ниже
представлены классы, которые были размещены в этом пространстве имен:
- ButtonBaseClickCommandBehavior
- Click
- CommandBehaviorBase<T>
- CompositeCommand
- DelegateCommand
- DelegateCommand<T>
- DelegateCommandBase
Методом
экспериментов и документации было обнаружено, что структура указанных классов
изменилась. Классы ButtonBaseClickCommandBehavior и Click
были удалены с использования, а класс CommandBehaviorBase<T> был перемещен в пространство имен Microsoft.Practices.Prism.Interactivity.
Остальные
классы потерпели небольших изменений и были перемещены в сборку Microsoft.Practices.Prism.Mvvm, но оставили старое пространство имен.
По
сути, был сделан только небольшой рефакторинг. Убрано старое, и разделено по
новым сборкам то, что было. Давайте рассмотрим, как можно использовать CommandBehavoirBase. Для этого создадим класс ButtonClickBehavior.
public class ButtonClickBehavior : CommandBehaviorBase<ButtonBase>
{
public
ButtonClickBehavior(ButtonBase targetObject) : base(targetObject)
{
targetObject.Click
+= OnClick;
}
private void
OnClick(object sender, System.Windows.RoutedEventArgs
e)
{
ExecuteCommand(null);
}
}
Теперь нужно реализовать Attached Properties, чтобы использовать созданный выше класс ButtonClickBehavior. Для этого создадим другой класс ButtonBaseBehaviorHelper. Ниже приведена реализация этого класса.
public static class ButtonBaseBehaviorHelper
{
private static readonly DependencyProperty ButtonCommandBehaviorProperty
= DependencyProperty.RegisterAttached(
"ButtonCommandBehavior",
typeof(ButtonClickBehavior),
typeof(ButtonBaseBehaviorHelper),
null);
public static readonly DependencyProperty CommandProperty
= DependencyProperty.RegisterAttached(
"Command",
typeof(ICommand),
typeof(ButtonBaseBehaviorHelper),
new PropertyMetadata(OnSetCommandCallback));
public static readonly DependencyProperty CommandParameterProperty
= DependencyProperty.RegisterAttached(
"CommandParameter",
typeof(object),
typeof(ButtonBaseBehaviorHelper),
new PropertyMetadata(OnSetCommandParameterCallback));
public static ICommand GetCommand(Control control)
{
return
control.GetValue(CommandProperty) as ICommand;
}
public static void
SetCommand(Control control, ICommand command)
{
control.SetValue(CommandProperty,
command);
}
public static void
SetCommandParameter(Control control, object parameter)
{
control.SetValue(CommandParameterProperty,
parameter);
}
public static object
GetCommandParameter(Control control)
{
return
control.GetValue(CommandParameterProperty);
}
private static void
OnSetCommandCallback
(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
var control
= dependencyObject as Control;
if
(control != null)
{
ButtonClickBehavior behavior = GetOrCreateBehavior(control);
behavior.Command
= e.NewValue as ICommand;
}
}
private static void
OnSetCommandParameterCallback
(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
var control
= dependencyObject as Control;
if
(control != null)
{
ButtonClickBehavior behavior = GetOrCreateBehavior(control);
behavior.CommandParameter
= e.NewValue;
}
}
private static ButtonClickBehavior GetOrCreateBehavior(Control
control)
{
var
behavior =
control.GetValue(ButtonCommandBehaviorProperty)
as ButtonClickBehavior;
if
(behavior == null)
{
behavior
= new ButtonClickBehavior((ButtonBase)control);
control.SetValue(ButtonCommandBehaviorProperty,
behavior);
}
return
behavior;
}
}
Использовать эти Attached Properties можно следующим образом:
<Button Content="Удалить
книгу" Grid.Row="1" Width="120"
source:ButtonBaseBehaviorHelper.Command="{Binding Path=RemoveBookCommand}"
/>
Не хочу сказать ничего плохого в адрес разработчиков Prism, но посмотрите, как можно сделать то, что предложено, намного проще. Для этого нам понадобится воспользоваться библиотекой Interactivity
с Expression Blend.
Теперь
если мы воспользуемся пространством имен System.Windows.Interactivity, а также пространством имен Interactivity с библиотеки Prism 5 (класс InvokeCommandAction), то наш код может упроститься к такому:
<Button Content="Удалить
книгу" Grid.Row="1" Width="120"
>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<prism:InvokeCommandAction Command="{Binding RemoveBookCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
Нам
нужно добавить в представление (View), в котором мы используем класс InvokeCommandAction, ссылку на два пространства имен.
xmlns:prism="clr-namespace:Microsoft.Practices.Prism.Interactivity;assembly=Microsoft.Practices.Prism.Interactivity"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Так
в чем, собственно, проблема, которая тянется очень давно. Посмотрите, как выглядит
класс InvokeCommandAction:
Я
специально на скриншоте выделил пространство имен, которое нам понадобилось
добавить вручную. Неужели так сложно было добавить свой MarkupExtension, в котором можно было бы реализовать использование сборки Inveractivity, которая
используется в поставке Prism 5. Дело в том, что если я буду использовать сборку System.Windows.Interactivity, то смогу написать
свой Behaviors
на основании поведения, которое есть в данном пространстве имен, и использовать
паттерн EventToCommand, например, с MVVM Light Toolkit. В итоге имеем, что вариант замены у нас есть: EventToCommand, а класс CommandBehaviorBase у нас избыточный,
так как требует много логики, часть из которой лучше сделать через Behaviors.
Единственный плюс только в CommandBehaviorBase - возможности задать команду, которая будет использована,
но делать это нужно через Attached Property.
И
напоследок осталось рассмотреть, что такое составные команды (CompositeCommand) в Prism. Во многих
случаях команда, определенная моделью представления, будет связана с
элементами управления в связанном представлении так, чтобы пользователь мог
непосредственно вызвать её изнутри представления. Однако в некоторых случаях может понадобиться вызвать команды от одной или более моделей представления из
элемента управления в родительском представлении. Например, если ваше приложение позволяет
пользователю редактировать разнообразные элементы одновременно, можно позволить
пользователю сохранять все элементы, используя единственную команду,
представленную кнопкой на панели инструментов. В этом случае команда Save All вызовет каждую из команд
Save
, реализованную экземпляром модели представления каждого
элемента, как показано на следующей иллюстрации.
Примечание.
Картинка взята с документации по Prism. Такой сценарий Prism поддерживает через
класс CompositeCommand. Класс
CompositeCommand
представляет команду, которая
складывается из разнообразных дочерних команд. Когда вызывается составная
команда, каждая из её дочерних команд вызывается поочередно. Для этого над
нашей моделью Book нужно написать presenter, который возьмет на себя всю логику по удалению
книги со списка. Пример показан ниже.
public class BookPresenterViewModel
{
#region Private Variables
private readonly Book _book;
#endregion
#region Constructor
public
BookPresenterViewModel(Book book)
{
_book =
book;
RemoveBookCommand
= new DelegateCommand(Remove);
}
#endregion
#region Public Properties
public string Author
{
get { return
_book.Author; }
set {
_book.Author = value; }
}
public double Price
{
get { return
_book.Price; }
set {
_book.Price = value; }
}
#endregion
#region Commands
public event EventHandler<DataEventArgs<BookPresenterViewModel>> Removed;
public DelegateCommand RemoveBookCommand { get; private set; }
private void
Remove()
{
OnRemoved(new DataEventArgs<BookPresenterViewModel>(this));
}
private void
OnRemoved(DataEventArgs<BookPresenterViewModel> e)
{
EventHandler<DataEventArgs<BookPresenterViewModel>> removedHandler = Removed;
if
(removedHandler != null)
{
removedHandler(this, e);
}
}
#endregion
}
Теперь нужно
добавить новый класс, который будет реализовывать логику по удалению книг с
использованием класса CompositeCommand.
public static class BooksCommands
{
public static CompositeCommand RemoveAllBooksCommand = new CompositeCommand();
}
public class BooksCommandProxy
{
public virtual CompositeCommand RemoveAllBooksCommand
{
get { return BooksCommands.RemoveAllBooksCommand; }
}
}
Мы использовали дополнительный класс BooksCommandProxy, чтобы для нашего примера можно было воспользоваться возможностями Constructor Injection, которые нам доступны благодаря загрузчику UnityBootstrapper с
использованием IoC контейнера Unity. Из-за
того, что мы добавили обертку для нашей модели Book, нам
нужно перестроить нашу модель представления BookViewModel для работы с этим презентером
BookPresenterViewModel.
public class BookViewModel : BindableBase
{
#region Private Variables
private readonly BooksCommandProxy _commandProxy;
#endregion
#region Constructors
public
BookViewModel(BooksCommandProxy commandProxy)
{
_commandProxy
= commandProxy;
GenerateBooksModels();
}
#endregion
#region Static Methods
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) };
}
#endregion
#region Public Properties
public ObservableCollection<BookPresenterViewModel> Books { get; set; }
private Book _currentBook;
public Book CurrentBook
{
get { return
_currentBook; }
set
{
_currentBook
= value;
OnPropertyChanged(()
=> CurrentBook);
}
}
#endregion
#region Private Methods
private void
GenerateBooksModels()
{
Books = new ObservableCollection<BookPresenterViewModel>();
foreach (Book book in GenerateBooks())
{
var
bookPresentationModel = new BookPresenterViewModel(book);
Books.Add(bookPresentationModel);
// Subscribe to the Removed event on the individual books.
bookPresentationModel.Removed
+= BookRemoved;
_commandProxy.RemoveAllBooksCommand.RegisterCommand(bookPresentationModel.RemoveBookCommand);
}
}
private void
BookRemoved(object sender, DataEventArgs<BookPresenterViewModel> e)
{
if (e != null
&& e.Value != null)
{
BookPresenterViewModel book = e.Value;
if
(Books.Contains(book))
{
book.Removed
-= BookRemoved;
_commandProxy.RemoveAllBooksCommand.UnregisterCommand(book.RemoveBookCommand);
Books.Remove(book);
}
}
}
#endregion
}
Логика
несложная и не должна нуждаться в особом комментировании. Возможно, вас немного смутит событие Removed, которое мы
используем для указания того, что мы удаляем какую то книгу. Но с EventHandler вы, наверное, уже
сталкивались, а класс DataEventArgs ничего сложного не
представляет.
public class DataEventArgs<TData> : EventArgs
{
public
DataEventArgs(TData value);
public TData
Value { get; }
}
Так
что это обычный класс, наследуемый от EventArgs, с которого можно получить переданные данные. Последним
штрихом нужно немного подправить нашу оболочку Shell, в которой добавить
использование CompositeCommand.
<Window x:Class="PrismCommandSample.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:source="clr-namespace:PrismCommandSample.Helpers"
Title="Command Sample" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"
/>
<RowDefinition Height="Auto"
/>
</Grid.RowDefinitions>
<ListBox Grid.Row="0" ItemsSource="{Binding Books}" SelectedItem="{Binding CurrentBook}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Author}" Width="100"/>
<TextBlock Text="{Binding Price, StringFormat={}{0:C}}"
Width="50" FontWeight="Bold"
/>
<Button Content="Удалить" Command="{Binding RemoveBookCommand}"></Button>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Content="Удалить
всё" Grid.Row="1" Width="120"
Command="{x:Static source:BooksCommands.RemoveAllBooksCommand}"
/>
</Grid>
</Window>
После запуска приложения мы можем посмотреть на то, что у
нас получилось.
Мы вкратце рассмотрели,
как построена работа с командами в Prism 5, и что изменилось, по сравнению с предыдущей версией. Надеюсь,
эта небольшая статья поможет вам преодолеть трудности, которые могут возникнуть при работе с библиотекой Prism.
No comments:
Post a Comment