Commanding
In a .NET Multi-platform App UI (.NET MAUI) app that uses the Model-View-ViewModel (MVVM) pattern, data bindings are defined between properties in the viewmodel, which is typically a class that derives from INotifyPropertyChanged
, and properties in the view, which is typically the XAML file. Sometimes an app has needs that go beyond these property bindings by requiring the user to initiate commands that affect something in the viewmodel. These commands are generally signaled by button clicks or finger taps, and traditionally they are processed in the code-behind file in a handler for the Clicked
event of the Button or the Tapped
event of a TapGestureRecognizer.
The commanding interface provides an alternative approach to implementing commands that is much better suited to the MVVM architecture. The viewmodel can contain commands, which are methods that are executed in reaction to a specific activity in the view such as a Button click. Data bindings are defined between these commands and the Button.
To allow a data binding between a Button and a viewmodel, the Button defines two properties:
Command
of typeSystem.Windows.Input.ICommand
CommandParameter
of typeObject
To use the command interface, you define a data binding that targets the Command
property of the Button where the source is a property in the viewmodel of type ICommand. The viewmodel contains code associated with that ICommand property that is executed when the button is clicked. You can set the CommandParameter
property to arbitrary data to distinguish between multiple buttons if they are all bound to the same ICommand property in the viewmodel.
Many other views also define Command
and CommandParameter
properties. All these commands can be handled within a viewmodel using an approach that doesn't depend on the user-interface object in the view.
ICommands
The ICommand interface is defined in the System.Windows.Input namespace, and consists of two methods and one event:
public interface ICommand
{
public void Execute (Object parameter);
public bool CanExecute (Object parameter);
public event EventHandler CanExecuteChanged;
}
To use the command interface, your viewmodel should contain properties of type ICommand:
public ICommand MyCommand { private set; get; }
The viewmodel must also reference a class that implements the ICommand interface. In the view, the Command
property of a Button is bound to that property:
<Button Text="Execute command"
Command="{Binding MyCommand}" />
When the user presses the Button, the Button calls the Execute
method in the ICommand object bound to its Command
property.
When the binding is first defined on the Command
property of the Button, and when the data binding changes in some way, the Button calls the CanExecute
method in the ICommand object. If CanExecute
returns false
, then the Button disables itself. This indicates that the particular command is currently unavailable or invalid.
The Button also attaches a handler on the CanExecuteChanged
event of ICommand. The event is raised from within the viewmodel. When that event is raised, the Button calls CanExecute
again. The Button enables itself if CanExecute
returns true
and disables itself if CanExecute
returns false
.
Warning
Do not use the IsEnabled
property of Button if you're using the command interface.
When your viewmodel defines a property of type ICommand, the viewmodel must also contain or reference a class that implements the ICommand interface. This class must contain or reference the Execute
and CanExecute
methods, and fire the CanExecuteChanged
event whenever the CanExecute
method might return a different value. You can use the Command
or Command<T>
class included in .NET MAUI to implement the ICommand interface. These classes allow you to specify the bodies of the Execute
and CanExecute
methods in class constructors.
Tip
Use Command<T>
when you use the CommandParameter
property to distinguish between multiple views bound to the same ICommand property, and the Command
class when that isn't a requirement.
Basic commanding
The following examples demonstrate basic commands implemented in a viewmodel.
The PersonViewModel
class defines three properties named Name
, Age
, and Skills
that define a person:
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));
}
}
The PersonCollectionViewModel
class shown below creates new objects of type PersonViewModel
and allows the user to fill in the data. For that purpose, the class defines IsEditing
, of type bool
, and PersonEdit
, of type PersonViewModel
, properties. In addition, the class defines three properties of type ICommand and a property named Persons
of 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));
}
}
In this example, changes to the three ICommand properties and the Persons
property do not result in PropertyChanged
events being raised. These properties are all set when the class is first created and do not change.
The following example shows the XAML that consumes the 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">
<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>
In this example, the page's BindingContext
property is set to the PersonCollectionViewModel
. The Grid contains a Button with the text New with its Command
property bound to the NewCommand
property in the viewmodel, an entry form with properties bound to the IsEditing
property, as well as properties of PersonViewModel
, and two more buttons bound to the SubmitCommand
and CancelCommand
properties of the viewmodel. The ListView displays the collection of persons already entered:
The following screenshot shows the Submit button enabled after an age has been set:
When the user first presses the New button, this enables the entry form but disables the New button. The user then enters a name, age, and skills. At any time during the editing, the user can press the Cancel button to start over. Only when a name and a valid age have been entered is the Submit button enabled. Pressing this Submit button transfers the person to the collection displayed by the ListView. After either the Cancel or Submit button is pressed, the entry form is cleared and the New button is enabled again.
All the logic for the New, Submit, and Cancel buttons is handled in PersonCollectionViewModel
through definitions of the NewCommand
, SubmitCommand
, and CancelCommand
properties. The constructor of the PersonCollectionViewModel
sets these three properties to objects of type Command
.
A constructor of the Command
class allows you to pass arguments of type Action
and Func<bool>
corresponding to the Execute
and CanExecute
methods. This action and function can be defined as lambda functions in the Command
constructor:
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();
}
···
}
When the user clicks the New button, the execute
function passed to the Command
constructor is executed. This creates a new PersonViewModel
object, sets a handler on that object's PropertyChanged
event, sets IsEditing
to true
, and calls the RefreshCanExecutes
method defined after the constructor.
Besides implementing the ICommand interface, the Command
class also defines a method named ChangeCanExecute
. A viewmodel should call ChangeCanExecute
for an ICommand property whenever anything happens that might change the return value of the CanExecute
method. A call to ChangeCanExecute
causes the Command
class to fire the CanExecuteChanged
method. The Button has attached a handler for that event and responds by calling CanExecute
again, and then enabling itself based on the return value of that method.
When the execute
method of NewCommand
calls RefreshCanExecutes
, the NewCommand
property gets a call to ChangeCanExecute
, and the Button calls the canExecute
method, which now returns false
because the IsEditing
property is now true
.
The PropertyChanged
handler for the new PersonViewModel
object calls the ChangeCanExecute
method of 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;
});
···
}
···
}
The canExecute
function for SubmitCommand
is called every time there's a property changed in the PersonViewModel
object being edited. It returns true
only when the Name
property is at least one character long, and Age
is greater than 0. At that time, the Submit button becomes enabled.
The execute
function for Submit removes the property-changed handler from the PersonViewModel
, adds the object to the Persons
collection, and returns everything to its initial state.
The execute
function for the Cancel button does everything that the Submit button does except add the object to the collection:
public class PersonCollectionViewModel : INotifyPropertyChanged
{
···
public PersonCollectionViewModel()
{
···
CancelCommand = new Command(
execute: () =>
{
PersonEdit.PropertyChanged -= OnPersonEditPropertyChanged;
PersonEdit = null;
IsEditing = false;
RefreshCanExecutes();
},
canExecute: () =>
{
return IsEditing;
});
}
···
}
The canExecute
method returns true
at any time a PersonViewModel
is being edited.
Note
It isn't necessary to define the execute
and canExecute
methods as lambda functions. You can write them as private methods in the viewmodel and reference them in the Command
constructors. However, this approach can result in a lot of methods that are referenced only once in the viewmodel.
Using Command parameters
It's' sometimes convenient for one or more buttons, or other user-interface objects, to share the same ICommand property in the viewmodel. In this case, you can use the CommandParameter
property to distinguish between the buttons.
You can continue to use the Command
class for these shared ICommand properties. The class defines an alternative constructor that accepts execute
and canExecute
methods with parameters of type Object
. This is how the CommandParameter
is passed to these methods. However, when specifying a CommandParameter
, it's easiest to use the generic Command<T>
class to specify the type of the object set to CommandParameter
. The execute
and canExecute
methods that you specify have parameters of that type.
The following example demonstrates a keyboard for entering decimal numbers:
<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>
In this example, the page's BindingContext
is a DecimalKeypadViewModel
. The Entry property of this viewmodel is bound to the Text
property of a Label. All the Button objects are bound to commands in the viewmodel: ClearCommand
, BackspaceCommand
, and DigitCommand
. The 11 buttons for the 10 digits and the decimal point share a binding to DigitCommand
. The CommandParameter
distinguishes between these buttons. The value set to CommandParameter
is generally the same as the text displayed by the button except for the decimal point, which for purposes of clarity is displayed with a middle dot character:
The DecimalKeypadViewModel
defines an Entry property of type string
and three properties of type 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; }
}
The button corresponding to the ClearCommand
is always enabled and sets the entry back to "0":
public class DecimalKeypadViewModel : INotifyPropertyChanged
{
···
public DecimalKeypadViewModel()
{
ClearCommand = new Command(
execute: () =>
{
Entry = "0";
RefreshCanExecutes();
});
···
}
void RefreshCanExecutes()
{
((Command)BackspaceCommand).ChangeCanExecute();
((Command)DigitCommand).ChangeCanExecute();
}
···
}
Because the button is always enabled, it is not necessary to specify a canExecute
argument in the Command
constructor.
The Backspace button is enabled only when the length of the entry is greater than 1, or if Entry is not equal to the string "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";
});
···
}
···
}
The logic for the execute
function for the Backspace button ensures that the Entry is at least a string of "0".
The DigitCommand
property is bound to 11 buttons, each of which identifies itself with the CommandParameter
property. The DigitCommand
is set to an instance of the Command<T>
class. When using the commanding interface with XAML, the CommandParameter
properties are usually strings, which is type of the generic argument. The execute
and canExecute
functions then have arguments of type 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("."));
});
}
···
}
The execute
method appends the string argument to the Entry property. However, if the result begins with a zero (but not a zero and a decimal point) then that initial zero must be removed using the Substring
function. The canExecute
method returns false
only if the argument is the decimal point (indicating that the decimal point is being pressed) and Entry already contains a decimal point. All the execute
methods call RefreshCanExecutes
, which then calls ChangeCanExecute
for both DigitCommand
and ClearCommand
. This ensures that the decimal point and backspace buttons are enabled or disabled based on the current sequence of entered digits.