Commands
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
A command separates what the user wants to do ("delete this book") from how the request was made (a button, a
menu item, the Delete key, a double click) and from the code that carries it out. It also answers a second question that
a plain Click event cannot: is the action available right now? A command that cannot run disables every button and
menu item bound to it, with no code at the call sites. This page was written against .NET 10 LTS (latest patch
10.0.13) with C# 14, Visual Studio 2026 18.11.1, Windows App SDK 2.5.1 and CommunityToolkit.Mvvm 8.4.2
on Windows 10 and Windows 11. .NET 11 is at release candidate 2 (11.0.0-rc.2) and general availability is
expected at .NET Conf, 10 to 13 November 2026; nothing in the WPF commanding model is listed as changed for it at the
time of writing. WPF is the stack in focus; WinUI 3 and Windows Forms are compared in a table further down.
The running scenario is Bookshelf Desktop, with a BooksViewModel, a BookViewModel, an IBookRepository and an
IDialogService registered in the Generic Host. The page builds the main window’s actions twice, because WPF has two
command families that solve different problems:
-
Routed commands (
RoutedCommand,RoutedUICommand) are view-level commands. They travel through the element tree, so a single "Copy" menu item works on whichever text box has the focus. This is the WPF-specific mechanism. -
View-model commands are plain
ICommandobjects exposed by a view model and bound with{Binding}. They are the MVVM way, and they are the same idea on WPF, WinUI 3, .NET MAUI and Avalonia.
Most Bookshelf Desktop actions ("add a book", "delete the selected book") are view-model commands, and a handful ("copy", "paste", "focus the search box", "close the window") are routed commands. The last sections show how the two meet. The MVVM pattern itself is on the page titled The MVVM Pattern, the generated commands in depth on MVVM Toolkit (CommunityToolkit.Mvvm), and the hand-written equivalent on MVVM from Scratch; this page concentrates on the WPF command infrastructure and on how to use view-model commands correctly from XAML. Routed events, the machinery under routed commands, are covered on Routed Events & Input, and binding syntax on Data Binding.
The ICommand contract
Every command in WPF, routed or not, implements System.Windows.Input.ICommand:
| Member | Meaning |
|---|---|
|
Whether the action is available now. A command source (a button, a menu item) calls it to decide whether it is enabled. |
|
Performs the action. The source calls it when the user invokes it. |
|
Tells the sources that the answer of |
The contract has no notion of who the command acts on: the optional parameter is the only data that flows from the
source to the command. Two properties of the contract matter in practice:
-
Sources do not poll. A
ButtoncallsCanExecuteonce when the command is assigned, and again only when it receivesCanExecuteChanged. A command that forgets to raise the event leaves its buttons enabled or disabled forever, which is the most common command bug. -
Sources subscribe to the event. A long-lived command that holds a strong reference to a short-lived source through the event keeps that source alive. WPF avoids the problem for routed commands (see CommandManager below); a hand-written command needs the same care.
A source that has a command also has an ICommandSource shape, which adds two properties:
| Property | Meaning |
|---|---|
|
The |
|
An arbitrary object passed to |
|
The element a routed command runs on. It is ignored for any command that is not a |
The classes that implement ICommandSource are ButtonBase (so Button, ToggleButton, CheckBox, RadioButton,
RepeatButton), MenuItem, Hyperlink and InputBinding (so KeyBinding and MouseBinding). Anything else, a
ListBoxItem or a Border for example, has no Command property; invoking a command from it needs an InputBinding
or a behavior.
Source: Commanding Overview
Routed commands
A RoutedCommand is WPF’s implementation of ICommand, and it is built on the opposite idea to most ICommand
implementations: it contains no logic at all. Its Execute and CanExecute methods raise routed events that
tunnel and bubble through the element tree from the command target until some element has a CommandBinding for that
command; the handlers of the CommandBinding do the work. The command object is only an identity ("Copy", "Save") plus
its default input gestures.
This indirection is the point. ApplicationCommands.Copy is one object, but a TextBox answers it with the selected
text, an image viewer would answer it with the picture, and the same menu item works for both depending on where the
focus is.
The four concepts of the model are:
| Concept | WPF type | Bookshelf Desktop example |
|---|---|---|
Command |
|
|
Command source |
|
The Edit menu’s "Copy" item, or Ctrl+E. |
Command target |
Any |
The search |
Command binding |
|
The |
Execute and CanExecute are routed events
RoutedCommand.Execute(parameter, target) raises PreviewExecuted (tunnelling) and Executed (bubbling) on the
command target. RoutedCommand.CanExecute(parameter, target) raises PreviewCanExecute and CanExecute in the same
way. The first CommandBinding for the command that is found on the route handles the event; a handler that sets
CanExecute to true enables the command, and a handler that sets Handled stops the search. Because the route starts
at the target and walks up, a CommandBinding on an ancestor of the target is reachable, and a CommandBinding on a
descendant is not.
The figure shows the two halves of a routed command. The question "can it run?" is asked first and answered by whichever element on the route has a handler; the answer enables or disables the source. Only when the user invokes the source does the second half run.
Source: Commanding Overview
The command target
A routed command needs a place to start routing. The rule from the documentation is: if CommandTarget is set on the
source, that element is the target; otherwise the element with keyboard focus is the target. That is why one "Paste"
menu item can paste into a TextBox or a PasswordBox depending on where the cursor is. It also explains the most
frequent surprise: a Button that is clicked takes the focus itself, so a "Paste" button placed next to a text box
would test its own route and find nothing. Menu and ToolBar avoid this, because they establish a focus scope and
the command is routed from the element that had the keyboard focus before the menu was used. A button that must act on a
particular element sets CommandTarget, or sits inside a container with FocusManager.IsFocusScope="True".
RoutedCommand and RoutedUICommand
| Aspect | RoutedCommand |
RoutedUICommand |
|---|---|---|
Base |
Implements |
Derives from |
Identity |
|
The same, plus a |
Gestures |
|
The same. |
Menu behavior |
A |
A |
Typical use |
Internal, non-visible actions. |
Anything that appears in a menu or toolbar. All the built-in commands are |
The command library classes expose static RoutedUICommand fields. Their logic is the responsibility of whoever handles
them, exactly as described above.
| Class | Examples | Notes |
|---|---|---|
|
|
The everyday application verbs. Most have a default gesture such as Ctrl+S, Ctrl+C or F1. |
|
|
In |
|
|
Handled by |
|
|
Media keys on the keyboard map to these gestures. |
|
|
Low-level navigation, rarely used from application code. |
In XAML the class name can usually be omitted: Command="Copy" is the same as Command="ApplicationCommands.Copy",
because the XAML processor has a type converter for commands. Prefer the long form for commands that are not in
ApplicationCommands, and use {x:Static} for your own commands.
Source: Commanding Overview
Built-in commands in the Bookshelf Desktop menu
The first example uses only built-in commands. The search box handles Cut, Copy, Paste, Undo, Redo and Select All
natively (through class command bindings registered by the text editing infrastructure), so the Edit menu needs no code.
The Exit item uses ApplicationCommands.Close, which has no built-in handler: the window supplies one.
<Window x:Class="Bookshelf.Desktop.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cmd="clr-namespace:Bookshelf.Desktop.Commands"
Title="Bookshelf Desktop" Width="900" Height="560">
<Window.CommandBindings>
<CommandBinding Command="ApplicationCommands.Close"
Executed="CloseExecuted" />
<CommandBinding Command="{x:Static cmd:BookshelfCommands.FocusSearch}"
Executed="FocusSearchExecuted" />
</Window.CommandBindings>
<DockPanel>
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem Header="_New book"
Command="{Binding AddBookCommand}"
InputGestureText="Ctrl+N" />
<Separator />
<MenuItem Command="ApplicationCommands.Close"
Header="E_xit" />
</MenuItem>
<MenuItem Header="_Edit">
<MenuItem Command="ApplicationCommands.Undo" />
<MenuItem Command="ApplicationCommands.Redo" />
<Separator />
<MenuItem Command="ApplicationCommands.Cut" />
<MenuItem Command="ApplicationCommands.Copy" />
<MenuItem Command="ApplicationCommands.Paste" />
<MenuItem Command="ApplicationCommands.SelectAll" />
<Separator />
<MenuItem Command="{x:Static cmd:BookshelfCommands.FocusSearch}" />
</MenuItem>
</Menu>
<TextBox x:Name="SearchBox" DockPanel.Dock="Top" Margin="8"
Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}" />
<!-- the list and details panes are shown in the next sections -->
</DockPanel>
</Window>
The Cut, Copy and Paste items have no Header and no Click handler: they show the localized Text of the command
("Cut", "Copy", "Paste") and the gesture text (Ctrl+X, Ctrl+C, Ctrl+V), and they are enabled only when the focused
control can run the command (Copy is disabled when nothing is selected). The File > New book item is a view-model
command, and because a view-model command has no InputGestures the gesture text is typed by hand.
Source: Commanding Overview
Writing a CommandBinding and a custom RoutedUICommand
A command that is purely about the view, such as moving the focus to the search box, belongs to the window, not to the
view model. It is a good fit for a custom RoutedUICommand with a CommandBinding in the window’s code-behind.
using System.Windows.Input;
namespace Bookshelf.Desktop.Commands;
/// <summary>View-level commands of the Bookshelf Desktop main window.</summary>
public static class BookshelfCommands
{
public static readonly RoutedUICommand FocusSearch = new(
text: "Focus search",
name: nameof(FocusSearch),
ownerType: typeof(BookshelfCommands),
inputGestures: new InputGestureCollection
{
new KeyGesture(Key.E, ModifierKeys.Control, "Ctrl+E")
});
}
using System.Windows;
using System.Windows.Input;
using Bookshelf.Desktop.ViewModels;
namespace Bookshelf.Desktop;
public partial class MainWindow : Window
{
public MainWindow(BooksViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
private void CloseExecuted(object sender, ExecutedRoutedEventArgs e) => Close();
private void FocusSearchExecuted(object sender, ExecutedRoutedEventArgs e)
{
SearchBox.Focus();
SearchBox.SelectAll();
e.Handled = true;
}
}
CommandBinding also has CanExecute handlers. A binding with only an Executed handler is always enabled, which is
what the two bindings above want. A handler that decides availability looks like this:
private void SaveCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// CanExecute is evaluated often (see CommandManager below): keep this cheap.
e.CanExecute = DataContext is BooksViewModel { SelectedBook: not null };
e.Handled = true;
}
The CanExecuteRoutedEventArgs and ExecutedRoutedEventArgs classes expose Command, Parameter and Source;
CanExecuteRoutedEventArgs also has ContinueRouting, which lets a handler report "no" yet still allow ancestors to
override the answer.
A CommandBinding can be attached at three scopes: on an element (UIElement.CommandBindings), on every instance of a
class (CommandManager.RegisterClassCommandBinding, the mechanism TextBox uses), or through the CommandManager
attached events. Application-wide commands usually go on the root Window.
Sources: Create a RoutedCommand, Commanding Overview
Input bindings and key gestures
An InputBinding ties an InputGesture to a command, which turns a key press or a mouse action into a command
invocation. KeyBinding uses a KeyGesture, and MouseBinding a MouseGesture. Because InputBinding is itself an
ICommandSource, it has the same Command, CommandParameter and CommandTarget as a button.
| Member | Notes |
|---|---|
|
Separate attributes: |
|
A single attribute with a string converted by |
|
|
|
Same meaning as on a button. |
There are two places to attach gestures. UIElement.InputBindings attaches the gesture to an element, and it fires when
the element or a descendant has the focus. RoutedCommand.InputGestures attaches the gesture to the command itself,
which makes it work wherever a CommandBinding for the command is reachable. The built-in commands use the second form:
that is why CommandBinding Command="ApplicationCommands.Open" alone makes Ctrl+O work.
<ListView x:Name="BooksList"
ItemsSource="{Binding Books}"
SelectedItem="{Binding SelectedBook}">
<ListView.InputBindings>
<KeyBinding Key="Delete" Command="{Binding DeleteSelectedCommand}" />
<KeyBinding Key="Return" Command="{Binding EditSelectedCommand}" />
<MouseBinding MouseAction="LeftDoubleClick"
Command="{Binding EditSelectedCommand}" />
</ListView.InputBindings>
<!-- columns shown in the next section -->
</ListView>
and on the window, the application-wide shortcuts:
<Window.InputBindings>
<KeyBinding Key="N" Modifiers="Control" Command="{Binding AddBookCommand}" />
<KeyBinding Gesture="F5" Command="{Binding LoadCommand}" />
</Window.InputBindings>
InputBinding is a Freezable, and Freezable objects take part in DataContext inheritance in WPF, which is why
{Binding} works inside InputBindings even though a KeyBinding is not a visual element. A CommandBinding has no
such support (it is not a dependency object), so it cannot bind its handlers: the Executed attribute takes an event
handler name only.
Source: Commanding Overview
Command invalidation and CommandManager
How does a menu item become disabled when the selection changes, if the RoutedCommand has no logic of its own? The
answer is CommandManager, a static class that centralizes the command infrastructure:
-
it hosts the attached events
Executed,CanExecute,PreviewExecutedandPreviewCanExecute; -
it registers class-level
CommandBindingandInputBindingobjects (RegisterClassCommandBinding,RegisterClassInputBinding); -
it raises
RequerySuggestedwhenever it detects a change that might affect command state, such as a change in focus, a key or mouse event, or a selection change, andRoutedCommand.CanExecuteChangedis wired to it; -
CommandManager.InvalidateRequerySuggested()forces that event for conditions the manager cannot see.
So routed commands do not need to raise CanExecuteChanged themselves: after almost any user input WPF asks every
visible source to evaluate again. This convenience has a cost, and it is why a CanExecute handler must be cheap: it
runs after every focus change and many input events, once per visible command source.
A hand-written ICommand
The same trick can be used by a plain ICommand: forwarding CanExecuteChanged to CommandManager.RequerySuggested
gives automatic re-evaluation. This is a community pattern; the documentation describes RequerySuggested but does not
show a view-model command built this way. MVVM from Scratch develops it in full, and the compact version below shows
the shape.
using System.Windows.Input;
namespace Bookshelf.Desktop.Commands;
/// <summary>Hand-written command that re-queries after any user input through CommandManager.</summary>
public sealed class SimpleCommand(Action<object?> execute, Predicate<object?>? canExecute = null) : ICommand
{
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);
/// <summary>Asks WPF to re-evaluate every command now, not after the next input.</summary>
public static void Invalidate() => CommandManager.InvalidateRequerySuggested();
}
Two properties of this design deserve a warning:
-
RequerySuggestedholds its handlers through weak references. A command source subscribed through aSimpleCommandstays alive on its own, so that is fine, but code that subscribes an anonymous lambda directly toRequerySuggestedcan find the handler garbage collected and never called again. -
CommandManagerlives inPresentationCore, so a view-model library that references it is tied to WPF. TheICommandimplementations of the MVVM Toolkit do not useCommandManager; they raiseCanExecuteChangedexplicitly, which is why the library works unchanged on WinUI 3, MAUI and Avalonia.
Source: CommandManager.RequerySuggested
View-model commands
A view-model command is an ICommand property on a view model. The view binds a Button.Command to it, and the
view model keeps the logic, the availability rule and the state together, with no reference to any window, which makes
it unit-testable. Four things distinguish it from a routed command:
| Aspect | Routed command | View-model command |
|---|---|---|
Where the logic lives |
In a |
In the command itself (a delegate to a view-model method). |
Who is the target? |
The element with the focus, or |
The view model; there is no routing and |
How availability is refreshed |
Automatically after input, through |
Explicitly, when the view model state changes ( |
Reusable on other stacks |
No. WPF-specific. |
Yes. |
Good for |
View concerns: clipboard, editing, focus, closing a window, navigation. |
Application actions: add, edit, delete, save, refresh, search. |
The MVVM Toolkit RelayCommand attribute
CommunityToolkit.Mvvm generates command properties from methods. Annotating a method with [RelayCommand] in a
partial class adds a property of type IRelayCommand (or IRelayCommand<T> when the method takes a parameter, or
IAsyncRelayCommand when it returns a Task). The name is the method name plus Command, with an Async suffix and an
On prefix removed: AddBook becomes AddBookCommand, and LoadAsync becomes LoadCommand.
| Attribute option | Meaning |
|---|---|
|
A method or property returning |
|
For async commands. |
|
For async commands. |
|
For async commands taking a |
|
Forwards the listed attributes to the generated property. |
A method with a CancellationToken parameter makes the generated command cancellable: calling
IAsyncRelayCommand.Cancel() signals that token. Availability is re-evaluated when NotifyCanExecuteChanged() is
called on the command, and the [NotifyCanExecuteChangedFor] attribute on an [ObservableProperty] calls it for you
whenever the property changes.
Sources: RelayCommand attribute, AsyncRelayCommand
BooksViewModel with commands
The view model below uses the canonical Bookshelf Desktop shapes of the collaborators, which the pages on the MVVM Toolkit and on Navigation, Dialogs & Services in MVVM develop in full.
namespace Bookshelf.Core;
public sealed record Book(int Id, string Title, string Author, int Year = 0, decimal Price = 0m, string? Isbn = null);
public interface IBookRepository
{
Task<IReadOnlyList<Book>> GetAllAsync(CancellationToken cancellationToken);
Task<Book?> GetByIdAsync(int id, CancellationToken cancellationToken);
Task<Book> AddAsync(Book book, CancellationToken cancellationToken);
Task UpdateAsync(Book book, CancellationToken cancellationToken);
Task DeleteAsync(int id, CancellationToken cancellationToken);
}
namespace Bookshelf.Desktop;
public interface IDialogService
{
Task<bool> ConfirmAsync(string title, string message);
/// <summary>Shows the add/edit dialog. Returns null when the user cancels.</summary>
Task<Book?> EditBookAsync(Book? existing);
}
using Bookshelf.Core;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Bookshelf.Desktop.ViewModels;
public partial class BookViewModel(Book book) : ObservableObject
{
public Book Model { get; } = book;
public int Id => Model.Id;
public string Title => Model.Title;
public string Author => Model.Author;
[ObservableProperty]
public partial bool IsFavorite { get; set; }
}
using System.Collections.ObjectModel;
using Bookshelf.Core;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Bookshelf.Desktop.ViewModels;
public partial class BooksViewModel(IBookRepository repository, IDialogService dialogs) : ObservableObject
{
public ObservableCollection<BookViewModel> Books { get; } = [];
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(EditSelectedCommand))]
[NotifyCanExecuteChangedFor(nameof(DeleteSelectedCommand))]
public partial BookViewModel? SelectedBook { get; set; }
[ObservableProperty]
public partial string? SearchText { get; set; }
[ObservableProperty]
public partial string? StatusMessage { get; set; }
// Async command with cancellation: the generated property is LoadCommand (IAsyncRelayCommand).
[RelayCommand(IncludeCancelCommand = true)]
private async Task LoadAsync(CancellationToken cancellationToken)
{
try
{
StatusMessage = "Loading books...";
var books = await repository.GetAllAsync(cancellationToken);
Books.Clear();
foreach (var book in books)
{
Books.Add(new BookViewModel(book));
}
StatusMessage = $"{Books.Count} books";
}
catch (OperationCanceledException)
{
StatusMessage = "Loading cancelled";
}
}
// Synchronous-looking command that awaits a dialog: AddBookCommand.
[RelayCommand]
private async Task AddBookAsync()
{
var created = await dialogs.EditBookAsync(existing: null);
if (created is null)
{
return;
}
await repository.AddAsync(created, CancellationToken.None);
await LoadCommand.ExecuteAsync(null);
}
private bool HasSelection() => SelectedBook is not null;
// CanExecute follows SelectedBook through [NotifyCanExecuteChangedFor] above.
[RelayCommand(CanExecute = nameof(HasSelection))]
private async Task EditSelectedAsync()
{
var edited = await dialogs.EditBookAsync(SelectedBook!.Model);
if (edited is null)
{
return;
}
await repository.UpdateAsync(edited, CancellationToken.None);
await LoadCommand.ExecuteAsync(null);
}
[RelayCommand(CanExecute = nameof(HasSelection))]
private async Task DeleteSelectedAsync()
{
var book = SelectedBook!;
if (!await dialogs.ConfirmAsync("Delete book", $"Delete \"{book.Title}\"?"))
{
return;
}
await repository.DeleteAsync(book.Id, CancellationToken.None);
Books.Remove(book);
StatusMessage = $"Deleted {book.Title}";
}
// Command with a parameter: the generated property is IRelayCommand<BookViewModel>.
[RelayCommand]
private void ToggleFavorite(BookViewModel? book)
{
if (book is not null)
{
book.IsFavorite = !book.IsFavorite;
}
}
[RelayCommand]
private void ClearSearch() => SearchText = null;
}
Source: RelayCommand attribute
LoadCommand.ExecuteAsync(null) is a member of IAsyncRelayCommand: it returns the task, so other code can await the
whole load. Calling LoadCommand.Execute(null) from code would start it without awaiting. The class is partial
because the source generator adds the command properties in a second part of the type; a nested class requires every
containing type to be partial too.
Binding the commands in XAML
The list, its buttons and a busy indicator complete the main window. The buttons bind to the view model’s commands; the
progress bar uses the IsRunning state of the async command, which is a property of the command object itself.
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisibility" />
</Window.Resources>
<!-- inside the DockPanel -->
<ToolBar DockPanel.Dock="Top">
<Button Content="Add" Command="{Binding AddBookCommand}" />
<Button Content="Edit" Command="{Binding EditSelectedCommand}" />
<Button Content="Delete" Command="{Binding DeleteSelectedCommand}" />
<Separator />
<Button Content="Refresh" Command="{Binding LoadCommand}" />
<Button Content="Cancel" Command="{Binding LoadCancelCommand}" />
<Separator />
<Button Command="ApplicationCommands.Paste" Content="Paste" />
</ToolBar>
<StatusBar DockPanel.Dock="Bottom">
<TextBlock Text="{Binding StatusMessage}" />
<ProgressBar Width="120" Height="12" IsIndeterminate="True"
Visibility="{Binding LoadCommand.IsRunning,
Converter={StaticResource BoolToVisibility}}" />
</StatusBar>
<ListView x:Name="BooksList" ItemsSource="{Binding Books}"
SelectedItem="{Binding SelectedBook}">
<ListView.View>
<GridView>
<GridViewColumn Header="Title" Width="300" DisplayMemberBinding="{Binding Title}" />
<GridViewColumn Header="Author" Width="200" DisplayMemberBinding="{Binding Author}" />
<GridViewColumn Header="Favorite" Width="90">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Button Content="{Binding IsFavorite}"
Command="{Binding DataContext.ToggleFavoriteCommand,
RelativeSource={RelativeSource AncestorType=ListView}}"
CommandParameter="{Binding}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
Source: RelayCommand attribute
Notes on the markup:
-
The "Cancel" button binds to the cancel command generated by
IncludeCancelCommand = true. The documentation says a property named after the method with aCancelCommandending is produced (DoWorkCancelCommandforDoWorkAsync); forLoadAsyncthat isLoadCancelCommand. The cancel command is disabled while no load is running. -
Inside a
DataTemplatetheDataContextis the row (aBookViewModel), so the button reaches the parent view model’s command with aRelativeSourcebinding to theDataContextof theListView, and passes the row withCommandParameter="{Binding}". This is the standard way to put a command from the parent on every row. -
The "Paste" button sits inside a
ToolBar, which is a focus scope, so the routedPastecommand still targets the searchTextBox. A toolbar button and a menu item are the usual places to mix routed and view-model commands.
Passing a parameter
CommandParameter is the second input of CanExecute and Execute. It is object?, and several rules follow from
that:
-
A generic command (
IRelayCommand<T>) casts the parameter toT; a wrong type throws an exception at the moment the source evaluates or runs the command, so keep theCommandParameterbinding and the parameter type in step. -
Declare the parameter as nullable (
BookViewModel?). WPF evaluatesCanExecutebefore every binding has produced a value, so the command is asked withnullfirst; code that dereferences the parameter without a check throws on start-up. -
CommandParameterbelongs to the source, not to the command. Two buttons can run the same command with different parameters, which is what makes "delete this row" possible with one command per list. -
A routed command receives the parameter as
e.Parameterin its handlers.
Routed and view-model commands together
The two families meet at the window. Three patterns cover almost every case, and the choice depends on whether the action needs the view model.
| Pattern | When | How |
|---|---|---|
Bind the source to a view-model command |
Application actions: add, edit, delete, refresh. |
|
Handle a routed command in the view |
View concerns: focus, clipboard, closing, zoom. |
A |
Forward a routed command to the view model |
A standard command should act on the model: for example |
A |
using System.Windows;
using System.Windows.Input;
using Bookshelf.Desktop.ViewModels;
namespace Bookshelf.Desktop;
public partial class MainWindow
{
// Registered once, after InitializeComponent: the standard Delete command (the Delete key,
// an Edit > Delete menu item) is routed to BooksViewModel.DeleteSelectedCommand.
private void ForwardDeleteToViewModel()
{
CommandBindings.Add(new CommandBinding(
ApplicationCommands.Delete,
executed: (_, e) =>
{
if (DataContext is BooksViewModel { DeleteSelectedCommand: var delete })
{
delete.Execute(null);
e.Handled = true;
}
},
canExecute: (_, e) =>
{
e.CanExecute = DataContext is BooksViewModel { DeleteSelectedCommand: var delete }
&& delete.CanExecute(null);
e.Handled = true;
}));
}
}
Source: Commanding Overview
The forwarding handler is a few lines, but it is a design decision: it makes the standard routed command (with its
gesture and its menu text) the public face, and the view model command the implementation. A user pattern that keeps
the view free of logic is to leave MainWindow with only FocusSearch and Close, and bind everything else directly.
There is no official WPF API that binds a CommandBinding to a view-model command in XAML. Libraries and behavior
packages exist for it, but they are not part of WPF, and Microsoft Learn does not document one.
Testing the commands
View-model commands are plain objects, so a unit test needs no window and no dispatcher. The example uses xUnit and NSubstitute for brevity, and any test framework and mocking library works.
using Bookshelf.Core;
using Bookshelf.Desktop;
using Bookshelf.Desktop.ViewModels;
using NSubstitute;
using Xunit;
namespace Bookshelf.Desktop.Tests;
public class BooksViewModelCommandTests
{
[Fact]
public void DeleteSelected_is_disabled_until_a_book_is_selected()
{
var viewModel = new BooksViewModel(
Substitute.For<IBookRepository>(),
Substitute.For<IDialogService>());
var raised = 0;
viewModel.DeleteSelectedCommand.CanExecuteChanged += (_, _) => raised++;
Assert.False(viewModel.DeleteSelectedCommand.CanExecute(null));
viewModel.SelectedBook = new BookViewModel(new Book(1, "Dune", "Frank Herbert"));
Assert.True(viewModel.DeleteSelectedCommand.CanExecute(null));
Assert.Equal(1, raised);
}
}
Source: RelayCommand attribute
Commands across the Windows UI stacks
| Concern | WPF | WinUI 3 | Windows Forms |
|---|---|---|---|
|
|
|
|
Routed commands and |
Yes: |
No. There is no routed-command mechanism and no |
No. |
Reusable command objects with label, icon and shortcut |
|
|
None built in. |
Keyboard shortcuts |
|
|
|
|
Automatic for routed commands, through |
Explicit: |
Explicit: |
MVVM Toolkit commands |
Fully supported. |
Fully supported. |
Possible with the command properties above, or plain event handlers. |
On WinUI 3 the closest equivalent of a library routed command is StandardUICommand, which supplies the label, icon and
accelerator of a platform command (Delete, Save, Copy and so on) and still needs your handler. The ICommand portion of
this page, and the whole View-model commands section, carry over to WinUI 3 unchanged, because the MVVM Toolkit does
not depend on a UI framework; the NavigationView, x:Bind and window model differ, and are covered on the WinUI 3
pages.
From .NET Framework to .NET 8, 9 and 10
The WPF commanding model is the one from .NET Framework 3.0: ICommand, RoutedCommand, CommandBinding,
CommandManager and the command library are present in .NET 10 with the same behavior, and the Learn overview has not
been rewritten for modern .NET. What changed around it:
| Change | Effect on commands |
|---|---|
Nullable reference types |
|
Built-in Fluent theme (.NET 9 and later, experimental) |
Restyles buttons and menus, including disabled and focus visuals. The command model is not affected. |
Source generators |
|
MVVM Toolkit 8.x and C# 14 |
|
.NET 11 (RC2) |
The WPF release notes list bug fixes only; nothing new for commands at the time of writing. |
Pitfalls, performance and security
-
A command that never raises
CanExecuteChanged. The classic bug: the selection changes and the Delete button stays gray. With a toolkit command, callNotifyCanExecuteChanged()or put[NotifyCanExecuteChangedFor]on the property that drives the rule. With a hand-written command, raise the event or route it throughCommandManager.RequerySuggested. -
Expensive
CanExecute. With routed commands and any command wired toRequerySuggested, the method runs after nearly every input event for every visible source. Compare cheap flags, never query a database or the file system. -
async voidcommands. A hand-writtenExecutethat isasync voidloses exceptions and cannot be awaited or tested. UseIAsyncRelayCommand(or[RelayCommand]on aTaskmethod) and catch the exceptions you can handle. -
Unhandled exceptions in async commands. By default the toolkit rethrows on the synchronization context, so an exception in
DeleteSelectedAsyncreaches the dispatcher’s unhandled-exception path and can close the app. Catch and report failures inside the command, or setFlowExceptionsToTaskScheduleron purpose. -
Disabled while running. An async command is disabled during execution by default, which is usually what is wanted (it prevents a double click from deleting twice).
AllowConcurrentExecutions = trueremoves that guard. -
Wrong command target. A button outside a focus scope that runs a routed command tests its own route and finds no binding. Use a
MenuorToolBar,FocusManager.IsFocusScope, or an explicitCommandTarget. -
CommandBindingplacement. ACommandBindingon a descendant of the target is never reached. Put it on an ancestor, normally the window. -
Binding to the wrong
DataContext. Inside a template,{Binding DeleteCommand}resolves against the row. Reach the parent’s command with aRelativeSourceAncestorTypebinding. AContextMenuis not part of the visual tree of its owner and needsPlacementTargetin the path; the DataContext of the menu is inherited from the target. -
Single-key
KeyBindinginside editors. AKeyBindingwith no modifier on an ancestor of aTextBoxcan steal typing; give shortcuts a modifier, or put the binding on the list rather than on the window. -
Leaks through
CanExecuteChanged. A long-lived command object holding sources by strong handlers keeps closed windows alive. Prefer the toolkit’s commands, and avoid static command instances that sources subscribe to without unsubscribing. -
CommandManagerin a shared library. It ties the library to WPF. KeepICommandimplementations in view model projects free of it. -
Disabled is not secured. Disabling a button is a usability measure. Authorization of "delete" must be enforced in the repository or service, not by the
CanExecuteof a command. -
Accessibility. A command source gets its name from its content; icon-only buttons need
AutomationProperties.Name. Keyboard shortcuts should be visible in the menu text, which aRoutedUICommandprovides for free.
What Microsoft Learn does not provide
-
The WPF commanding documentation describes the routed model and the library, but it has no guidance on view-model commands in WPF. The
Bindingto anICommand, theRelativeSourcepattern for rows and the forwarding of a routed command to a view model are common practice, not documented recommendations. -
There is no official
RelayCommandorDelegateCommandin WPF itself. The documented one is in the CommunityToolkit.Mvvm package, which is a separate library. -
There is no documented way to bind the handlers of a
CommandBindingto a view model in XAML. -
Wiring a custom
ICommandtoCommandManager.RequerySuggestedis a community pattern; the reference documents the event but does not show a view-model command that uses it. -
The overview has not been rewritten for .NET 5 and later, so it does not mention nullable annotations, Fluent, or the toolkit.
-
Performance of
CanExecutere-evaluation is not quantified in the documentation. Measure your own application.
References
-
XAML Language Fundamentals for the XAML syntax used here
-
Markup Extensions & Namescopes for
Binding,RelativeSourceandx:Static -
Resources & Resource Dictionaries for the converter resource