Найти в Дзене

Создание WPF-приложения для Revit. Часть 3. Добавление команд

Оглавление

Часть 1.

Часть 2.

В 2 предыдущих частях мы научились создавать окно приложения (View) на языке XAML, а также создали для него класс ViewModel и запустили приложение. В принципе, этого достаточно — приложение из 2 части цикла про WPF полноценно работает. Однако, в нём есть довольно большой минус — мы использовали обработку события Click для запуска команды, а не механизм привязок. Такая реализация не позволяет полноценно отделить окно от бизнес-логики приложения, и несёт некоторые другие минусы, о которых я скажу позже.

Сегодня мы реализуем в нашем приложении интерфейс INotifyPropertyChanged и создадим класс RelayCommand для записи команд в него.

Статья получилась довольно сложная, но я настоятельно советую ознакомиться с ней, если вы планируете писать WPF-приложения.

Класс RelayCommand

Сначала напишем вспомогательный класс для хранения команд. Это будет реализация интерфейса ICommand, хранящая в себе методы Execute и CanExecute, и событие CanExecuteChanged. Что это нам даёт:

  1. Метод Execute хранит реализацию команды.
  2. С помощью метода CanExecute мы можем управлять доступностью команды (например, не позволять пользователю запускать плагин, если в поле для числа введён текст).
  3. А событие CanExecuteChanged информирует View о том, что статус CanExecute мог измениться.

Как выглядит код класса: (в конце статьи будет ссылка на весь код на GitHub)

public class RelayCommand : ICommand
{
private readonly Action<object> execute;
private readonly Func<object, bool> canExecute;
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public RelayCommand(Action<object> Execute, Func<object, bool> CanExecute)
{
execute = Execute ?? throw new ArgumentNullException(nameof(Execute));
canExecute = CanExecute;
}
public bool CanExecute(object parameter)
{
return canExecute?.Invoke(parameter) ?? true;
}

public void Execute(object parameter)
{
execute(parameter);
}
}

Подробнее об этом можно почитать здесь. Вообще, я один раз реализовал этот класс, а дальше он у меня кочует из проекта в проект, поэтому, например, его сложно воспроизвести с нуля на память. Можете тоже воспользоваться этим кодом как шпаргалкой.

Запись команд во ViewModel и интерфейс INotifyPropertyChanged

Далее модифицируем нашу ViewModel. Нам нужно добавить свойство для хранения RelayCommand, написать метод CanExecute (Execute уже написан во второй части), а так же реализовать INotifyPropertyChanged.

Что ещё за INotifyPropertyChanged?

Если кратко, это интерфейс, который информирует ViewModel об изменении значения свойств связанного с ней View. Благодаря этому, в данном случае, мы будем иметь возможность пользоваться методом CanExecute, а также будем уверены в том, что все обновленные в окне значения свойств обновятся и во ViewModel.

Как реализуем интерфейс? Добавим в название класса наследование:

public class ViewModel : INotifyPropertyChanged

А в конце добавим следующий код:

В данном случае непосредственно реализацией являются только PropertyChanged и OnPropertyChanged. Но я обычно всегда добавляю и предыдущие события, чтобы иметь возможность из ViewModel управлять видимостью окна.

Атрибут CallerMemberName в 132 строке позволяет нам вызывать OnPropertyChanged и не задумываться о том, что мы имя свойства, которое мы передаём, поменяется и получится ошибка. Компилятор подставит его автоматически.

Теперь модифицируем наш написанный код. Заменим свойства на комбинацию приватное поле+свойство. Смысл в чём: при чтении свойства мы получаем значение приватного поля, а при записи свойства обновляем это значение и вызываем метод OnPropertyChanged:

Добавили приватные поля с подчёркивания и маленькой буквы
Добавили приватные поля с подчёркивания и маленькой буквы
А вот так теперь выглядят свойства
А вот так теперь выглядят свойства

Не забудьте оформить в таком же стиле свойство SelectedParameter.

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

-4

Далее модифицируем конструктор ViewModel, чтобы у нас появлялась команда

-5

Метод Numerate, напомню, был написан в Части 2.

Отлично, мы модифицировали ViewModel. Осталось внести правки во View — передать ссылку на команду, и добавить действий на события CloseRequest, HideRequest и ShowRequest.

-6

Во View изменение небольшое: вместо обработки Click добавляем команду и привязку к ней.

А в Command записываем следующее:

-7

Я очень рекомендую вызывать метод ShowDialog, а не Show. В чём причина: ShowDialog делает модальное окно (блокирует Ревит), и код возвращается к выполнению только после его закрытия. Show же делает немодальное окно и код продолжает выполняться: внешняя команда возвращает Result.Succeded на строке 29. А потом мы жмём "Запуск", и пытаемся изменить элемент вне метода Execute, и получаем фатальную ошибку Ревита.

Это не означает, что всегда надо делать модальные окна, можно сделать и немодальное окно, но там будет другая техника выполнения (то есть ещё на несколько статей).

Результат

Честно говоря, результат относительно части 2 не особо изменился: плагин делает всё абсолютно тоже самое, но теперь более правильно. Но теперь появился важный нюанс: кнопка Запуск неактивна, если не выбран параметр или в поле для стартового значения записано не целое число:

Команда недоступна
Команда недоступна
Команда доступна
Команда доступна

А на этом всё. Итоговый код, как обычно, в моём репозитории на GitHub.
Не забывайте подписываться на
мой телеграм-канал. До новых встреч в следующих статьях!

-10