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 ICommand objects 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

bool CanExecute(object? parameter)

Whether the action is available now. A command source (a button, a menu item) calls it to decide whether it is enabled.

void Execute(object? parameter)

Performs the action. The source calls it when the user invokes it.

event EventHandler? CanExecuteChanged

Tells the sources that the answer of CanExecute may have changed, so they must ask again.

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 Button calls CanExecute once when the command is assigned, and again only when it receives CanExecuteChanged. 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

Command

The ICommand to run when the source is invoked.

CommandParameter

An arbitrary object passed to CanExecute and Execute. Usually a binding to the selected item or to the item of a row.

CommandTarget

The element a routed command runs on. It is ignored for any command that is not a RoutedCommand.

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.

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

RoutedCommand, RoutedUICommand

ApplicationCommands.Copy, or the app’s own BookshelfCommands.FocusSearch.

Command source

Button, MenuItem, Hyperlink, KeyBinding

The Edit menu’s "Copy" item, or Ctrl+E.

Command target

Any IInputElement

The search TextBox that has the keyboard focus.

Command binding

CommandBinding

The TextBox supplies one for Copy; the main window supplies one for Close.

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.

sequenceDiagram actor U as User participant S as Command source (MenuItem or Button) participant C as RoutedCommand participant T as Command target (focused element) participant W as Window with CommandBinding S->>C: CanExecute(parameter, target) C->>T: PreviewCanExecute tunnels down C->>T: CanExecute bubbles up T->>W: CanExecute reaches the CommandBinding W-->>S: handler sets CanExecute true, so the source is enabled U->>S: clicks or presses the key gesture S->>C: Execute(parameter, target) C->>T: PreviewExecuted tunnels down C->>T: Executed bubbles up T->>W: Executed reaches the CommandBinding W->>W: handler runs the action and marks the event handled

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.

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 ICommand.

Derives from RoutedCommand.

Identity

Name and OwnerType.

The same, plus a Text property: the localized display name.

Gestures

InputGestures collection; defaults come from the constructor.

The same.

Menu behavior

A MenuItem needs its own header.

A MenuItem with no Header shows the command’s Text, and shows the first gesture as its gesture text.

Typical use

Internal, non-visible actions.

Anything that appears in a menu or toolbar. All the built-in commands are RoutedUICommand.

The command library classes expose static RoutedUICommand fields. Their logic is the responsibility of whoever handles them, exactly as described above.

Class Examples Notes

ApplicationCommands

New, Open, Save, SaveAs, Close, Print, Cut, Copy, Paste, Delete, Undo, Redo, Find, Replace, SelectAll, Help, Properties, Stop

The everyday application verbs. Most have a default gesture such as Ctrl+S, Ctrl+C or F1.

EditingCommands

ToggleBold, AlignCenter, IncreaseFontSize, MoveToLineEnd, Backspace

In System.Windows.Documents. Handled by RichTextBox and other document editors.

NavigationCommands

BrowseBack, BrowseForward, GoToPage, NextPage, Refresh, Zoom

Handled by Frame, NavigationWindow and document viewers.

MediaCommands

Play, Pause, Stop, TogglePlayPause, NextTrack

Media keys on the keyboard map to these gestures.

ComponentCommands

MoveDown, ScrollPageDown, SelectToEnd

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.

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.

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.

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

KeyBinding.Key, KeyBinding.Modifiers

Separate attributes: Key="N" Modifiers="Control".

InputBinding.Gesture

A single attribute with a string converted by KeyGestureConverter, for example Gesture="Ctrl+N" or Gesture="F5".

MouseBinding.MouseAction, MouseBinding.Gesture

MouseAction="LeftDoubleClick"; combine with Modifiers for Ctrl+click.

Command, CommandParameter, CommandTarget

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.

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, PreviewExecuted and PreviewCanExecute;

  • it registers class-level CommandBinding and InputBinding objects (RegisterClassCommandBinding, RegisterClassInputBinding);

  • it raises RequerySuggested whenever it detects a change that might affect command state, such as a change in focus, a key or mouse event, or a selection change, and RoutedCommand.CanExecuteChanged is 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:

  • RequerySuggested holds its handlers through weak references. A command source subscribed through a SimpleCommand stays alive on its own, so that is fine, but code that subscribes an anonymous lambda directly to RequerySuggested can find the handler garbage collected and never called again.

  • CommandManager lives in PresentationCore, so a view-model library that references it is tied to WPF. The ICommand implementations of the MVVM Toolkit do not use CommandManager; they raise CanExecuteChanged explicitly, which is why the library works unchanged on WinUI 3, MAUI and Avalonia.

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 CommandBinding handler, usually in code-behind.

In the command itself (a delegate to a view-model method).

Who is the target?

The element with the focus, or CommandTarget.

The view model; there is no routing and CommandTarget is ignored.

How availability is refreshed

Automatically after input, through RequerySuggested.

Explicitly, when the view model state changes (CanExecuteChanged).

Reusable on other stacks

No. WPF-specific.

Yes. ICommand exists on WinUI 3, MAUI and Avalonia.

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

CanExecute = nameof(Method)

A method or property returning bool that decides availability. It may take the same parameter as the command.

AllowConcurrentExecutions

For async commands. false (the default) disables the command while an execution is pending; true lets any number of invocations run.

FlowExceptionsToTaskScheduler

For async commands. false (the default) awaits and rethrows on the synchronization context, which crashes the app through the dispatcher’s unhandled-exception path as a synchronous command would; true flows the exception to ExecutionTask and TaskScheduler.UnobservedTaskException.

IncludeCancelCommand

For async commands taking a CancellationToken. Generates a second command that cancels the running one.

[property: ...] on the method

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.

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;
}

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>

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 a CancelCommand ending is produced (DoWorkCancelCommand for DoWorkAsync); for LoadAsync that is LoadCancelCommand. The cancel command is disabled while no load is running.

  • Inside a DataTemplate the DataContext is the row (a BookViewModel), so the button reaches the parent view model’s command with a RelativeSource binding to the DataContext of the ListView, and passes the row with CommandParameter="{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 routed Paste command still targets the search TextBox. 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 to T; a wrong type throws an exception at the moment the source evaluates or runs the command, so keep the CommandParameter binding and the parameter type in step.

  • Declare the parameter as nullable (BookViewModel?). WPF evaluates CanExecute before every binding has produced a value, so the command is asked with null first; code that dereferences the parameter without a check throws on start-up.

  • CommandParameter belongs 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.Parameter in 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.

Command="{Binding AddBookCommand}" on a button, a menu item or a KeyBinding. No code-behind.

Handle a routed command in the view

View concerns: focus, clipboard, closing, zoom.

A CommandBinding on the window, with the handler in code-behind or a small view helper.

Forward a routed command to the view model

A standard command should act on the model: for example ApplicationCommands.Delete or Print.

A CommandBinding whose handler calls the view-model command, shown below.

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;
            }));
    }
}

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);
    }
}

Commands across the Windows UI stacks

Concern WPF WinUI 3 Windows Forms

ICommand on sources

Button, MenuItem, Hyperlink, InputBinding have Command and CommandParameter.

Button, AppBarButton, MenuFlyoutItem and other controls have Command and CommandParameter.

ButtonBase and ToolStripItem gained Command and CommandParameter properties in .NET 7, introduced as a preview feature there; check the current status for the .NET version you target.

Routed commands and CommandBinding

Yes: RoutedCommand, RoutedUICommand, the command library classes.

No. There is no routed-command mechanism and no CommandManager.

No.

Reusable command objects with label, icon and shortcut

RoutedUICommand (text and gestures).

XamlUICommand (Label, Description, IconSource, KeyboardAccelerators, ExecuteRequested, CanExecuteRequested, NotifyCanExecuteChanged) and StandardUICommand with predefined commands.

None built in.

Keyboard shortcuts

KeyBinding and KeyGesture.

KeyboardAccelerator on controls or on a XamlUICommand.

ToolStripMenuItem.ShortcutKeys, Form.KeyPreview and ProcessCmdKey.

CanExecute refresh

Automatic for routed commands, through CommandManager.

Explicit: CanExecuteChanged or NotifyCanExecuteChanged.

Explicit: CanExecuteChanged.

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

ICommand is annotated object? in .NET. A command written for .NET Framework as Execute(object parameter) now produces nullability warnings; update the signatures.

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

[RelayCommand] replaces the hand-written RelayCommand or DelegateCommand class and the property wrapper around it.

MVVM Toolkit 8.x and C# 14

[ObservableProperty] on a partial property replaces the field-based form, and [NotifyCanExecuteChangedFor] works with both. See The MVVM Toolkit (CommunityToolkit.Mvvm).

.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, call NotifyCanExecuteChanged() or put [NotifyCanExecuteChangedFor] on the property that drives the rule. With a hand-written command, raise the event or route it through CommandManager.RequerySuggested.

  • Expensive CanExecute. With routed commands and any command wired to RequerySuggested, the method runs after nearly every input event for every visible source. Compare cheap flags, never query a database or the file system.

  • async void commands. A hand-written Execute that is async void loses exceptions and cannot be awaited or tested. Use IAsyncRelayCommand (or [RelayCommand] on a Task method) 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 DeleteSelectedAsync reaches the dispatcher’s unhandled-exception path and can close the app. Catch and report failures inside the command, or set FlowExceptionsToTaskScheduler on 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 = true removes 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 Menu or ToolBar, FocusManager.IsFocusScope, or an explicit CommandTarget.

  • CommandBinding placement. A CommandBinding on 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 a RelativeSource AncestorType binding. A ContextMenu is not part of the visual tree of its owner and needs PlacementTarget in the path; the DataContext of the menu is inherited from the target.

  • Single-key KeyBinding inside editors. A KeyBinding with no modifier on an ancestor of a TextBox can 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.

  • CommandManager in a shared library. It ties the library to WPF. Keep ICommand implementations 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 CanExecute of 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 a RoutedUICommand provides 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 Binding to an ICommand, the RelativeSource pattern for rows and the forwarding of a routed command to a view model are common practice, not documented recommendations.

  • There is no official RelayCommand or DelegateCommand in 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 CommandBinding to a view model in XAML.

  • Wiring a custom ICommand to CommandManager.RequerySuggested is 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 CanExecute re-evaluation is not quantified in the documentation. Measure your own application.