命令

Browse sample.浏览示例

在使用模型-视图-视图模型 (MVVM) 模式的 .NET Multi-platform App UI (.NET MAUI) 应用中,数据绑定是在 viewmodel 中的属性(通常是派生自 INotifyPropertyChanged 的类)和视图中的属性(通常是 XAML 文件)之间定义的。 有时,应用的需求超越了属性绑定层面,它要求用户启动影响 viewmodel 中某些内容的命令。 这些命令通常通过点击按钮或手指敲击触发信号,往往是以下两个事件的处理程序中的代码隐藏文件中处理它们:ButtonClicked 事件或 TapGestureRecognizerTapped 事件。

命令接口提供了另一种实现命令的方法,这种方法更适合 MVVM 体系结构。 viewmodel 可以包含命令,这些命令是针对视图中的特定活动(例如 Button 单击)而执行的方法。 在这些命令和 Button 之间定义了数据绑定。

为允许在 Button 和 viewmodel 之间进行数据绑定,Button 定义了两个属性:

若要使用命令接口,需定义面向 ButtonCommand 属性的数据绑定,其中源是 viewmodel 中的属性,类型为 ICommand。 viewmodel 包含与单击按钮时执行的 ICommand 属性关联的代码。 如果多个按钮都绑定到 viewmodel 中的同一个 ICommand 属性,可以将 CommandParameter 属性设置为任意数据,以区分这些按钮。

许多其他视图也会定义 CommandCommandParameter 属性。 可在 viewmodel 中以不依赖于视图中用户界面对象的方法来处理上述所有命令。

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; }

viewmodel 还必须引用实现 ICommand 接口的类。 在视图中,ButtonCommand 属性绑定到该属性:

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

用户按下 Button 时,Button 调用绑定到其 Command 属性的 ICommand 对象中的 Execute 方法。

首次在 ButtonCommand 属性上定义绑定时,以及数据绑定以某种方式更改时,Button 调用 ICommand 对象中的 CanExecute 方法。 如果 CanExecute 返回 false,则 Button 将禁用其自身。 这表示特定命令当前不可用或无效。

Button 还在 ICommandCanExecuteChanged 事件上附加处理程序。 该事件是从 viewmodel 内引发的。 引发该事件时,Button 将再次调用 CanExecute。 如果 CanExecute 返回 trueButton 启用其自身;如果 CanExecute 返回 false,则禁用其自身。

警告

如果使用命令接口,请勿使用 ButtonIsEnabled 属性。

viewmodel 定义类型为 ICommand 的属性时,它还必须包含或引用实现 ICommand 接口的类。 该类必须包含或引用 ExecuteCanExecute 方法,并且每当 CanExecute 方法返回不同的值时均触发 CanExecuteChanged 事件。 可以使用 .NET MAUI 中包含的 CommandCommand<T> 类来实现 ICommand 接口。 通过这些类,可以在类构造函数中指定 ExecuteCanExecute 方法的主体。

提示

需要使用 CommandParameter 属性区分绑定到同一 ICommand 属性的多个视图时,使用 Command<T>,而在没有必要进行区分时,使用 Command 类。

基本命令

以下示例演示了在 viewmodel 中实现的基本命令。

PersonViewModel 类定义了分别名为 NameAgeSkills 的三个属性,这三个属性定义一个人。

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 的新对象,并允许用户填写数据。 为此,该类定义了类型为 boolIsEditing 属性和类型为 PersonViewModelPersonEdit 属性。 此外,该类还定义了 ICommand 类型的三个属性和 IList<PersonViewModel> 类型的一个名为 Persons 的属性:

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 事件。 这些属性都是在类首次创建时设置的,此后不会更改。

以下示例展示了使用 PersonCollectionViewModel 的 XAML:

<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 属性被设置为 PersonCollectionViewModelGrid 包含:一个 Button(带有文本“New”,其 Command 属性绑定到 viewmodel 中的 NewCommand 属性)、一个输入窗体(其属性绑定到 IsEditing 属性以及 PersonViewModel 的属性)以及另外两个绑定到 viewmodel 的 SubmitCommandCancelCommand 属性的按钮。 ListView 显示已录入的人员的集合:

以下屏幕截图展示了设置年龄之后启用的“提交”按钮:

Person Entry.

当用户首次按“新建”按钮时,此操作将启用输入窗体,但会禁用“新建”按钮。 然后用户输入姓名、年龄和技能。 在编辑过程中,用户随时都可以按下“取消”按钮重新开始。 只有在输入了姓名和有效年龄后,才启用“提交”按钮。 按“提交”按钮可将人员转移到 ListView 显示的集合中。 按“取消”或“提交”按钮后,会清除输入窗体中的内容并再次启用“新建”按钮

“新建”、“提交”和“取消”按钮的所有逻辑都通过定义 NewCommandSubmitCommandCancelCommand 属性在 PersonCollectionViewModel 中处理PersonCollectionViewModel 的构造函数将这三个属性设置为 Command 类型的对象。

借助 Command 类的构造函数,你可以传递与 ExecuteCanExecute 相对应的 ActionFunc<bool> 类型的参数。 此操作和函数可以在 Command 构造函数中定义为 lambda 函数:

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();
    }
    ···
}

用户单击“新建”按钮时,执行传递给 Command 构造函数的 execute 函数。 这将创建一个新的 PersonViewModel 对象,为该对象的 PropertyChanged 事件设置一个处理程序,将 IsEditing 设置为 true,并调用在构造函数之后定义的 RefreshCanExecutes 方法。

除了实现 ICommand 接口外,Command 类还定义了名为 ChangeCanExecute 的方法。 每当发生任何可能更改 CanExecute 方法的返回值的事件时,viewmodel 都应为 ICommand 属性调用 ChangeCanExecute。 调用 ChangeCanExecute 将导致 Command 类触发 CanExecuteChanged 方法。 Button 已为该事件附加了一个处理程序,并通过再次调用 CanExecute 进行响应,然后根据该方法的返回值启用自身。

NewCommandexecute 方法调用 RefreshCanExecutes 时,NewCommand 属性得到对 ChangeCanExecute 的调用,Button 调用 canExecute 方法,该方法现在返回 false,因为 IsEditing 的属性现在是 true

PersonViewModel 对象的 PropertyChanged 处理程序调用 SubmitCommandChangeCanExecute 方法:

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;
            });
        ···
    }
    ···
}

每当编辑的 PersonViewModel 对象中的属性发生更改时,都会调用 SubmitCommandcanExecute 函数。 仅当 Name 属性的长度至少为一个字符且 Age 大于 0 时,它返回 true。 此时,“提交”按钮将变为启用状态

“提交”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 方法在编辑 PersonViewModel 时随时返回 true

注意

不必将 executecanExecute 方法定义为 lambda 函数。 可以在 viewmodel 中将它们作为专用方法写入,并在 Command 构造函数中引用。 但是,这种方式往往会导致许多方法在 viewmodel 中只得到一次引用。

使用命令参数

有时,一个或多个按钮(或其他用户界面对象)可以方便地在 viewmodel 中共享相同的 ICommand 属性。 在这种情况下,可以使用 CommandParameter 属性来区分按钮。

可以继续为这些共享 ICommand 属性使用 Command 类。 该类定义了一个替代构造函数,该构造函数接受具有类型为 Object 的参数的 executecanExecute 方法。 这就是将 CommandParameter 传递给这些方法的方式。 但是,在指定 CommandParameter 时,最简单的方法是使用泛型 Command<T> 类来指定设置为 CommandParameter 的对象的类型。 指定的 executecanExecute 方法具有该类型的参数。

以下示例演示了用于输入十进制数字的键盘:

<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。 此 viewmodel 的 Entry 属性绑定到 LabelText 属性。 所有 Button 对象都绑定到 viewmodel 中的命令:ClearCommandBackspaceCommandDigitCommand。 表示 10 位数和小数点的共计 11 个按钮共享与 DigitCommand 的绑定。 CommandParameter 区分这些按钮。 设置为 CommandParameter 的值通常与按钮显示的文本相同,但小数点除外,为清晰起见,小数点以中间点字符显示:

Decimal keyboard.

DecimalKeypadViewModel 定义类型为 stringEntry 属性和三个类型为 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();
    }
    ···
}

由于始终启用按钮,因此不必在 Command 构造函数中指定 canExecute 参数。

退格按钮仅在输入的长度大于 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 函数的逻辑确保 Entry 至少是字符串“0”

DigitCommand 属性绑定到 11 个按钮,每个按钮用 CommandParameter 属性标识自己。 DigitCommand 被设置为 Command<T> 类的一个实例。 通过 XAML 使用命令接口时,CommandParameter 属性通常是字符串,这是泛型参数的类型。 然后,executecanExecute 函数具有 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 方法仅在参数为小数点(指示按下小数点)且 Entry 已经包含小数点时才返回 false。 所有 execute 方法都调用 RefreshCanExecutes,然后它再为 DigitCommandClearCommand 调用 ChangeCanExecute。 这确保根据当前输入的数字的序列启用或禁用小数点和退格按钮。