MVVM basics and commands

The view-model pattern, ICommand and a reusable RelayCommand, and where commands stop being enough.

What MVVM actually buys you

MVVM splits a screen into a model (data and rules), a view-model (state and behaviour for one screen) and a view (XAML that binds to the view-model). The payoff is that the view-model has no reference to any control, so you can test the logic that fills a form without opening a window.

ConcernLives inMust not
Data and validation rulesModelKnow about WPF types
Screen state, commands, navigationView-modelReference a Control, Window or UserControl
Layout, styles, animation, convertersViewHold business logic
Cross-screen messagingA service or messengerUse static global mutable state
  • Push back on any Click handler that contains a decision. Handlers belong in code-behind only for view-specific concerns such as focus or a drag gesture.
  • A view-model that takes an IWindowService instead of calling MessageBox.Show is testable; one that shows dialogs directly is not.
  • Keep view-models small and per screen. A god view-model shared by the whole app reintroduces the coupling MVVM was meant to remove.

ICommand and RelayCommand

A command is the binding-friendly replacement for a click handler. It exposes Execute, CanExecute and a CanExecuteChanged event that WPF subscribes to in order to enable or disable every bound button automatically.

public sealed class RelayCommand : ICommand
{
    private readonly Action<object?> _execute;
    private readonly Predicate<object?>? _canExecute;

    public RelayCommand(Action<object?> execute, Predicate<object?>? canExecute = null)
    {
        _execute = execute ?? throw new ArgumentNullException(nameof(execute));
        _canExecute = canExecute;
    }

    public event EventHandler? CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }

    public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;

    public void Execute(object? parameter) => _execute(parameter);
}

public sealed class OrdersViewModel : ViewModelBase
{
    private string _filter = string.Empty;

    public string Filter
    {
        get => _filter;
        set { if (Set(ref _filter, value)) SaveCommand.RaiseCanExecuteChanged(); }
    }

    public ICommand SaveCommand { get; }
    public ICommand DeleteCommand { get; }

    public OrdersViewModel(IOrderService service)
    {
        SaveCommand = new RelayCommand(
            _ => service.Save(Selected),
            _ => Selected is not null && !string.IsNullOrWhiteSpace(Filter));

        DeleteCommand = new RelayCommand(p => service.Delete((Order)p!), p => p is Order);
    }
}
<Button Content="Save"
        Command="{Binding SaveCommand}" />

<Button Content="Delete"
        Command="{Binding DeleteCommand}"
        CommandParameter="{Binding SelectedItem, ElementName=OrderGrid}" />

<ListBox x:Name="OrderGrid" ItemsSource="{Binding Orders}" />
  • WPF calls CanExecute on every button bound to the command, so the enablement rule is written once in the view-model.
  • RelayCommand is not built in; write the twenty lines above or use a maintained MVVM library rather than duplicating it per project.
  • CommandParameter is how a command receives the row, the sender or the selected item it acts on.
  • Do not make CanExecute do work. It runs often, including during unrelated re-evaluation passes.

Where commands stop being enough

// Async work needs its own guard - ICommand.Execute is void
public sealed class AsyncRelayCommand : ICommand
{
    private readonly Func<Task> _run;
    private bool _running;

    public AsyncRelayCommand(Func<Task> run) => _run = run;

    public event EventHandler? CanExecuteChanged;
    public bool CanExecute(object? parameter) => !_running;

    public async void Execute(object? parameter)
    {
        if (_running) return;
        _running = true; Raise();
        try { await _run(); }
        finally { _running = false; Raise(); }
    }

    private void Raise() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

// Behaviors cover what commands cannot express
// xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
// <i:Interaction.Triggers>
//   <i:EventTrigger EventName="Loaded">
//     <i:InvokeCommandAction Command="{Binding LoadCommand}" />
//   </i:EventTrigger>
// </i:Interaction.Triggers>
You needTool
Click on a button or menu itemA command binding
React to an event with no command equivalentAn interaction trigger or a small code-behind handler
Drag and drop, focus, raw inputCode-behind — these are genuinely view concerns
Navigation between screensA navigation service behind an interface
Communication between view-modelsA messenger or event aggregator, not static state
💡
MVVM is a means, not a rule. A code-behind handler that moves focus is correct; the same handler deciding whether an order can be deleted is a design problem. Judge each handler by whether it holds a decision, not by which file it lives in.

FAQ

Do I need a framework such as Prism or CommunityToolkit.Mvvm?
Not to learn the pattern — write ViewModelBase and RelayCommand yourself once and the mechanics are clear. Once you have several screens, a library removes real boilerplate, especially for commands and dependency injection.
My CanExecute never updates. Why?
The command's CanExecuteChanged was declared but never raised. Either wire it to CommandManager.RequerySuggested as above, or call a raise method from every setter that affects the condition.

Data binding XAML and layout panels

Last refreshed 2026-09-18.