命令
在使用 Model-View-ViewModel (MVVM) 模式的 .NET 多平臺應用程式 UI (.NET MAUI) 應用程式中,數據系結會在 viewmodel 中的屬性之間定義,這通常是衍生自 INotifyPropertyChanged
的類別,而檢視中的屬性通常是 XAML 檔案。 有時候,應用程式需要超越這些屬性系結的需求,方法是要求使用者起始會影響 viewmodel 中某些事物的命令。 這些命令通常是透過按鈕點擊或手指點選發出訊號,傳統上會以 Button 的 Clicked
事件處理常式或 TapGestureRecognizer 的 Tapped
事件處理常式在程式碼後置檔案中加以處理。
命令介面可針對比較適合 MVVM 架構的命令實作,提供一種替代方法。 viewmodel 可以包含命令,這些命令是響應檢視中特定活動的方法,例如 Button 按兩下。 資料繫結會定義在這些命令與 Button 之間。
若要允許 與 viewmodel 之間的 Button 數據系結,定義 Button 兩個屬性:
- 型別
Command
的System.Windows.Input.ICommand
- 型別
CommandParameter
的Object
若要使用命令介面,您可以定義數據系結,其目標Command
為 類型 viewmodel 中來源為 屬性的 ICommand屬性Button。 viewmodel 包含與按鍵時所執行該 ICommand 屬性相關聯的程式代碼。 您可以將 屬性設定 CommandParameter
為任意數據,以區分多個按鈕是否全部系結至 ViewModel 中的相同 ICommand 屬性。
許多其他檢視也會定義 Command
和 CommandParameter
屬性。 所有這些命令都可以在 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 類別。 在檢視中, Command
的 Button 屬性會系結至該屬性:
<Button Text="Execute command"
Command="{Binding MyCommand}" />
當使用者按下 Button 時,Button 會呼叫 ICommand 物件中繫結至其 Command
屬性的 Execute
方法。
第一次在 Button 的 Command
屬性上定義繫結時,若資料繫結以某種方式變更,Button 就會呼叫 ICommand 物件中的 CanExecute
方法。 如果 CanExecute
傳回 false
,則 Button 會停用其本身。 這表示,特定命令目前無法使用或無效。
Button 也會在 ICommand 的 CanExecuteChanged
事件上附加處理常式。 事件會從 viewmodel 內引發。 引發該事件時,會 Button 再次呼叫 CanExecute
。 若 CanExecute
傳回 true
,Button 即會啟用其本身;若 CanExecute
傳回 false
,則會停用其本身。
警告
如果您要使用命令介面,請勿使用 Button 的 IsEnabled
屬性。
當您的 viewmodel 定義 類型的 ICommand屬性時,viewmodel 也必須包含或參考實作 介面的 ICommand 類別。 這個類別必須包含或參考 Execute
和 CanExecute
方法,而且只要 CanExecute
方法可能傳回不同的值時,就會引發 CanExecuteChanged
事件。 您可以使用 Command
.NET MAUI 中包含的 或 Command<T>
類別來實作 ICommand 介面。 這些類別可讓您在類別建構函式中指定 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
事件。 當類別第一次建立且不會變更時,就會設定這些屬性。
下列範例顯示取用 的 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
屬性會設定為 PersonCollectionViewModel
。 Grid包含Button具有 New 文字的 ,其Command
屬性系結至 viewmodel 中的 屬性、屬性系結至 IsEditing
NewCommand
屬性的項目窗體,以及 PersonViewModel
和 系結至 SubmitCommand
viewmodel 和 CancelCommand
屬性的兩個按鈕。 會顯示 ListView 已輸入的人員集合:
下列螢幕快照顯示設定年齡後啟用的 [提交 ] 按鈕:
當使用者第一次按下 [ 新增 ] 按鈕時,這會啟用輸入窗體,但會停用 [ 新增 ] 按鈕。 使用者接著輸入姓名、年齡和技能。 在編輯期間的任何時間,使用者可以按下 Cancel (取消) 按鈕,以便從頭開始。 只有在已輸入姓名和有效的年齡時,才會啟用 Submit (提交) 按鈕。 按下此 Submit 按鈕,就會將人員資料傳送到 ListView 所顯示的集合。 按下 Cancel 或 Submit 按鈕之後,系統會清除項目表單,並再次啟用 New 按鈕。
[新增]、[提交] 和 [取消] 按鈕的所有邏輯是透過 NewCommand
、SubmitCommand
和 CancelCommand
屬性的定義在 PersonCollectionViewModel
中加以處理。 PersonCollectionViewModel
的建構函式會將這三個屬性設定為 Command
類型的物件。
類別的 Command
建構函式可讓您傳遞 型 Action
別的自變數,並 Func<bool>
對應至 Execute
和 CanExecute
方法。 此動作與函式可以在建構函式中 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 應該呼叫 ChangeCanExecute
ICommand 屬性。 呼叫 ChangeCanExecute
會導致 Command
類別引發 CanExecuteChanged
方法。 Button 已附加該事件的處理常式,並透過再次呼叫 CanExecute
,然後根據該方法的傳回值啟用其本身來回應。
當 NewCommand
的 execute
方法呼叫 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;
});
···
}
···
}
每次所編輯的 PersonViewModel
物件有屬性變更時,即會呼叫 SubmitCommand
的 canExecute
函式。 只有在 Name
屬性長度至少是 1 個字元,且 Age
大於 0 時,它才會傳回 true
。 此時,[提交] 按鈕會變成啟用狀態。
Submit 的execute
函式會從 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
。
注意
不需要將 execute
和 canExecute
方法定義為 Lambda 函式。 您可以將它們撰寫為 viewmodel 中的私用方法,並在建構函式中 Command
參考它們。 不過,此方法可能會導致許多方法在 viewmodel 中只參考一次。
使用命令參數
有時候,一或多個按鈕或其他使用者介面物件在 viewmodel 中共用相同 ICommand 屬性會很方便。 在此情況下,您可以使用 CommandParameter
屬性來區分按鈕。
您可以針對這些共用的 ICommand 屬性繼續使用 Command
類別。 類別會定義替代建構函式,這個建構函式會接受 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">
<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="⇦"
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="·"
Grid.Row="5" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="." />
</Grid>
</ContentPage>
在這裡範例中,頁面的 BindingContext
是 DecimalKeypadViewModel
。 Entry這個 viewmodel 的 屬性會系結至 Text
的 Label屬性。 Button所有物件都會繫結至 viewmodel 中的命令:ClearCommand
、 BackspaceCommand
和 DigitCommand
。 10 個數字和小數點的 11 個按鈕共用與 DigitCommand
的繫結。 CommandParameter
可區分這些按鈕。 設定為 CommandParameter
的值通常與按鈕所顯示的文字相同,但小數點除外,為了清楚起見,會以中間點字元顯示:
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
屬性通常是字串,也就是泛型自變數的類型。 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
函式來移除該初始零。 只有在引數是小數點 (表示按下小數點) 和 Entry 已包含小數點時,canExecute
方法才會傳回 false
。 所有 execute
方法都會呼叫 RefreshCanExecutes
,後者接著會針對 DigitCommand
和 ClearCommand
這兩者呼叫 ChangeCanExecute
。 這可確保小數點和退格鍵按鈕,都會根據目前的輸入數字序列啟用或停用。