Команды и взаимодействие с пользователем в MVVM

Последнее обновление: 29.01.2023

В прошлой теме описывался паттерн MVVM, позволяющий отображать связанные данные. Однако, как правило, необходимо не только отображать данные, но и как-то взаимодействовать с пользователем, обрабатывать пользовательский ввод. Рассмотрим, как это сделать в рамках паттерна MVVM в Windows Forms.

Одним из базовых моментов паттерна MVVM является взаимодействие с моделью через ViewModel, то есть в данном случае использование событий визуальных компонентов и их обработчиков нежелательно. Чтобы решить эту задачу, в платформе .NET MAUI имеется механизм команд, которые представляют реализацию интерфейса System.Windows.Input.ICommand:

public interface ICommand 
{ 
	void Execute(object? arg); 
	bool CanExecute(object? arg); 
	event EventHandler? CanExecuteChanged; 
}

Метод Execute() выполняет команду.

Метод CanExecute возвращает true, если команда может быть выполнена.

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

Стоит отметить, что несмотря на то, что интерфейс ICommand определен в пространстве имен System.Windows.Input, которое нацелено на другую технологию для построения графического интерфейса - WPF, но интерфейс ICommand не имеет никаких зависимостей от WPF или какой-либо конкретной технологии.

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

ViewModel может определять свойства типа ICommand. Затем подобные свойства можно привяать к свойству Command кнопки или другого визуального компонента, который поддерживает комманды. Например, когда пользователь нажимает кнопку, внутри Button вызывается метод Execute соответствующей команды. И мы можем либо использовать обработку события нажатия кнопки, либо привязать команду и также выполнять некоторые действия.

Определим свой класс команды, который назовем MainCommand:

using System.Windows.Input;

public class MainCommand : ICommand
{
    public event EventHandler? CanExecuteChanged;
    Action<object?> action;
    public MainCommand(Action<object?> action)
    {
        this.action = action;
    }
    public bool CanExecute(object? parameter) => true;

    public void Execute(object? parameter) => action?.Invoke(parameter);
}

Команда MainCommand через конструктор будет принимать некоторое действие, которое принимает один параметр типа object?. Это действие присваивается переменной action. В методе Execute(), который будет вызываться при вызове команды, выполняем действие action, передавая в него параметр parameter.

Метод CanExecute пока нам не важен, поэтому просто возвращаем true, то есть команда будет доступна всегда.

Рассмотрим простейшее применение команд. Допустим, данные у нас представлены классом Person:

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public int Age { get; set; }
    public override string ToString() => Name;
}

Также определим класс MainViewModel:

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace HelloApp;

public class MainViewModel : INotifyPropertyChanged
{
    static int id = 0; // для генерации идентификаторов

    // данные для нового объекта
    string name ="";
    int age;

    // команда для добавления
    public ICommand AddCommand { get; set; }
    // данные для отображения в списке
    public BindingList<Person> People { get; }
    public string Name
    {
        get => name;
        set
        {
            if (name != value)
            {
                name = value;
                OnPropertyChanged();
            }
        }
    }
    public int Age
    {
        get => age;
        set
        {
            if (age != value)
            {
                age = value;
                OnPropertyChanged();
            }
        }
    }
    public MainViewModel()
    {
        People = new()
        {
            
            new Person {Id=++id, Name="Tom", Age=38 },
            new Person {Id=++id, Name ="Bob", Age = 42},
            new Person {Id=++id, Name = "Sam", Age = 25}
        };
        // определяем команду
        AddCommand = new MainCommand(_ =>
        {
            People.Add(new Person { Id = ++id, Name = this.Name, Age = this.Age });
            Name = ""; Age = 0;
        });
    }

    public event PropertyChangedEventHandler? PropertyChanged;
    public void OnPropertyChanged([CallerMemberName] string prop = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
    }
}

Класс MainViewModel хранит поля name и age и надстройки над ними - свойства Name и Age, через которые пользователь будет ввводить новые данные. Также класс определяет свойство People - коллекцию BindingList, в которую будут добавляться данные.

Для добавления данных определено свойство-команда AddCommand, которая устанавливается в конструкторе

AddCommand = new MainCommand(_ =>
{
    People.Add(new Person { Id = ++id, Name = this.Name, Age = this.Age });
    Name = ""; Age = 0;
});

Для установки команды в конструктор класса Command передается делегат Action, который представляет выполняемое командой действие. В данном случае берем значения свойств Name и Age (которые представляют введенные пользователем данные) и с помощью статической переменной генерируем значение для свойства Id, создаем по ним объект Person и добавляем его в коллекцию People. Таким образом, будет выполняться добавление данных. После добавления сбрасываем значения свойств Name и Age.

В классе формы выполним привязку к этой модели представления:

namespace MetanitApp;

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        // список для отображения данных
        ListBox peopleListBox = new ListBox();
        peopleListBox.Dock = DockStyle.Left;
        peopleListBox.SelectionMode = SelectionMode.One;
        Controls.Add(peopleListBox);

        // форма для ввода данных пользователя
        Panel personForm = new Panel{ Padding = new Padding(10), Width=260 };
        personForm.Dock = DockStyle.Right;
        // текстовое поле для ввода имени
        TextBox nameBox = new TextBox();
        nameBox.Location = new Point(12, 10);
        nameBox.Size = new Size(230, 27);
        personForm.Controls.Add(nameBox);
        // числовое поле для ввода возраста
        NumericUpDown ageBox = new NumericUpDown { Minimum=0, Maximum=100 };
        ageBox.Location = new Point(12, 50);
        ageBox.Size = new Size(230, 27);
        personForm.Controls.Add(ageBox);
        // кнопка для ввода данных
        Button addButton = new Button { Text = "Save", AutoSize = true };
        addButton.Location = new Point(12, 80);
        personForm.Controls.Add(addButton);
        
        Controls.Add(personForm);


        DataContext = new MainViewModel();
        // устанавливаем привязку списка ListBox к коллекции People
        peopleListBox.DataBindings.Add(new Binding("DataSource", DataContext, "People"));
        peopleListBox.DisplayMember = "Name";
        peopleListBox.ValueMember = "Id";
        // устанавливаем привязку полей TextBox и NumericUpDown к свойствам Name и Age
        nameBox.DataBindings.Add(new Binding("Text", DataContext, "Name", true, DataSourceUpdateMode.OnPropertyChanged));
        ageBox.DataBindings.Add(new Binding("Value", DataContext, "Age", true, DataSourceUpdateMode.OnPropertyChanged));
        // устанавливаем привязку свойства Command у кнопки к команде AddCommand
        addButton.DataBindings.Add(new Binding("Command", DataContext, "AddCommand", true));
    }
}

Вначале страницы определен компонент ListBox для вывода списка People. Далее на элементе Panel определены два поля ввода - TextBox и NumericUpDown о два текстовых поля, которые привязаны к свойствам Name и Age.

Кроме того, свойство Command кнопки addButton привязано к команде AddButton:

addButton.DataBindings.Add(new Binding("Command", DataContext, "AddCommand", true));

Таким образом, по нажатию на кнопку будет срабатывать команда AddCommand, в которой введенные в текстовые поля данные будут добавлены в список People в MainViewModel.

Результат работы приложения:

Команды и ICommand в реализации паттерна MVVM в приложении в Windows Forms и C#

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

Единственно что стоит заметить, что в Windows Forms не так много визуальных компонентов типа кнопки, которые имеют свойство Command и позволяют выполнять привязку к командам.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850