On this page:
Introduction to Commands
In the Model-View-ViewModel (MVVM) pattern, Commands are a fundamental concept for handling user input and executing actions within your application. They decouple the user interface element that triggers an action from the logic that performs the action. This promotes cleaner code, better testability, and improved maintainability.
Commands allow you to encapsulate actions as objects. This enables binding UI elements (like buttons or menu items) to these actions without direct event handler wiring in your UI code. The ViewModel then manages the command's execution and its enabled/disabled state.
The ICommand
Interface
The foundation of commands in .NET is the System.Windows.Input.ICommand
interface. This interface defines two primary members:
void Execute(object parameter)
: This method is called when the command is invoked. It contains the logic to be executed.bool CanExecute(object parameter)
: This method determines whether the command can currently be executed. It's used to enable or disable UI elements bound to the command.
The interface also includes an event, CanExecuteChanged
, which is raised when the state of the command (whether it can be executed) changes. UI elements can subscribe to this event to update their enabled/disabled status.
CanExecute
method is crucial for providing a responsive user experience. It prevents users from attempting actions that are not currently valid.
Common Implementations
While you can implement ICommand
from scratch, .NET MAUI and related libraries provide convenient base classes and implementations.
RelayCommand
The RelayCommand
(often found in community toolkit libraries like the CommunityToolkit.Mvvm) is a popular and straightforward implementation. It allows you to bind an Action
(for Execute
) and an optional Func<bool>
(for CanExecute
) to a command.
Example: Basic RelayCommand
using CommunityToolkit.Mvvm.Input;
using System.Windows.Input;
public class MyViewModel : ObservableObject
{
public ICommand IncrementCommand { get; }
private int _counter;
public int Counter
{
get => _counter;
set => SetProperty(ref _counter, value);
}
public MyViewModel()
{
// Command that increments the counter
IncrementCommand = new RelayCommand(() =>
{
Counter++;
});
// Example with CanExecute (always enabled in this case)
// IncrementCommand = new RelayCommand(() => Counter++, () => true);
}
}
AsyncRelayCommand
For asynchronous operations (e.g., network requests, database operations), AsyncRelayCommand
is the preferred choice. It simplifies handling the execution of Task
or Task<TResult>
methods and manages the command's busy state.
Example: AsyncRelayCommand
using CommunityToolkit.Mvvm.Input;
using System.Threading.Tasks;
using System.Windows.Input;
public class MyViewModel : ObservableObject
{
public ICommand LoadDataCommand { get; }
public MyViewModel()
{
LoadDataCommand = new AsyncRelayCommand(LoadDataAsync, CanLoadData);
}
private async Task LoadDataAsync()
{
// Simulate a long-running operation
await Task.Delay(2000);
// Load data...
System.Diagnostics.Debug.WriteLine("Data loaded!");
}
private bool CanLoadData()
{
// Example: Only allow loading if not already loading
return !((AsyncRelayCommand)LoadDataCommand).IsRunning;
}
}
AsyncRelayCommand
, the command is automatically disabled while the asynchronous operation is running, and CanExecuteChanged
is raised.
Binding Commands in XAML
The power of MVVM commands truly shines when you bind them to UI elements in XAML. The Command
property of many UI controls (like Button
, MenuItem
, TapGestureRecognizer
) is used for this purpose.
Example: Binding a Button to IncrementCommand
<!-- In your XAML file -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:YourApp.ViewModels"
x:Class="YourApp.MyPage">
<ContentPage.BindingContext>
<viewmodels:MyViewModel />
</ContentPage.BindingContext>
<StackLayout Padding="20" Spacing="10">
<Label Text="{Binding Counter}" HorizontalOptions="Center" VerticalOptions="Center" FontSize="Large" />
<Button Text="Increment" Command="{Binding IncrementCommand}" HorizontalOptions="Center" />
</StackLayout>
</ContentPage>
Notice how the Command="{Binding IncrementCommand}"
line directly links the button's click event to the IncrementCommand
in the ViewModel, without any code-behind in the XAML.
Command Parameters
Commands can accept parameters, which are passed from the UI element to the Execute
and CanExecute
methods. This is useful when the action needs context from the UI, such as the selected item in a list or a value from a text box.
The CommandParameter
property on UI elements is used to specify the value to be passed. If the command parameter is not explicitly set, it will be null
.
Example: Command with Parameter
// ViewModel
public ICommand SaveItemCommand { get; }
public MyViewModel()
{
SaveItemCommand = new RelayCommand<string>(itemText =>
{
// Save the itemText
System.Diagnostics.Debug.WriteLine($"Saving: {itemText}");
});
}
// XAML
<Entry x:Name="itemEntry" />
<Button Text="Save" Command="{Binding SaveItemCommand}" CommandParameter="{Binding Text, Source={x:Reference itemEntry}}" />
RelayCommand<T>
or AsyncRelayCommand<T>
, the type parameter T
dictates the expected type of the command parameter.
The CanExecute
Logic
The CanExecute
method is vital for guiding user interaction. It should return true
only when the command's associated action is valid and can be performed. The UI element bound to the command will automatically be enabled or disabled based on the return value of CanExecute
.
It's important to call CommandManager.InvalidateRequerySuggested()
(or the equivalent method provided by your command implementation, like ((RelayCommand)MyCommand).NotifyCanExecuteChanged()
) whenever the conditions affecting the command's executability change. This signals the UI to re-evaluate CanExecute
.
Example: Conditional CanExecute
public class LoginViewModel : ObservableObject
{
private string _username;
public string Username
{
get => _username;
set => SetProperty(ref _username, value, onChanged: () => LoginCommand.NotifyCanExecuteChanged());
}
private string _password;
public string Password
{
get => _password;
set => SetProperty(ref _password, value, onChanged: () => LoginCommand.NotifyCanExecuteChanged());
}
public ICommand LoginCommand { get; }
public LoginViewModel()
{
// Using a lambda that takes a parameter type for Execute
LoginCommand = new RelayCommand<object>(Login, CanLogin);
}
private void Login(object obj)
{
// Perform login logic with Username and Password
System.Diagnostics.Debug.WriteLine($"Logging in as: {Username}");
}
private bool CanLogin()
{
// Enable login only if both username and password are not empty
return !string.IsNullOrWhiteSpace(Username) && !string.IsNullOrWhiteSpace(Password);
}
}
Best Practices
- Keep ViewModels Clean: Commands should reside in your ViewModels. Avoid putting command logic directly in your code-behind files.
- Use Generic Commands When Possible: If your command always takes a parameter of a specific type, use
RelayCommand<T>
orAsyncRelayCommand<T>
for better type safety. - Handle Asynchronous Operations with Care: Always use
AsyncRelayCommand
for any operation that might take time. Ensure proper state management (e.g., showing a loading indicator). - Provide Clear
CanExecute
Logic: Make sure yourCanExecute
methods accurately reflect the current state and prevent invalid actions. - Notify State Changes: Ensure that
CanExecuteChanged
is properly invoked when the state relevant toCanExecute
changes. Frameworks like CommunityToolkit.Mvvm often handle this automatically when properties change. - Use Command Parameters Wisely: Pass only necessary data through command parameters. Avoid passing complex UI objects.