在使用 Model-View-ViewModel (MVVM) 模式的 .NET 多平臺應用程式 UI (.NET MAUI) 應用程式中,資料系結會在檢視模型中的屬性 (通常是衍生自 INotifyPropertyChanged) 與檢視中的屬性 (通常是 XAML 檔案) 之間定義。 有時候,應用程式的需求會超越這些屬性繫結的範疇,需要使用者發起影響檢視模型中某些項目的命令。 這些命令通常由按鈕點擊或手指點擊來發出信號,傳統上它們會在處理程序Clicked中的代碼後置檔案中處理 事件ButtonTapped或事件TapGestureRecognizer。
命令介面提供另一種方法來實作更適合 MVVM 架構的命令。 檢視模型可以包含命令,這些命令是針對檢視中特定活動 (例如按一下) Button 而執行的方法。 在這些命令與 Button 之間定義了資料繫結。
若要允許 a Button 與 viewmodel 之間的資料繫結,定義 Button 兩個屬性:
-
Command的System.Windows.Input.ICommand類型 -
CommandParameter的Object類型
若要使用指令介面,您需要定義資料繫結,其目標是Command屬性,而Button的來源是類型為ICommand的檢視模型中的一個屬性。 檢視模型包含與按一下按鈕時執行的該 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;
}
若要使用指令介面,您的視圖模型應該包含 ICommand 類型的屬性:
public ICommand MyCommand { private set; get; }
檢視模型也必須參考實作 ICommand 介面的類別。 在檢視中,Command 的屬性被繫結到 Button 的該屬性:
<Button Text="Execute command"
Command="{Binding MyCommand}" />
當使用者按下 Button時,Button會在繫結至其Execute屬性的ICommand物件中呼叫Command方法。
當繫結第一次在Command屬性上定義時,以及當資料繫結以某種方式變更時,Button會在Button物件中呼叫CanExecute方法。 如果 CanExecute 傳回 false,則 Button 會自行停用。 這表示特定命令目前無法使用或無效。
也在Button的CanExecuteChanged事件上附加了一個處理常式ICommand。 每當影響結果的 CanExecute 條件改變時,事件必須在檢視模型內手動提出。 當該事件被引發時,Button 會再次呼叫 CanExecute 。 如果Button傳回CanExecute,則true會啟用本身;如果CanExecute傳回false,則true會停用本身。
這很重要
與某些 UI 框架(如 WPF)不同,.NET MAUI 不會自動偵測CanExecute的回傳值是否可能變動。 每當任何會影響CanExecute結果的條件改變時,你必須手動提出CanExecuteChanged事件(或呼叫ChangeCanExecute()位於Command類別上)。 這通常是在修改依賴 CanExecute 於 的屬性時進行的。
備註
您也可以使用 IsEnabled of Button 屬性來取代 CanExecute 方法,或與它結合使用。 在 .NET MAUI 7 和更早版本中,使用命令介面時無法使用 IsEnabled 的 Button 屬性,因為 CanExecute 方法的傳回值一律會覆寫 IsEnabled 屬性。 此問題已在 .NET MAUI 8 和更新版本中修正,該 IsEnabled 屬性現在可以在基於命令的 Buttons 上使用。 不過,請注意, IsEnabled 屬性和 CanExecute 方法現在 都 必須傳回 true,才能 Button 啟用 (而且也必須啟用父控制項) 。
當您的 viewmodel 定義類型 ICommand的屬性時,viewmodel 也必須包含或參照實作 ICommand 介面的類別。 此類別必須包含或參考Execute和CanExecute方法,並且每當CanExecute方法可能回傳不同值時,手動觸發CanExecuteChanged事件。 您可以使用在 .NET MAUI 中包含的 Command 或 Command<T> 類別來實作 ICommand 介面。 這些類別可讓您在類別建構函式中指定 和 ExecuteCanExecute 方法的主體。
小提示
當您使用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屬性。 此外,該類別定義了 type ICommand 的三個屬性和一個名為 Persons type 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,其 屬性繫結到 viewmodel 中的 Command 屬性;一個入口表單,其屬性繫結到屬性 NewCommand 和 IsEditing;以及兩個繫結到 viewmodel 的 PersonViewModel 和 SubmitCommand 屬性的按鈕。
ListView 顯示已輸入的人員清單:
下列螢幕擷取畫面顯示設定年齡後啟用的 [提交] 按鈕:
當使用者第一次按下 「新增 」按鈕時,這會啟用輸入表單,但會停用「 新增」 按鈕。 然後,使用者輸入姓名、年齡和技能。 在編輯過程中,使用者可以隨時按下 取消 按鈕重新開始。 只有在輸入姓名和有效年齡時,才會啟用 提交 按鈕。 按下此 提交 按鈕會將人員轉移到 所 ListView顯示的集合中。 按下 取消 或 提交 按鈕後,輸入表單將被清除,並再次啟用 新建 按鈕。
新增、提交和取消按鈕的所有邏輯處理都是透過在PersonCollectionViewModel中定義NewCommand、SubmitCommand和CancelCommand屬性來完成的。 的建構函式會將 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();
}
···
}
當使用者按一下 「新增」 按鈕時, execute 會執行傳遞給 Command 建構函式的函式。 這會建立新 PersonViewModel 物件、在該物件的 PropertyChanged 事件上設定處理常式、設定 IsEditing 為 true,並呼叫建構函式之後定義的方法 RefreshCanExecutes 。
除了實作 ICommand 介面之外,該 Command 類別還定義了一個名為 ChangeCanExecute 的方法。 你的視圖模型必須在若有任何事件可能改變CanExecute方法的返回值時,呼叫ChangeCanExecute以更新ICommand屬性。 呼叫 ChangeCanExecute 會讓 Command 該類別觸發事件 CanExecuteChanged 。 已附加該事件的處理常式,透過再次呼叫 Button 來回應,然後根據該方法的傳回值啟用自身。
當 execute 的 NewCommand 方法呼叫 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物件中發生屬性變更時,都會呼叫 for PersonViewModel 函數。 當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。
備註
不需要將 execute 和 canExecute 方法定義為 lambda 函式。 您可以在 viewmodel 中將它們寫成私有方法,並在建構函式中 Command 引用它們。 不過,此做法可能會導致大量方法在視圖模型中僅被引用一次。
使用指令參數
有時候,讓一或多個按鈕或其他使用者介面物件在檢視模型中共用相同的 ICommand 屬性是很方便的。 在此情況下,您可以使用屬性 CommandParameter 來區分按鈕。
您可以繼續將類別 Command 用於這些共用 ICommand 屬性。 該類別定義了一個替代建構函式,該 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"
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="⇦"
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屬性繫結至TextLabel屬性。 所有 Button 物件都繫結至視圖模型中的指令: ClearCommand、 BackspaceCommand和 DigitCommand。 10 位數和小數點的 11 個按鈕共同綁定至 DigitCommand。
CommandParameter 區分這些按鈕。 設定為 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 屬性通常是字串,這是泛型引數的類型。 然後,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。 這可確保根據輸入的數字的當前順序啟用或停用小數點和退格鍵。
瀏覽範例