Модели команд

95

Модель команд WPF состоит из удивительного количества подвижных частей. Ключевыми в ней являются четыре следующих компонента:

Команды

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

Привязки команд

Каждая такая привязка подразумевает соединение команды с имеющей к ней отношение прикладной логикой, отвечающей за обслуживание определенной области пользовательского интерфейса. Такое распределенное проектное решение очень важно, потому что одна и та же команда может использоваться в нескольких местах в приложении и иметь в каждом из них разное предназначение. Для целей обработки применяется одна и та же команда, но с разными привязками.

Источники команд

Источник команды приводит команду в действие. Например, источником команды может служить как элемент управления MenuItem, так и элемент управления Button. Щелчок на любом из них будет приводить к выполнению привязанной к ним команды.

Целевые объекты команд

Целевым объектом команды называется элемент, в отношении которого команда должна выполняться. Например, команда Paste может вставлять текст в элемент управления TextBox, а команда OpenFile — отображать документ в элементе управления DocumentViewer. Целевой объект может быть важен, а может быть и неважен, что зависит от природы команды.

Интерфейс ICommand

В основе модели команд WPF лежит интерфейс System.Windows.Input.ICommand, определяющий способ, в соответствии с которым работают команды. Этот интерфейс включает в себя два метода и событие:

public interface ICommand
{
   void Execute(object parameter);
   bool CanExecute(object parameter);
   event EventHandler CanExecuteChanged;
}

В простой реализации в методе Execute() должна содержаться прикладная логика, решающая задачу (например, печать документа). Однако, как будет показано, WPF является немного более совершенной технологией. Она использует метод Execute() для запуска более сложного процесса, который, в конечном счете, заканчивается возбуждением события, обрабатываемого в совершенно другом месте в приложении. Это дает разработчику возможность использовать готовые классы команд и включать в них собственную логику, а также обеспечивает гибкость применения одной команды (например, Print) в нескольких различных местах.

Метод CanExecute() возвращает информацию о состоянии команды, а именно — значение true, если она включена, и false, если она отключена. Методы Execute() и CanExecute() принимают дополнительный объект-параметр, который можно применять для передачи с ними любой другой необходимой информации.

И, наконец, событие CanExecuteChanged вызывается при изменении состояния. Для любых использующих данную команду элементов управления оно является сигналом о том, что им следует вызвать метод CanExecute() и проверить состояние команды. Это часть связующего звена, которое позволяет источникам команд (вроде элемента управления Button или элемента управления MenuItem) автоматически включать себя, когда команда доступна, и отключать, когда она не доступна.

Класс RoutedCommand

При создании собственных команд реализовать интерфейс ICommand напрямую не обязательно. Вместо этого можно воспользоваться классом System.Windows.Input.RoutedCommand, который реализует этот интерфейс автоматически. Класс RoutedCommand является единственным классом в WPF, который реализует ICommand. Другими словами, все команды WPF представляют собой экземпляры класса RoutedCommand (или производного от него класса).

Одна из ключевых концепций, лежащих в основе модели команд в WPF, состоит в том, что класс RoutedCommand не содержит никакой прикладной логики. Он просто представляет команду. Это означает, что один объект RoutedCommand обладает теми же возможностями, что и другой.

Класс RoutedCommand добавляет дополнительную инфраструктуру для туннелирования и пузырькового распространения событий. Если интерфейс ICommand инкапсулирует идею команды — действие, которое может инициироваться и быть или не быть доступным, то класс RoutedCommand изменяет команду так, чтобы она могла подобно пузырьку подниматься вверх по иерархии элементов WPF до подходящего обработчика событий.

Впервые взглянув на модель команд в WPF, трудно сразу же понять точно, почему команды WPF требуют механизма маршрутизируемых событий. В конце концов, разве не должен объект команды заботиться о ее выполнении независимо от того, как она вызывается?

В случае непосредственного использования интерфейса ICommand для создания собственных команд это было бы так. Код понадобилось бы жестко связывать с командой, и он работал бы одинаково независимо от того, что конкретно активизировало команду. При этом в пузырьковом распространении событий не было бы необходимости.

Однако в WPF используется набор заготовленных команд. Классы этих команд не содержат никакого реального кода. Они являются просто удобно определенными объектами, которые представляют общую задачу приложения (например, печать документа). Для выполнения действий над этими командами необходимо использовать привязку, вызывающую в коде соответствующее событие. Чтобы обеспечить обработку данного события в одном месте, даже если оно возбуждается разными источниками команд в одном и том же окне, как раз и необходима мощь пузырькового распространения событий.

Из этого вытекает интересный вопрос: зачем вообще использовать заготовленные команды? Не лучше ли заставить делать всю работу специальные классы команд и не полагаться на обработчики событий? Во многом такое проектное решение было бы проще. Однако преимущество заготовленных команд состоит в том, что они предлагают гораздо более удобные возможности для интеграции. Например, предположим, что некий сторонний разработчик создал элемент управления Documentview, использующий заготовленную команду Print. Если в приложении применяется такая же заготовленная команда, не придется прилагать никаких дополнительных усилий для включения в него возможности печати. С этой точки зрения команды являются важной частью подключаемой архитектуры WPF.

Для поддержки маршрутизируемых событий класс RoutedCommand реализует интерфейс ICommand как приватный и затем добавляет немного отличающиеся версии его методов. Наиболее заметным изменением является то, что методы Execute() и CanExecute() теперь принимают дополнительный параметр. Ниже показаны новые сигнатуры этих методов:

public void Execute(object parameter, IInputElement target)
{ ... }
public bool CanExecute(object parameter, IInputElement target)
{ ... }

target представляет собой целевой элемент, в котором начинается обработка события. Это событие начинает обрабатываться в целевом элементе и затем поднимается вверх до находящихся на более высоком уровне контейнеров до тех пор, пока приложение не использует его для выполнения подходящей задачи. (Для обработки события Executed элементу необходима помощь еще одного класса, а именно — CommandBinding.)

Помимо этого в классе RoutedCommand теперь еще предлагаются три новых свойства: Name (имя команды), OwnerType (класс, членом которого является команда) и InputGestures (коллекция любых нажатий клавиш, клавиатурных комбинаций или действий мыши, которые также могут использоваться для вызова команды).

Класс RoutedUICommand

Большинство команд, с которыми придется иметь дело, будут представлять собой не объекты RoutedCommand, а экземпляры класса RoutedUICommand, унаследованного от RoutedCommand. (В действительности все заготовленные команды, которые предлагаются в WPF, являются объектами RoutedUICommand.)

Класс RoutedUICommand предназначен для команд с текстом, который должен отображаться где-нибудь в пользовательском интерфейсе (например, текстом для элемента меню или текстом подсказки для кнопки в панели инструментов). Класс RoutedUICommand добавляет одно свойство Text, в котором указывается текст, отображаемый вместе с данной командой.

Преимущество подхода с определением текста вместе с командой (а не непосредственно в элементе управления) состоит в том, что это позволяет производить локализацию в одном месте. Если же текст команды никогда не отображается где-либо в пользовательском интерфейсе, то вполне подойдет и класс RoutedCommand.

Отображать в пользовательском интерфейсе текст RoutedUICommand вовсе не обязательно. Часто могут быть веские причины для использования какого-то другого текста. Например, вместо текста "Print Document" (Печать документа) может применяться просто "Print" (Печать), а в некоторых случаях текст вообще заменяется небольшим графическим изображением.

Пройди тесты
Лучший чат для C# программистов