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

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

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

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

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

  • Реализация паттерна MVVM на примере C# (WPF). «Нестрогий» вариант;
  • Паттерн «Команда» (Command). Описание и примеры использования.
Особенности реализации паттерна «Команда» в WPF

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

abstract class MyCommand : ICommand
{
    protected CarViewModel _cvm;
    public MyCommand(CarViewModel cvm)
    {
        _cvm = cvm;
    }
    public event EventHandler CanExecuteChanged;
    public abstract bool CanExecute(object parameter);
    public abstract void Execute(object parameter);
}

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

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

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

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

class AddCommand : MyCommand
{
    public AddCommand(CarViewModel cvm) : base(cvm)
    {
    }
    public override bool CanExecute(object parameter)
    {
        return true;
    }
    public override void Execute(object parameter)
    {
        Car car = new Car();
        _cvm.Cars.Insert(0, car);
        _cvm.SelectedCar = car;
    }
}

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

class DeleteCommand : MyCommand
{
    public DeleteCommand(CarViewModel cvm) : base(cvm)
    {
    }
    public override bool CanExecute(object parameter)
    {
        return true;
    }
    public override void Execute(object parameter)
    {
        _cvm.Cars.Remove(_cvm.SelectedCar);
    }
}

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

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

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

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

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

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

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

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

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

class CarViewModel : INotifyPropertyChanged
{
    private Car _selectedCar;
    public ObservableCollection<Car> Cars { get; set; }
    // Закрытые поля команд
    private ICommand _addCommand;
    private ICommand _deleteCommand;
   public Car SelectedCar
    {
        get { return _selectedCar; }
        set
        {
            _selectedCar = value;
            OnPropertyChanged("SelectedCar");
        }
    }
    public CarViewModel()
    {
        Cars = new ObservableCollection<Car>
        {
            new Car { Model="ВАЗ-2105", MaxSpeed=150, Price=56000 },
            new Car { Model="LADA Priora", MaxSpeed=170, Price=560000 },
            new Car { Model="КамАЗ", MaxSpeed=100, Price=5600000 }
        };
    }
    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged([CallerMemberName]string prop = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
    }
    // Свойства доступные только для чтения для обращения к командам и их инициализации
    public ICommand Add {
        get
        {
            if (_addCommand == null)
            {
                _addCommand = new AddCommand(this);
            }
            return _addCommand;
        }
    }
    public ICommand Delete
    {
        get
        {
            if(_deleteCommand==null)
            {
                _deleteCommand = new DeleteCommand(this);
            }
                return _deleteCommand;
        }
    }
}

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

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

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

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

<Window x:Class="MVVM_Example.MainWindow"
        xmlns=http://schemas.microsoft.com/winfx/2006/xaml/presentation
        xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml
        xmlns:d=http://schemas.microsoft.com/expression/blend/2008
        xmlns:mc=http://schemas.openxmlformats.org/markup-compatibility/2006
        xmlns:local="clr-namespace:MVVM_Example"
        mc:Ignorable="d"
        Title="MVVM_Example" Height="403.921" Width="310.294">
    <Grid Margin="0,81,0,0">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Button  Command="{Binding Add}" Margin="0,-78,0,341">+</Button>
        <Button Command="{Binding Delete}" Margin="0,-44,0,308" >-</Button>
        <StackPanel Grid.Column="0" DataContext="{Binding SelectedCar}" Grid.ColumnSpan="2">
            <TextBlock Text="Выбранный элемент" Margin="0,0,-233,0"  />
            <TextBlock Text="Модель" />
            <TextBox Text="{Binding Model, UpdateSourceTrigger=PropertyChanged}" />
            <TextBlock Text="Максимальная скрорость, км/ч" />
            <TextBox Text="{Binding MaxSpeed, UpdateSourceTrigger=PropertyChanged}" />
            <TextBlock Text="Цена, руб." />
            <TextBox Text="{Binding Price, UpdateSourceTrigger=PropertyChanged}" />
        </StackPanel>
        <ListBox Grid.Column="0" Grid.Row="0" ItemsSource="{Binding Cars}"
                 SelectedItem="{Binding SelectedCar}" Grid.ColumnSpan="2" Margin="0,170,0,0">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Margin="5">
                        <TextBlock FontSize="18" Text="{Binding Path=Model}" />
                        <TextBlock Text="{Binding Path=MaxSpeed}" />
                        <TextBlock Text="{Binding Path=Price}" />
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Window>

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

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

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

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

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

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

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

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

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

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

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

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

  2. Не пойму откуда вызываются методы CanExecute и Execute

  3. Я слышал про фреймворки MVVMCross, Prism, MugenMvvmToolkit. Есть ли опыт использования чего-то подобного. Если есть можете написать статью для чего они нужны и как ими пользоваться. Спасибо

  4. Это методы интерфейса ICommand. Когда нажимается та или иная кнопка, эти методы соответствующих классов команд вызываются «под капотом».

  5. Любой фреймворк, что MVC, что MVVM и т.д. призван избавить разработчика от необходимости написания однотипного (или по другому «шаблонного») кода, в частности при использовании архитектурных паттернов. Это снижает трудоёмкость процесса разработки и позволяет сократить сроки без потери качества. В то же время, при использовании того или иного фреймворка, Ваш проект становится зависим от него. И, внедрив фреймворк, Вы получаете не только его возможности и преимущества, но также его ограничения и недостатки. По фреймворкам, которые Вы привели я ничего конкретно сказать не могу, т.к. с ними, к сожалению, пока не сталкивался. Сейчас работаю в основном в других направлениях. А, вообще за идею для статей спасибо! Когда будет возможность уделять десктопным технологиям больше времени, возможно, подобные статьи появятся.

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

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