Паттерн «Команда» в WPF. «Строгий» вариант реализации паттерна MVVM

В первой статье, посвящённой паттерну MVVM, мы рассматривали так называемый «нестрогий» вариант его реализации, когда взаимодействие представления и модели представления осуществляется посредством обработки событий.

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

Теперь мы рассмотрим применение паттерна команда в контексте WPF и усовершенствуем представленную ранее реализацию паттерна MVVM.

Материал этой статьи во многом опирается на первую статью о MVVM и статью о паттерне «Команда». Поэтому если вы ещё только изучаете WPF и паттерны, в частности MVVM, то перед дальнейшим прочтением настоятельно рекомендуется ознакомиться с вышеназванными статьями. Ниже приведены ссылки на них.

Особенности реализации паттерна «Команда» в WPF

WPF накладывает ряд характерных особенностей на работу паттерна «Команда» при работе с пользовательским интерфейсом как в рамках MVVM так и вне его.

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

В частности, класс команды должен реализовывать интерфейс ICommand (пространство имён System.Windows.Input).

Характерным отличием от «классической» реализации является то, что в случае команд в WPF отсутствуют такие элементы паттерна как Invoker (его роль играет сам механизм команд) и, как правило, Receiver (в качестве него может выступать класс окна или модели представления (MVVM).

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

Написание классов команд

Создадим команды для добавления и удаления автомобиля из коллекции.

Как говорилось выше, классы команд должны реализовывать интерфейс ICommand. Рассмотрим его подробнее.

Данный интерфейс содержит всего три члена. Два метода:

  • CanExecute(Object)
    Метод, указывающий, может ли команда выполниться в текущем состоянии. Возвращает значение типа bool;
  • Execute(Object)
    Метод, вызываемый при вызове данной команды. Не возвращает никакого значения (имеет тип void).

Оба метода принимаю единственный параметр типа Object – данные используемые при выполнении команды.

Помимо указанных методов интерфейс содержит событие CanExecuteChanged, которое происходит при возникновении изменений, влияющих на то, должна ли выполняться данная команда.

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

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

Инициализацию экземпляра класса модели представления будем производить в конструкторе.

Ниже представлен код базового класса команды.

На основе этого класса создадим классы команд, которые будут выполняться из интерфейса пользователя.

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

Перенесём весь необходимый функционал в классы команд.

Ниже представлен код команды для добавления автомобиля.

Класс команды удаления автомобиля.

Оба класса реализуют тот же функционал, что ранее реализовали методы AddCar и DeletCar класса модели представления (CarViewModel). Потому эти методы можно будет впоследствии за ненадобностью из него исключить.

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

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

Теперь, когда команды готовы их следует интегрировать в модель представления.

Интеграция с моделью представления

Интеграция команд с моделью представления на самом деле не представляет особой сложности.

Команды объявляются как поля или свойства её класса и инициализируются либо в конструкторе, либо по запросу. В последнем случае используется паттерн «отложенная инициализация».

Заменим методы, отвечающие за добавление и удаление автомобилей на соответствующие команды.

Ниже приведён исходный код класса модели представления после доработки. Команды реализованы как свойства класса доступные только для чтения и инициализируются по запросу. Внесённые изменения снабжены комментариями.

Представление при «строгой» реализации паттерна MVVM

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

В XAML коде также заменяем обработчики событий на команды.

Ниже представлен код XAML разметки после внесения изменений.

Расположение кнопок в разметке в первой и нынешней статьях о MVVM наглядно иллюстрирует одно из ключевых различий между обработкой событий и механизмом команд.

Обработка событий не зависит от свойства DataContext элемента разметки, а команды связываются со свойствами класса, который в нём задан.

В элементе StackPanel, в котором ранее располагались кнопки, в качестве DataContext установлено свойство SelectedCar модели представления, которое в свою очередь является объектом класса Car. Последний не имеет свойств для доступа к командам (в данном примере Add и Delete), так как играет роль модели.

Поэтому, после перехода от обработки событий к командам мы были вынуждены вынести обе кнопки за пределы вышеупомянутого элемента StackPanel.

Данную особенность следует обязательно учитывать при проектировании и реализации.

Сравнение «строгой» и «нестрогой реализации паттерна MVVM

«Нестрогий» вариант реализации паттерна MVVM имеет более простую архитектуру. Он более неприхотлив к структуре пользовательского интерфейса (разметки XAML), так как использует обработку событий и его можно реализовать за более короткий промежуток времени.

«Строгий» вариант при всей своей сложности и трудоёмкости более гибок.

Механизм команд устраняет жесткую связь между моделью представления и самим представлением, а также предоставляет возможность изменения логики взаимодействия между этими двумя элементами паттерна независимо от них.

Всё это положительно влияет на расширяемость и масштабируемость проекта, а также в целом упрощает дальнейшее сопровождение, так как трудоёмкость данного варианта реализации проявляется только при разработке «с нуля» или глубокой модернизации (весь функционал распределён между несколькими отдельными компонентами).

Поэтому, по возможности лучше использовать всё-таки «строгий» вариант реализации паттерна MVVM.

Один комментарий

  1. Отличная статья, спасибо. Только она и помогла с командами разобраться. Автор, продолжайте в том же духе!

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *