Отключение команд

57

Преимущества модели команд по-настоящему проявляются при создании команды, способной менять свое состояние с активного на неактивное и наоборот. Например, рассмотрим приложение с одним окном, показанное на рисунке. Оно представляет собой простой текстовый редактор, состоящий из меню, панели инструментов и большого текстового поля (TextBox), который позволяет открывать файлы, создавать новые (пустые) документы и сохранять выполненную работу:

Простой текстовый редактор

В данном случае вполне логично сделать команды New (Создать), Open (Открыть), Save (Сохранить), SaveAs (Сохранить как) и Close (Закрыть) доступными всегда. Однако другое проектное решение может требовать, чтобы команда Save становилась доступной только в случае изменения текста, когда он отличается от первоначального. По соглашению такую деталь можно отслеживать в коде с помощью простого булевского значения isDirty. Этот флаг будет устанавливаться при любом изменении текста:

private bool isDirty = false;
private void txt_TextChanged(object sender, RoutedEventArgs e)
{
    isDirty = true;
}

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

binding = new CommandBinding(ApplicationCommands.Save);
binding.Executed += SaveCommand_Executed;
binding.CanExecute += SaveCommand_CanExecute;
this.CommandBindings.Add(binding);

либо декларативно:

<Window.CommandBindings>
        <CommandBinding Command="Save" Executed="SaveCommand_Executed"
                        CanExecute="SaveCommand_CanExecute"></CommandBinding>
</Window.CommandBindings>

В этом обработчике событий должна просто осуществляться проверка значения переменной isDirty и устанавливаться соответствующее значение для свойства CanExecuteRoutedEventArg.CanExecute:

private void SaveCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{            
     e.CanExecute = isDirty;
}

Если свойство isDirty равно false, команда будет отключаться, а если true — включаться. (Если флаг CanExecute не установлен, сохраняется самое последнее значение.)

При использовании CanExecute важно помнить о следующем. Платформа WPF самостоятельно решает, когда вызывать метод RoutedCommand.CanExecute() для запуска обработчика событий и определения состояния команды. Диспетчер команд WPF делает это только тогда, когда обнаруживает изменение, которое считает значительным — например, в случае перемещения фокуса с одного элемента управления на другой или после выполнения команды. Элементы управления могут также инициировать событие CanExecuteChanged, указывающее WPF на необходимость оценки состояния команды заново — например, такое происходит при нажатии клавиши в текстовом поле. В целом событие CanExecute будет возбуждаться довольно часто, поэтому не следует использовать внутри него длительно выполняющийся код.

Однако на состояние команды могут влиять и другие факторы. Так, в текущем примере флаг isDirty мог бы изменяться и в ответ на другое действие. Если обнаруживается, что состояние команды не обновляется в нужный момент, WPF можно принудить вызывать метод CanExecute() на всех используемых командах. Это делается вызовом статического метода CommandManager.InvalidateRequerySuggested(). После этого диспетчер команд будет генерировать событие RequerySuggested для уведомления всех присутствующих в окне источников команд (кнопок, элементов меню и т.д.). Эти источники команд затем повторно опросят связанные с ними команды и соответствующим образом обновят свое состояние.

Ограничения команд WPF

Команды WPF способны изменять только один аспект состояния связанного с ними элемента, а именно — значение его свойства IsEnabled. Совсем не трудно представить ситуации, в которых требуется более сложное поведение. Например, может понадобиться создать команду PageLayoutView, которую можно было бы включать и отключать. При ее включении связанные с ней элементы управления должны соответствующим образом настраиваться. (Например, связанный элемент меню должен отмечаться флажком, а связанная кнопка в панели инструментов — подсвечиваться, как это происходит с элементом CheckBox в случае его добавления в ToolBar.) К сожалению, возможности отслеживать "отмеченное" состояние команды не существует. Это означает, что обрабатывать событие для этого элемента управления и обновлять его состояние и состояние других связанных элементов необходимо вручную.

Простого решения упомянутой проблемы не существует. Даже если создать специальный класс, унаследованный от RoutedUICommand, и наделить его функциональностью отслеживания состояния "отмечен/не отмечен" (с возбуждением соответствующего события в случае его изменения), все равно понадобится заменить часть имеющей к нему отношение инфраструктуры. Например, нужно будет создать специальный класс CommandBinding, способный перехватывать уведомления от специальной команды, реагировать при изменении ее состояния "отмечен/не отмечен" и затем обновлять соответствующим образом все связанные с ней элементы управления.

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

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