В 2 предыдущих частях мы научились создавать окно приложения (View) на языке XAML, а также создали для него класс ViewModel и запустили приложение. В принципе, этого достаточно — приложение из 2 части цикла про WPF полноценно работает. Однако, в нём есть довольно большой минус — мы использовали обработку события Click для запуска команды, а не механизм привязок. Такая реализация не позволяет полноценно отделить окно от бизнес-логики приложения, и несёт некоторые другие минусы, о которых я скажу позже.
Сегодня мы реализуем в нашем приложении интерфейс INotifyPropertyChanged и создадим класс RelayCommand для записи команд в него.
Статья получилась довольно сложная, но я настоятельно советую ознакомиться с ней, если вы планируете писать WPF-приложения.
Класс RelayCommand
Сначала напишем вспомогательный класс для хранения команд. Это будет реализация интерфейса ICommand, хранящая в себе методы Execute и CanExecute, и событие CanExecuteChanged. Что это нам даёт:
- Метод Execute хранит реализацию команды.
- С помощью метода CanExecute мы можем управлять доступностью команды (например, не позволять пользователю запускать плагин, если в поле для числа введён текст).
- А событие 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 — мы можем запускать программу, если пользователь выбрал параметр из списка, и в поле для стартового значение указано целое число:
Далее модифицируем конструктор ViewModel, чтобы у нас появлялась команда
Метод Numerate, напомню, был написан в Части 2.
Отлично, мы модифицировали ViewModel. Осталось внести правки во View — передать ссылку на команду, и добавить действий на события CloseRequest, HideRequest и ShowRequest.
Во View изменение небольшое: вместо обработки Click добавляем команду и привязку к ней.
А в Command записываем следующее:
Я очень рекомендую вызывать метод ShowDialog, а не Show. В чём причина: ShowDialog делает модальное окно (блокирует Ревит), и код возвращается к выполнению только после его закрытия. Show же делает немодальное окно и код продолжает выполняться: внешняя команда возвращает Result.Succeded на строке 29. А потом мы жмём "Запуск", и пытаемся изменить элемент вне метода Execute, и получаем фатальную ошибку Ревита.
Это не означает, что всегда надо делать модальные окна, можно сделать и немодальное окно, но там будет другая техника выполнения (то есть ещё на несколько статей).
Результат
Честно говоря, результат относительно части 2 не особо изменился: плагин делает всё абсолютно тоже самое, но теперь более правильно. Но теперь появился важный нюанс: кнопка Запуск неактивна, если не выбран параметр или в поле для стартового значения записано не целое число:
А на этом всё. Итоговый код, как обычно, в моём репозитории на GitHub.
Не забывайте подписываться на мой телеграм-канал. До новых встреч в следующих статьях!