Система команд
В приложении многоплатформенного пользовательского интерфейса приложения .NET (.NET MAUI), использующего шаблон Model-View-ViewModel (MVVM), привязки данных определяются между свойствами в режиме просмотра, который обычно является классом, производным от INotifyPropertyChanged
представления, и свойствами в представлении, который обычно является XAML-файлом. Иногда приложению требуется выйти за рамки этих привязок свойств, требуя от пользователя инициировать команды, влияющие на что-то в режиме просмотра. Эти команды обычно обозначаются путем нажатия кнопки или касания пальцем и традиционно обрабатываются в файле с выделенным кодом в обработчике для события Clicked
объекта Button или события Tapped
объекта TapGestureRecognizer.
Командный интерфейс предоставляет альтернативный подход к реализации команд, который гораздо лучше подходит для архитектуры MVVM. Модель представления может содержать команды, которые являются методами, выполняемыми в реакции на определенное действие в представлении, например щелчком Button мыши. Привязки данных определяются между этими командами и объектом Button.
Чтобы разрешить привязку данных между a Button и viewmodel, Button определяет два свойства:
Command
типаSystem.Windows.Input.ICommand
CommandParameter
типаObject
Чтобы использовать командный интерфейс, необходимо определить привязку данных, предназначенную Command
для свойства источника, которое является свойством Button в типе viewmodel ICommand. Объект viewmodel содержит код, связанный с этим ICommand свойством, которое выполняется при нажатии кнопки. Свойство можно задать CommandParameter
произвольным данным, чтобы различать несколько кнопок, если они все привязаны к ICommand одному свойству в режиме просмотра.
Многие другие представления также определяют Command
и CommandParameter
свойства. Все эти команды можно обрабатывать в режиме представления с помощью подхода, который не зависит от объекта пользовательского интерфейса в представлении.
ICommands
Интерфейс ICommand определяется в пространстве имен System.Windows.Input и состоит из двух методов и одного события:
public interface ICommand
{
public void Execute (Object parameter);
public bool CanExecute (Object parameter);
public event EventHandler CanExecuteChanged;
}
Чтобы использовать командный интерфейс, viewmodel должен содержать свойства типа ICommand:
public ICommand MyCommand { private set; get; }
Модуль представления также должен ссылаться на класс, реализующий ICommand интерфейс. В представлении Command
свойство a Button привязано к такому свойству:
<Button Text="Execute command"
Command="{Binding MyCommand}" />
Когда пользователь нажимает Button, Button вызывает метод Execute
в объекте ICommand, привязанном к его свойству Command
.
При первом определении привязки для свойства Command
объекта Button и при изменении привязки данных Button вызывает метод CanExecute
в объекте ICommand. Если CanExecute
возвращает false
, Button отключается. Это означает, что определенная команда в данный момент недоступна или недопустима.
Button также подключает обработчик для события CanExecuteChanged
объекта ICommand. Событие вызывается из модели представления. При возникновении Button этого события вызовы CanExecute
снова вызываются. Button включается, если CanExecute
возвращает true
, и отключается, если CanExecute
возвращает false
.
Предупреждение
Не используйте свойство IsEnabled
объекта Button при использовании командного интерфейса.
Когда модуль представления определяет свойство типа ICommand, модель представления также должна содержать или ссылаться на класс, реализующий ICommand интерфейс. Этот класс должен содержать или ссылаться на методы Execute
и CanExecute
и вызывать событие CanExecuteChanged
каждый раз, когда метод CanExecute
может возвращать другое значение. Для реализации интерфейса можно использовать класс или Command<T>
класс, включенный Command
ICommand в .NET MAUI. Эти классы позволяют указать текст методов Execute
и CanExecute
в конструкторах классов.
Совет
Используется Command<T>
при использовании CommandParameter
свойства для различения нескольких представлений, привязанных к ICommand одному свойству, и Command
класса, если это не обязательно.
Базовая команда
В следующих примерах показаны основные команды, реализованные в режиме просмотра.
Класс PersonViewModel
определяет три свойства с именем Name
, Age
а Skills
также определяет человека:
public class PersonViewModel : INotifyPropertyChanged
{
string name;
double age;
string skills;
public event PropertyChangedEventHandler PropertyChanged;
public string Name
{
set { SetProperty(ref name, value); }
get { return name; }
}
public double Age
{
set { SetProperty(ref age, value); }
get { return age; }
}
public string Skills
{
set { SetProperty(ref skills, value); }
get { return skills; }
}
public override string ToString()
{
return Name + ", age " + Age;
}
bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Object.Equals(storage, value))
return false;
storage = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Приведенный PersonCollectionViewModel
ниже класс создает новые объекты типа PersonViewModel
и позволяет пользователю заполнять данные. Для этого класс определяет IsEditing
, тип bool
и PersonEdit
тип , свойства PersonViewModel
. Кроме того, класс определяет три свойства типа ICommand и свойство с именем Persons
типа IList<PersonViewModel>
:
public class PersonCollectionViewModel : INotifyPropertyChanged
{
PersonViewModel personEdit;
bool isEditing;
public event PropertyChangedEventHandler PropertyChanged;
···
public bool IsEditing
{
private set { SetProperty(ref isEditing, value); }
get { return isEditing; }
}
public PersonViewModel PersonEdit
{
set { SetProperty(ref personEdit, value); }
get { return personEdit; }
}
public ICommand NewCommand { private set; get; }
public ICommand SubmitCommand { private set; get; }
public ICommand CancelCommand { private set; get; }
public IList<PersonViewModel> Persons { get; } = new ObservableCollection<PersonViewModel>();
bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Object.Equals(storage, value))
return false;
storage = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
В этом примере изменения трех ICommand свойств и Persons
свойства не приводят к PropertyChanged
возникновению событий. Эти свойства задаются при первом создании класса и не изменяются.
В следующем примере показан КОД XAML, который использует PersonCollectionViewModel
следующий код:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:DataBindingDemos"
x:Class="DataBindingDemos.PersonEntryPage"
Title="Person Entry">
<ContentPage.BindingContext>
<local:PersonCollectionViewModel />
</ContentPage.BindingContext>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- New Button -->
<Button Text="New"
Grid.Row="0"
Command="{Binding NewCommand}"
HorizontalOptions="Start" />
<!-- Entry Form -->
<Grid Grid.Row="1"
IsEnabled="{Binding IsEditing}">
<Grid BindingContext="{Binding PersonEdit}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Text="Name: " Grid.Row="0" Grid.Column="0" />
<Entry Text="{Binding Name}"
Grid.Row="0" Grid.Column="1" />
<Label Text="Age: " Grid.Row="1" Grid.Column="0" />
<StackLayout Orientation="Horizontal"
Grid.Row="1" Grid.Column="1">
<Stepper Value="{Binding Age}"
Maximum="100" />
<Label Text="{Binding Age, StringFormat='{0} years old'}"
VerticalOptions="Center" />
</StackLayout>
<Label Text="Skills: " Grid.Row="2" Grid.Column="0" />
<Entry Text="{Binding Skills}"
Grid.Row="2" Grid.Column="1" />
</Grid>
</Grid>
<!-- Submit and Cancel Buttons -->
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Text="Submit"
Grid.Column="0"
Command="{Binding SubmitCommand}"
VerticalOptions="Center" />
<Button Text="Cancel"
Grid.Column="1"
Command="{Binding CancelCommand}"
VerticalOptions="Center" />
</Grid>
<!-- List of Persons -->
<ListView Grid.Row="3"
ItemsSource="{Binding Persons}" />
</Grid>
</ContentPage>
В этом примере для свойства страницы BindingContext
задано PersonCollectionViewModel
значение . Содержит Grid текст New со свойствомCommand
, привязанным к NewCommand
свойству в моделе представления, форме записи с свойствами, привязанными к IsEditing
свойству, а также свойствамPersonViewModel
, а также двумя дополнительными кнопками, привязанными к SubmitCommand
свойству и CancelCommand
свойствам моделя представления.Button Отображается ListView коллекция лиц, уже введенных:
На следующем снимка экрана показана кнопка "Отправить" после установки возраста:
Когда пользователь сначала нажимает кнопку "Создать ", это включает форму записи, но отключает кнопку "Создать ". Пользователь вводит имя, возраст и навыки. В любое время во время редактирования пользователь может нажать кнопку Отмена, чтобы начать заново. Кнопка Отправить будет доступна только после того, как пользователь введет допустимое имя и возраст. При нажатии на кнопку Отправить пользователь переходит в коллекцию, отображаемую объектом ListView. После нажатия кнопки Отмена или Отправить форма ввода очищается и снова включается кнопка Создать.
Вся логика для кнопок Создать, Отправить и Отмена обрабатывается в PersonCollectionViewModel
посредством определения свойств NewCommand
, SubmitCommand
и CancelCommand
. Конструктор PersonCollectionViewModel
задает эти три свойства объектам типа Command
.
Конструктор Command
класса позволяет передавать аргументы типа Action
и Func<bool>
соответствующие Execute
методам.CanExecute
Это действие и функция можно определить как лямбда-функции в конструкторе Command
:
public class PersonCollectionViewModel : INotifyPropertyChanged
{
···
public PersonCollectionViewModel()
{
NewCommand = new Command(
execute: () =>
{
PersonEdit = new PersonViewModel();
PersonEdit.PropertyChanged += OnPersonEditPropertyChanged;
IsEditing = true;
RefreshCanExecutes();
},
canExecute: () =>
{
return !IsEditing;
});
···
}
void OnPersonEditPropertyChanged(object sender, PropertyChangedEventArgs args)
{
(SubmitCommand as Command).ChangeCanExecute();
}
void RefreshCanExecutes()
{
(NewCommand as Command).ChangeCanExecute();
(SubmitCommand as Command).ChangeCanExecute();
(CancelCommand as Command).ChangeCanExecute();
}
···
}
Когда пользователь щелкает кнопку Создать, выполняется функция execute
, переданная конструктору Command
. При этом создается новый объект PersonViewModel
, задается обработчик для события PropertyChanged
этого объекта, для IsEditing
устанавливается значение true
и вызывается метод RefreshCanExecutes
, определенный после конструктора.
Помимо реализации интерфейса ICommand, класс Command
определяет метод с именем ChangeCanExecute
. Объект viewmodel должен вызывать ChangeCanExecute
ICommand свойство всякий раз, когда все происходит, что может изменить возвращаемое значение CanExecute
метода. Вызов ChangeCanExecute
заставляет класс Command
активировать метод CanExecuteChanged
. Объект Button присоединяет обработчик для этого события и реагирует повторным вызовом CanExecute
, а затем включается в зависимости от возвращаемого значения этого метода.
Когда метод execute
NewCommand
вызывает RefreshCanExecutes
, свойство NewCommand
получает вызов ChangeCanExecute
, а Button вызывает метод canExecute
, который теперь возвращает false
, поскольку свойство IsEditing
теперь имеет значение true
.
Обработчик PropertyChanged
нового PersonViewModel
объекта вызывает ChangeCanExecute
метод SubmitCommand
:
public class PersonCollectionViewModel : INotifyPropertyChanged
{
···
public PersonCollectionViewModel()
{
···
SubmitCommand = new Command(
execute: () =>
{
Persons.Add(PersonEdit);
PersonEdit.PropertyChanged -= OnPersonEditPropertyChanged;
PersonEdit = null;
IsEditing = false;
RefreshCanExecutes();
},
canExecute: () =>
{
return PersonEdit != null &&
PersonEdit.Name != null &&
PersonEdit.Name.Length > 1 &&
PersonEdit.Age > 0;
});
···
}
···
}
Функция canExecute
для SubmitCommand
вызывается каждый раз при изменении свойства в редактируемом объекте PersonViewModel
. Она возвращает true
, только когда свойство Name
имеет по крайней мере один символ, а Age
больше 0. В этот момент кнопка Отправить становится доступной.
Функция execute
отправки удаляет обработчик измененных свойств из PersonViewModel
коллекции, добавляет объект в Persons
коллекцию и возвращает все в исходное состояние.
Функция execute
для кнопки Отмена делает все то же самое, что и кнопка Отправить, но не добавляет объект в коллекцию:
public class PersonCollectionViewModel : INotifyPropertyChanged
{
···
public PersonCollectionViewModel()
{
···
CancelCommand = new Command(
execute: () =>
{
PersonEdit.PropertyChanged -= OnPersonEditPropertyChanged;
PersonEdit = null;
IsEditing = false;
RefreshCanExecutes();
},
canExecute: () =>
{
return IsEditing;
});
}
···
}
Метод canExecute
возвращает true
каждый раз при редактировании PersonViewModel
.
Примечание.
Не обязательно определять методы execute
и canExecute
как лямбда-функции. Их можно написать как частные методы в режиме просмотра и ссылаться на них в Command
конструкторах. Однако этот подход может привести к множеству методов, на которые ссылается только один раз в представлении.
Использование параметров команды
Иногда это удобно для одной или нескольких кнопок или других объектов пользовательского интерфейса для совместного использования того же ICommand свойства в viewmodel. В этом случае можно использовать CommandParameter
свойство для различения кнопок.
Вы можете продолжать использовать класс Command
для таких общих свойств ICommand. Класс определяет альтернативный конструктор, который принимает и canExecute
методы execute
с параметрами типаObject
. Вот как CommandParameter
передается этим методам. Однако при указании CommandParameter
объекта проще всего использовать универсальный Command<T>
класс, чтобы указать тип набора CommandParameter
объектов. Указываемые вами методы execute
и canExecute
имеют параметры этого типа.
В следующем примере показана клавиатура для ввода десятичных чисел:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:DataBindingDemos"
x:Class="DataBindingDemos.DecimalKeypadPage"
Title="Decimal Keyboard">
<ContentPage.BindingContext>
<local:DecimalKeypadViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<Style TargetType="Button">
<Setter Property="FontSize" Value="32" />
<Setter Property="BorderWidth" Value="1" />
<Setter Property="BorderColor" Value="Black" />
</Style>
</ContentPage.Resources>
<Grid WidthRequest="240"
HeightRequest="480"
ColumnDefinitions="80, 80, 80"
RowDefinitions="Auto, Auto, Auto, Auto, Auto, Auto"
ColumnSpacing="2"
RowSpacing="2"
HorizontalOptions="Center"
VerticalOptions="Center">
<Label Text="{Binding Entry}"
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3"
Margin="0,0,10,0"
FontSize="32"
LineBreakMode="HeadTruncation"
VerticalTextAlignment="Center"
HorizontalTextAlignment="End" />
<Button Text="CLEAR"
Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
Command="{Binding ClearCommand}" />
<Button Text="⇦"
Grid.Row="1" Grid.Column="2"
Command="{Binding BackspaceCommand}" />
<Button Text="7"
Grid.Row="2" Grid.Column="0"
Command="{Binding DigitCommand}"
CommandParameter="7" />
<Button Text="8"
Grid.Row="2" Grid.Column="1"
Command="{Binding DigitCommand}"
CommandParameter="8" />
<Button Text="9"
Grid.Row="2" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="9" />
<Button Text="4"
Grid.Row="3" Grid.Column="0"
Command="{Binding DigitCommand}"
CommandParameter="4" />
<Button Text="5"
Grid.Row="3" Grid.Column="1"
Command="{Binding DigitCommand}"
CommandParameter="5" />
<Button Text="6"
Grid.Row="3" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="6" />
<Button Text="1"
Grid.Row="4" Grid.Column="0"
Command="{Binding DigitCommand}"
CommandParameter="1" />
<Button Text="2"
Grid.Row="4" Grid.Column="1"
Command="{Binding DigitCommand}"
CommandParameter="2" />
<Button Text="3"
Grid.Row="4" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="3" />
<Button Text="0"
Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="2"
Command="{Binding DigitCommand}"
CommandParameter="0" />
<Button Text="·"
Grid.Row="5" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="." />
</Grid>
</ContentPage>
В этом примере страница BindingContext
— это DecimalKeypadViewModel
. Свойство Entry этого представления привязано к Text
свойству объекта Label. Button Все объекты привязаны к командам в модели представления: ClearCommand
, BackspaceCommand
и DigitCommand
. 11 кнопок для 10 цифр и десятичного разделителя имеют общую привязку к DigitCommand
. CommandParameter
различает эти кнопки. CommandParameter
Значение, заданное как правило, совпадает с текстом, отображаемым кнопкой, за исключением десятичной запятой, что для целей ясности отображается со средним символом точки:
Определяет DecimalKeypadViewModel
свойство типа string
и три свойства типаICommand:Entry
public class DecimalKeypadViewModel : INotifyPropertyChanged
{
string entry = "0";
public event PropertyChangedEventHandler PropertyChanged;
···
public string Entry
{
private set
{
if (entry != value)
{
entry = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Entry"));
}
}
get
{
return entry;
}
}
public ICommand ClearCommand { private set; get; }
public ICommand BackspaceCommand { private set; get; }
public ICommand DigitCommand { private set; get; }
}
Кнопка, соответствующая ClearCommand
всегда включена, и задает для записи значение "0":
public class DecimalKeypadViewModel : INotifyPropertyChanged
{
···
public DecimalKeypadViewModel()
{
ClearCommand = new Command(
execute: () =>
{
Entry = "0";
RefreshCanExecutes();
});
···
}
void RefreshCanExecutes()
{
((Command)BackspaceCommand).ChangeCanExecute();
((Command)DigitCommand).ChangeCanExecute();
}
···
}
Поскольку кнопка всегда активна, не нужно указывать аргумент canExecute
в конструкторе Command
.
Кнопка Backspace доступна только в том случае, если длина записи больше 1 или если Entry не равно строке "0":
public class DecimalKeypadViewModel : INotifyPropertyChanged
{
···
public DecimalKeypadViewModel()
{
···
BackspaceCommand = new Command(
execute: () =>
{
Entry = Entry.Substring(0, Entry.Length - 1);
if (Entry == "")
{
Entry = "0";
}
RefreshCanExecutes();
},
canExecute: () =>
{
return Entry.Length > 1 || Entry != "0";
});
···
}
···
}
Логика для функции execute
для кнопки Backspace гарантирует, что Entry — это по крайней мере строка "0".
Свойство DigitCommand
привязано к 11 кнопкам, каждая из которых определяет себя с помощью свойства CommandParameter
. Для DigitCommand
экземпляра Command<T>
класса задано значение. При использовании интерфейса командной строки с XAML CommandParameter
свойства обычно являются строками, которые являются типом универсального аргумента. Функции execute
и canExecute
получают аргументы типа string
:
public class DecimalKeypadViewModel : INotifyPropertyChanged
{
···
public DecimalKeypadViewModel()
{
···
DigitCommand = new Command<string>(
execute: (string arg) =>
{
Entry += arg;
if (Entry.StartsWith("0") && !Entry.StartsWith("0."))
{
Entry = Entry.Substring(1);
}
RefreshCanExecutes();
},
canExecute: (string arg) =>
{
return !(arg == "." && Entry.Contains("."));
});
}
···
}
Метод execute
добавляет строковый аргумент в свойство Entry. Тем не менее, если результат начинается с нуля (но не с нуля с десятичным разделителем), начальный ноль необходимо удалить с помощью функции Substring
. Метод canExecute
возвращает false
только в том случае, если аргумент является десятичным разделителем (указывая, что десятичный разделитель нажат) и Entry уже содержит десятичный разделитель. Все методы execute
вызывают RefreshCanExecutes
, который затем вызывает ChangeCanExecute
для DigitCommand
и ClearCommand
. Это гарантирует, что кнопки десятичного разделителя и удаления включаются и отключаются в соответствии с текущей последовательностью введенных цифр.