Специальные команды

98

Какими бы полными не были пять стандартных классов команд (ApplicationCommands, NavigationCommands, EditingCommands, ComponentCommands и MediaCommands), они, очевидно, не могут предоставить абсолютно все, что нужно приложению. К счастью, в WPF довольно легко определять собственные специальные команды. Все, что для этого понадобится — это создать новый объект RoutedUICommand.

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

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

public class MyCommands
{
        // Создание команды requery
        private static RoutedUICommand requery;

        static MyCommands()
        {
            // Инициализация команды
            InputGestureCollection inputs = new InputGestureCollection();
            inputs.Add(new KeyGesture(Key.R, ModifierKeys.Control, "Ctrl + R"));
            requery = new RoutedUICommand("Requery", "Requery", typeof(MyCommands), inputs);
        }

        public static RoutedUICommand Requery
        {
            get { return requery; }
        }
}

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

После определения команду можно использовать в своих привязках команд точно так же, как любую из готовых команд, предлагаемых WPF. Тем не менее, есть одна особенность. Чтобы применять свою команду в XAML, сначала необходимо отобразить свое пространство имен .NET на пространство имен XML. Например, если класс находится в пространстве имен Commands, потребуется добавить следующую строку:

xmlns:local="clr-namespace:Commands"

В данном примере в качестве псевдонима для пространства имен был выбран local. Однако в принципе псевдонимом может быть любое слово, соответствующее стилю файла XAML. Теперь к команде можно получать доступ через пространство имен local.

Ниже показан полный код примера простого окна с кнопкой, запускающей команду Requery:

<Window x:Class="Commands.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Commands"
        Title="MyCommand" Height="200" Width="300">
    <Window.CommandBindings>
        <CommandBinding Command="local:MyCommands.Requery"
                        Executed="CommandBinding_Executed"></CommandBinding>
    </Window.CommandBindings>
    <Grid Margin="5">
        <Button Command="local:MyCommands.Requery">Requery</Button>
    </Grid>
</Window>

Использование одной и той же команды в разных местах

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

До сих пор еще не было показано, как добиться подобного поведения для создаваемых самостоятельно команд. Для примера предположим, что создается окно с пространством под два документа:

Текстовый редактор с возможностью обработки двух документов

Команды Cut, Сору и Paste, как обнаружится, будут автоматически работать с правильным текстовым полем. Однако команды New, Open и Save, реализованные самостоятельно, этого делать не будут. Проблема здесь в том, что при срабатывании события Executed для одной из этих команд совершенно не понятно, к какому из текстовых полей оно относится — к первому или второму. Хотя объект ExecutedRoutedEventArgs поддерживает свойство Source, оно отражает лишь элемент, который имеет привязку к команде (подобно ссылке на отправителя). А пока что все привязки команд присоединены к содержащему окну.

Решить эту проблему можно, привязав команду в каждом текстовом поле по-разному с помощью коллекции CommandBindings.

Однако эта реализация имеет два недостатка. Первый состоит в том, что простой флаг isDirty теперь больше использовать нельзя, т.к. требуется следить не за одним, а за двумя текстовыми полями. Для этой проблемы существует несколько решений.

Во-первых, можно воспользоваться свойством TextBox.Tag для сохранения флага isDirty. Тогда при каждом вызове метода CanExecuteSave() достаточно будет просто заглядывать в свойство Tag отправителя. Во-вторых, для сохранения значения isDirty можно создать приватную словарную коллекцию с индексацией по ссылке на элемент управления. Тогда при вызове метода CanExecuteSave() достаточно будет просто найти в этой коллекции значение isDirty, которое принадлежит отправителю. Ниже показан код, который должен использоваться в таком случае:

private Dictionary<Object, bool> isDirty = new Dictionary<Object, bool>();
        private void txt_TextChanged(object sender, RoutedEventArgs e)
        {
            isDirty[sender] = true;
        }

        private void SaveCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            if (isDirty.ContainsKey(sender) && isDirty[sender] == true)
            {
                e.CanExecute = true;
            }
            else
            {
                e.CanExecute = false;
            }             
        }

Вторым недостатком текущей реализации является то, что она предусматривает создание двух привязок команд, а на самом деле требуется только одна. Это привносит беспорядок в файл XAML и усложняет его сопровождение. Проблема еще больше усугубляется при наличии большого количества таких команд, которые должны использоваться в обоих текстовых полях.

Решение состоит в создании единой командной привязки и ее добавление в коллекцию CommandBindings обоих элементов TextBox. Реализовать это в коде не составит труда. Однако чтобы сделать это в XAML-разметке, потребуется использовать ресурсы WPF.

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