Поделиться через


Система команд

Browse sample. Обзор примера

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

Командный интерфейс предоставляет альтернативный подход к реализации команд, который гораздо лучше подходит для архитектуры 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 свойство 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 коллекция лиц, уже введенных:

На следующем снимка экрана показана кнопка "Отправить" после установки возраста:

Person Entry.

Когда пользователь сначала нажимает кнопку "Создать ", это включает форму записи, но отключает кнопку "Создать ". Пользователь вводит имя, возраст и навыки. В любое время во время редактирования пользователь может нажать кнопку Отмена, чтобы начать заново. Кнопка Отправить будет доступна только после того, как пользователь введет допустимое имя и возраст. При нажатии на кнопку Отправить пользователь переходит в коллекцию, отображаемую объектом 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;
            });
        ···
    }
    ···
}

Функция 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="&#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 Значение, заданное как правило, совпадает с текстом, отображаемым кнопкой, за исключением десятичной запятой, что для целей ясности отображается со средним символом точки:

Decimal keyboard.

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