Командный

Просмотрите пример. Обзор примера

В приложении многоплатформенного пользовательского интерфейса приложений .NET (.NET MAUI), использующего шаблон Модель-Представление-ПредставлениеМодель (MVVM), привязки данных определяются между свойствами в модели-представление, которая обычно является классом, производным от INotifyPropertyChanged, и свойствами в представлении, которое обычно является XAML-файлом. Иногда приложению требуется выйти за рамки этих привязок свойств, требуя от пользователя инициировать команды, влияющие на что-то в режиме просмотра. Эти команды обычно сигнализируются нажатиями кнопки или касаниями пальцев, и традиционно они обрабатываются в файле связанного кода в обработчике события Clicked или события Button в Tapped.

Интерфейс командной строки предоставляет альтернативный подход к реализации команд, которые гораздо лучше подходят для архитектуры MVVM. Модель представления может содержать команды, которые являются методами, выполняемыми в реакции на определенное действие в представлении, например щелчком Button мыши. Привязки данных определяются между этими командами и Button.

Чтобы разрешить привязку данных между a Button и viewmodel, Button определяет два свойства:

Чтобы использовать командный интерфейс, необходимо определить привязку данных к свойству 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 объекта Button привязано к этому свойству:

<Button Text="Execute command"
        Command="{Binding MyCommand}" />

Когда пользователь нажимает Button, Button вызывает метод Execute в объекте ICommand, привязанном к его свойству Command.

Когда привязка сначала определена в Command свойстве Buttonобъекта, а также при изменении привязки данных каким-то образом Button вызывает CanExecute метод в объекте ICommand . Если CanExecute возвращает false, то Button отключает себя. Это означает, что конкретная команда в настоящее время недоступна или недопустима.

Кроме того, обработчик Button подключается к событию CanExecuteChangedICommand. Событие должно вызываться вручную из модели представления при изменении условий CanExecute , влияющих на результат. При возникновении этого события Button вызывает CanExecute снова. Button включает себя, если CanExecute возвращает true, и отключает себя, если CanExecute возвращает false.

Это важно

В отличие от некоторых платформ пользовательского интерфейса (например, WPF), .NET MAUI не обнаруживает автоматически, когда возвращаемое значение CanExecute может измениться. Необходимо вручную вызвать событие CanExecuteChanged (или вызвать ChangeCanExecute() в классе Command) всякий раз, когда происходит любое изменение условий, влияющее на результат CanExecute. Обычно это делается, когда изменяются свойства, от которых зависит CanExecute.

Замечание

Вы также можете использовать свойство IsEnabledButton в сочетании с методом CanExecute или вместо него. В .NET MAUI 7 и более ранних версиях невозможно использовать свойство IsEnabled при использовании командного интерфейса, так как возвращаемое значение метода Button всегда переопределяет свойство CanExecute. Это исправлено в .NET MAUI 8 и более поздних версиях; свойство IsEnabled теперь доступно для использования в командных Button. Однако помните, что теперь и IsEnabled свойство, и CanExecuteметод должны возвращать значение true, чтобы Button был задействован, и родительский элемент управления также должен быть активирован.

Когда модуль представления определяет свойство типа ICommand, модель представления также должна содержать или ссылаться на класс, реализующий ICommand интерфейс. Этот класс должен содержать или ссылаться на ExecuteCanExecute методы и вручную запускать CanExecuteChanged событие, когда CanExecute метод может возвращать другое значение. Для реализации интерфейса Command можно использовать класс Command<T> или ICommand, включённый в .NET MAUI. Эти классы позволяют указать тела методов Execute и CanExecute в конструкторах классов.

Подсказка

Используйте Command<T>, когда используете CommandParameter свойство для различения нескольких представлений, привязанных к тому же ICommand свойству, и Command класс, когда это не требуется.

Основные команды

В следующих примерах показаны основные команды, реализованные в viewmodel.

Класс 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"
             x:DataType="local:PersonCollectionViewModel">             
    <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 x:DataType="local:PersonViewModel"
                  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 содержит Button, в котором текст New со свойством Command, привязанным к свойству NewCommand в модели представления, форму ввода с свойствами, привязанными к свойству IsEditing, а также свойства PersonViewModel, и две дополнительные кнопки, привязанные к свойствам SubmitCommand и CancelCommand модели представления. 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 снова, а затем включает себя в зависимости от возвращаемого значения этого метода.

Когда метод executeNewCommand вызывает 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;
            });
        ···
    }
    ···
}

Функция canExecuteSubmitCommand вызывается при каждом изменении свойства в редактируемом объекте 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 свойств. Класс определяет альтернативный конструктор, который принимает методы execute и canExecute с параметрами типа 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"
             x:DataType="local:DecimalKeypadViewModel">
    <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="&#x21E6;"
                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="&#x00B7;"
                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 определяет свойство типа Entry и три свойства типа string:ICommand

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 включена только в том случае Entry , если длина записи превышает 1 или не равна строке "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. Это гарантирует, что кнопки десятичной точки и клавиши Backspace активированы или деактивированы на основе текущей последовательности введенных цифр.