命令

Browse sample. 流覽範例

在使用 Model-View-ViewModel (MVVM) 模式的 .NET 多平臺應用程式 UI (.NET MAUI) 應用程式中,數據系結會在 viewmodel 中的屬性之間定義,這通常是衍生自 INotifyPropertyChanged的類別,而檢視中的屬性通常是 XAML 檔案。 有時候,應用程式需要超越這些屬性系結的需求,方法是要求使用者起始會影響 viewmodel 中某些事物的命令。 這些命令通常是透過按鈕點擊或手指點選發出訊號,傳統上會以 ButtonClicked 事件處理常式或 TapGestureRecognizerTapped 事件處理常式在程式碼後置檔案中加以處理。

命令介面可針對比較適合 MVVM 架構的命令實作,提供一種替代方法。 viewmodel 可以包含命令,這些命令是響應檢視中特定活動的方法,例如 Button 按兩下。 資料繫結會定義在這些命令與 Button 之間。

若要允許 與 viewmodel 之間的 Button 數據系結,定義 Button 兩個屬性:

若要使用命令介面,您可以定義數據系結,其目標Command為 類型 viewmodel 中來源為 屬性的 ICommand屬性Button。 viewmodel 包含與按鍵時所執行該 ICommand 屬性相關聯的程式代碼。 您可以將 屬性設定 CommandParameter 為任意數據,以區分多個按鈕是否全部系結至 ViewModel 中的相同 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 會呼叫 ICommand 物件中繫結至其 Command 屬性的 Execute 方法。

第一次在 ButtonCommand 屬性上定義繫結時,若資料繫結以某種方式變更,Button 就會呼叫 ICommand 物件中的 CanExecute 方法。 如果 CanExecute 傳回 false,則 Button 會停用其本身。 這表示,特定命令目前無法使用或無效。

Button 也會在 ICommandCanExecuteChanged 事件上附加處理常式。 事件會從 viewmodel 內引發。 引發該事件時,會 Button 再次呼叫 CanExecute 。 若 CanExecute 傳回 trueButton 即會啟用其本身;若 CanExecute 傳回 false,則會停用其本身。

警告

如果您要使用命令介面,請勿使用 ButtonIsEnabled 屬性。

當您的 viewmodel 定義 類型的 ICommand屬性時,viewmodel 也必須包含或參考實作 介面的 ICommand 類別。 這個類別必須包含或參考 ExecuteCanExecute 方法,而且只要 CanExecute 方法可能傳回不同的值時,就會引發 CanExecuteChanged 事件。 您可以使用 Command .NET MAUI 中包含的 或 Command<T> 類別來實作 ICommand 介面。 這些類別可讓您在類別建構函式中指定 ExecuteCanExecute 方法的主體。

提示

當您 Command<T> 使用 CommandParameter 屬性來區別系結至相同 ICommand 屬性的多個檢視,以及不需要時類別 Command 時使用 。

基本命令

下列範例示範在 viewmodel 中實作的基本命令。

類別 PersonViewModel 會定義三個名為 NameAge和 的屬性,並 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 事件。 當類別第一次建立且不會變更時,就會設定這些屬性。

下列範例顯示取用 的 PersonCollectionViewModelXAML:

<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 中的 屬性、屬性系結至 IsEditingNewCommand 屬性的項目窗體,以及 PersonViewModel和 系結至 SubmitCommand viewmodel 和 CancelCommand 屬性的兩個按鈕。 會顯示 ListView 已輸入的人員集合:

下列螢幕快照顯示設定年齡後啟用的 [提交 ] 按鈕:

Person Entry.

當使用者第一次按下 [ 新增 ] 按鈕時,這會啟用輸入窗體,但會停用 [ 新增 ] 按鈕。 使用者接著輸入姓名、年齡和技能。 在編輯期間的任何時間,使用者可以按下 Cancel (取消) 按鈕,以便從頭開始。 只有在已輸入姓名和有效的年齡時,才會啟用 Submit (提交) 按鈕。 按下此 Submit 按鈕,就會將人員資料傳送到 ListView 所顯示的集合。 按下 CancelSubmit 按鈕之後,系統會清除項目表單,並再次啟用 New 按鈕。

[新增]、[提交] 和 [取消] 按鈕的所有邏輯是透過 NewCommandSubmitCommandCancelCommand 屬性的定義在 PersonCollectionViewModel 中加以處理。 PersonCollectionViewModel 的建構函式會將這三個屬性設定為 Command 類型的物件。

類別的 Command 建構函式可讓您傳遞 型 Action 別的自變數,並 Func<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();
    }
    ···
}

當使用者按一下 New 按鈕時,即會執行傳遞至 Command 建構函式的 execute 函式。 這會建立新的 PersonViewModel 物件、在該物件的 PropertyChanged 事件上設定處理常式、將 IsEditing 設定為 true,以及呼叫建構函式之後定義的 RefreshCanExecutes 方法。

除了實作 ICommand 介面,Command 類別還會定義名為 ChangeCanExecute 的方法。 每當發生任何可能變更方法傳CanExecute回值時,ViewModel 應該呼叫 ChangeCanExecuteICommand 屬性。 呼叫 ChangeCanExecute 會導致 Command 類別引發 CanExecuteChanged 方法。 Button 已附加該事件的處理常式,並透過再次呼叫 CanExecute,然後根據該方法的傳回值啟用其本身來回應。

NewCommandexecute 方法呼叫 RefreshCanExecutes 時,則 NewCommand 屬性會呼叫 ChangeCanExecute,而 Button 會呼叫 canExecute 方法,它現在會傳回 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;
            });
        ···
    }
    ···
}

每次所編輯的 PersonViewModel 物件有屬性變更時,即會呼叫 SubmitCommandcanExecute 函式。 只有在 Name 屬性長度至少是 1 個字元,且 Age 大於 0 時,它才會傳回 true。 此時,[提交] 按鈕會變成啟用狀態。

Submitexecute函式會從 PersonViewModel中移除 屬性變更的處理程式,將 物件新增至Persons集合,並將所有專案傳回其初始狀態。

[取消] 按鈕的 execute 函式會執行 Submit 按鈕執行的所有作業,但將物件新增至集合除外:

public class PersonCollectionViewModel : INotifyPropertyChanged
{
    ···
    public PersonCollectionViewModel()
    {
        ···
        CancelCommand = new Command(
            execute: () =>
            {
                PersonEdit.PropertyChanged -= OnPersonEditPropertyChanged;
                PersonEdit = null;
                IsEditing = false;
                RefreshCanExecutes();
            },
            canExecute: () =>
            {
                return IsEditing;
            });
    }
    ···
}

在編輯 PersonViewModel 的任何時候,canExecute 方法都會傳回 true

注意

不需要將 executecanExecute 方法定義為 Lambda 函式。 您可以將它們撰寫為 viewmodel 中的私用方法,並在建構函式中 Command 參考它們。 不過,此方法可能會導致許多方法在 viewmodel 中只參考一次。

使用命令參數

有時候,一或多個按鈕或其他使用者介面物件在 viewmodel 中共用相同 ICommand 屬性會很方便。 在此情況下,您可以使用 CommandParameter 屬性來區分按鈕。

您可以針對這些共用的 ICommand 屬性繼續使用 Command 類別。 類別會定義替代建構函式,這個建構函式會接受 executecanExecute 具有類型 Object參數的方法。 這是 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>

在這裡範例中,頁面的 BindingContextDecimalKeypadViewModelEntry這個 viewmodel 的 屬性會系結至 TextLabel屬性。 Button所有物件都會繫結至 viewmodel 中的命令:ClearCommandBackspaceCommandDigitCommand。 10 個數字和小數點的 11 個按鈕共用與 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();
    }
    ···
}

因為一律會啟用按鈕,所以不需要在 Command 建構函式中指定 canExecute 引數。

只有在項目的長度大於 1,或 Entry 不等於字串 "0" 時,才會啟用 [退格鍵]Backspace 按鈕:

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 函式來移除該初始零。 只有在引數是小數點 (表示按下小數點) 和 Entry 已包含小數點時,canExecute 方法才會傳回 false。 所有 execute 方法都會呼叫 RefreshCanExecutes,後者接著會針對 DigitCommandClearCommand 這兩者呼叫 ChangeCanExecute。 這可確保小數點和退格鍵按鈕,都會根據目前的輸入數字序列啟用或停用。