指挥

浏览示例。 浏览示例

在使用 Model-View-ViewModel (MVVM) 模式的 .NET 多平台应用 UI(.NET MAUI)应用中,数据绑定在 viewmodel 中的属性(通常是派生自 INotifyPropertyChanged的类)和视图中的属性(通常是 XAML 文件)之间定义。 有时,应用的需求超越这些属性绑定,需要用户启动命令来影响视图模型中的某些内容。 这些命令通常通过按钮点击或手指触碰来产生信号,传统上,这些命令在后置代码文件中处理,用于处理 ClickedButton 事件或 TappedTapGestureRecognizer 事件。

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

若要允许在 Button 和 viewmodel 之间绑定数据,Button 会定义两个属性:

若要使用命令接口,请定义一个数据绑定,用于定位Command源是类型 Buttonviewmodel 中的属性ICommand的属性。 viewmodel 包含与单击按钮时执行的该 ICommand 属性关联的代码。 可以将 CommandParameter 属性设置为任意数据,以便区分多个按钮(如果它们都绑定到视图模型中的同一 ICommand 属性)。

许多其他视图也定义 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 类。 在视图中,CommandButton 属性被绑定到该属性:

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

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

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

此外,Button也会在CanExecuteChangedICommand事件上附加处理程序。 每当影响结果的条件发生更改时,都必须从 viewmodel 中手动引发该 CanExecute 事件。 引发该事件时, Button 再次调用 CanExecuteButton如果CanExecute返回true,则启用自身;如果CanExecute返回false,则禁用自身。

重要

与某些 UI 框架(如 WPF)不同,.NET MAUI 不会自动检测可能更改的 CanExecute 返回值。 每当任何影响CanExecute结果的条件更改时,都必须手动引发CanExecuteChanged事件(或者在Command类上调用ChangeCanExecute())。 这通常是在修改依赖的属性 CanExecute 时完成的。

注释

还可以使用 IsEnabled 属性 Button 而不是 CanExecute 方法,也可以与该方法结合使用。 在 .NET MAUI 7 及更早版本中,不能在使用IsEnabled命令接口时使用Button属性,因为CanExecute方法的返回值总是会覆盖IsEnabled属性。 在 .NET MAUI 8 及其更高版本中,此问题已得到修复;现在基于命令的 IsEnabled 可以使用 Button 属性。 但是,请注意,IsEnabled 属性和 CanExecute 方法现在必须 同时 返回 true 才能启用 Button(而且父控件也必须启用)。

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

小窍门

使用 Command<T> 是为了在使用 CommandParameter 属性时区分绑定到同一 ICommand 属性的多个视图;如果不需要此特性,则使用 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对象,并允许用户填充数据。 为此,类定义了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属性绑定到viewmodel中的NewCommand属性;一个输入表单,其属性绑定到IsEditing属性,以及PersonViewModel,将其属性绑定到viewmodel中的相关属性;两按钮分别绑定到viewmodel的SubmitCommandCancelCommand属性。 显示已经输入的人员集合 ListView:

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

人员录入。

当用户首次按下 “新建 ”按钮时,这将启用输入窗体,但禁用 “新建 ”按钮。 然后,用户输入名称、年龄和技能。 在编辑期间,用户可以随时按 “取消” 按钮开始。 仅当输入名称和有效年龄时,才启用 “提交 ”按钮。 按下此提交按钮会将该人员转移到由ListView显示的集合。 按下 “取消 ”或 “提交 ”按钮后,将清除输入表单,并再次启用 “新建 ”按钮。

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

类的 Command 构造函数允许您传递类型为 ActionFunc<bool> 的参数,分别对应于 ExecuteCanExecute 方法。 此操作和函数可以在 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();
    }
    ···
}

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

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

execute的方法调用NewCommand时,RefreshCanExecutes的属性将获取对NewCommand的调用,接着ChangeCanExecute调用Button方法。现在,Button方法返回false,因为IsEditing的属性现在是true

PropertyChangedPersonViewModel对象的处理程序调用ChangeCanExecuteSubmitCommand方法:

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发生属性更改时,都会调用SubmitCommandPersonViewModel函数。 仅当true属性长度至少为一个字符且Name大于 0 时,才返回Age。 此时,将启用 “提交 ”按钮。

execute Submit 的函数从 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

注释

不需要将executecanExecute方法定义为 lambda 函数。 可以在 viewmodel 中将其编写为私有方法,并在构造函数中 Command 引用它们。 然而,这种方法可能会导致许多方法在视图模型中只被引用一次。

使用命令参数

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

可以继续使用Command类来处理这些共享的ICommand属性。 类定义一个替代构造函数,该构造函数接受 executecanExecute 具有类型 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 是一个 DecimalKeypadViewModelEntry此视图模型的属性绑定到TextLabel的属性。 Button所有对象都绑定到 viewmodel 中的命令:ClearCommandBackspaceCommandDigitCommand。 10 位数字和小数点的 11 个按钮共享绑定到 DigitCommandCommandParameter 区分了这些按钮。 设置为 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参数。

仅当条目长度大于 1 或不等于字符串“0”时,才启用 Entry 按钮:

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 属性通常是字符串,这是泛型参数的类型。 然后,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仅当参数是小数点(指示按下小数点)并且false已包含小数点时,该方法才返回Entryexecute 的所有方法调用 RefreshCanExecutes,然后调用 ChangeCanExecute 用于 DigitCommandClearCommand。 这可确保根据输入的数字的当前序列启用或禁用小数点和后空按钮。