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:

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.

The 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;
    }
}
                
When using 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}}" />
                
When using generic 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