В прошлой теме описывался паттерн 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.
Результат работы приложения:
Таким образом, мы не используем события, не обращаемся явным образом к элементам интерфейса для получения или установки их значений, а взаимодействуем через привязку данных и команды.
Единственно что стоит заметить, что в Windows Forms не так много визуальных компонентов типа кнопки, которые имеют свойство Command и позволяют выполнять привязку к командам.