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.
| Concern | Lives in | Must not |
|---|---|---|
| Data and validation rules | Model | Know about WPF types |
| Screen state, commands, navigation | View-model | Reference a Control, Window or UserControl |
| Layout, styles, animation, converters | View | Hold business logic |
| Cross-screen messaging | A service or messenger | Use static global mutable state |
- Push back on any
Clickhandler 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
IWindowServiceinstead of callingMessageBox.Showis 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
CanExecuteon every button bound to the command, so the enablement rule is written once in the view-model. RelayCommandis not built in; write the twenty lines above or use a maintained MVVM library rather than duplicating it per project.CommandParameteris how a command receives the row, the sender or the selected item it acts on.- Do not make
CanExecutedo 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 need | Tool |
|---|---|
| Click on a button or menu item | A command binding |
| React to an event with no command equivalent | An interaction trigger or a small code-behind handler |
| Drag and drop, focus, raw input | Code-behind — these are genuinely view concerns |
| Navigation between screens | A navigation service behind an interface |
| Communication between view-models | A 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.Related
Data binding XAML and layout panels
Last refreshed 2026-09-18.