Fixing a RelayCommand Whose CanExecute Does Not Update the Button State in WPF

A custom RelayCommand's button stays stuck when CanExecuteChanged is never raised. This compares delegating to RequerySuggested with raising it manually.

Overview

In WPF MVVM, a button is bound to an ICommand on the view model, and its enabled state follows the result of CanExecute.
A common defect is that changing the condition CanExecute depends on, such as whether an input field is filled, does not update the button.
This article explains that the cause is a missing ICommand.CanExecuteChanged notification, and it organizes two approaches with their trade-offs: delegating to CommandManager.RequerySuggested, and raising CanExecuteChanged manually.


Prerequisites / Environment


Problem

Consider binding a view model command to Button.Command and driving the enabled state from CanExecute.
The following code intends to enable a save button only when a name has been entered.

public class RelayCommand : ICommand
{
    private readonly Action _execute;
    private readonly Func<bool> _canExecute;

    public RelayCommand(Action execute, Func<bool> canExecute)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

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

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

    // Never raised, so the button stays at its first evaluation
    public event EventHandler? CanExecuteChanged;
}

CanExecute is evaluated when the command is first bound. It would normally be re-evaluated afterward in response to CanExecuteChanged, but because this implementation never raises it, entering a value into Name leaves the save button disabled.
The button has no trigger to re-evaluate CanExecute.

Two pairs of an input box and a button holding the same text. With an implementation that never raises CanExecuteChanged the button stays disabled, while delegating to CommandManager.RequerySuggested enables it.
Both rows use the same condition (executable when Name is not empty) and contain the same text. The upper implementation never raises CanExecuteChanged, so the button stays disabled after typing. The lower one delegates to CommandManager.RequerySuggested, so the requery runs and the button becomes enabled.

Cause / Background

A command source, such as a Button, subscribes to ICommand.CanExecuteChanged and re-queries CanExecute only when that event is raised, updating its own enabled state accordingly.
The official documentation states that a command source typically subscribes to CanExecuteChanged, calls CanExecute when it is raised, and disables itself if the command cannot execute.
Therefore, no matter how the return value of CanExecute changes, the button never reflects it unless CanExecuteChanged is raised.

The reason the built-in RoutedCommand rarely exposes this problem is that its CanExecuteChanged is delegated to CommandManager.RequerySuggested.
When the CommandManager detects conditions that might change a command’s ability to execute, such as a change in keyboard focus, it raises RequerySuggested and prompts every bound command to re-evaluate.
A custom RelayCommand does not ride on this mechanism, so it is responsible for raising CanExecuteChanged itself.
Note also that the CommandManager only detects UI interactions such as focus changes; it does not detect UI-independent condition changes, such as a view model property being updated.


Solution

There are two ways to raise CanExecuteChanged.

The former rides on WPF’s re-evaluation cycle; the latter re-evaluates only when explicitly told to.


Implementation

Delegate to CommandManager.RequerySuggested

The add / remove of CanExecuteChanged is forwarded to CommandManager.RequerySuggested.
The CommandManager then prompts a re-evaluation on each UI interaction, such as a focus change, and the button follows.

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

For condition changes without a UI interaction, such as a timer or the completion of an asynchronous operation, force a re-evaluation explicitly.
InvalidateRequerySuggested raises RequerySuggested, prompting the connected command sources (buttons subscribing through the built-in RoutedCommand or a delegating RelayCommand) to re-query CanExecute.

// Called when a condition changes without a UI interaction
CommandManager.InvalidateRequerySuggested();

This call does not evaluate immediately; it raises RequerySuggested to prompt the connected command sources to re-query CanExecute.
It therefore carries the cost of re-evaluating the command sources connected to RequerySuggested, as noted below.

Raise CanExecuteChanged manually

The CanExecuteChanged of the opening RelayCommand is changed to a dedicated event, and a method that raises it when re-evaluation is needed is added.

public class RelayCommand : ICommand
{
    private readonly Action _execute;
    private readonly Func<bool> _canExecute;

    public RelayCommand(Action execute, Func<bool> canExecute)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    public bool CanExecute(object? parameter) => _canExecute();
    public void Execute(object? parameter) => _execute();

    public event EventHandler? CanExecuteChanged;

    public void RaiseCanExecuteChanged()
        => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

The view model initializes SaveCommand and calls RaiseCanExecuteChanged right after updating a property (Name) that CanExecute depends on.
The following is a compilable, minimal setup that re-evaluates whenever the save button’s condition, whether Name is entered, changes.

public class SaveViewModel
{
    public RelayCommand SaveCommand { get; }

    public SaveViewModel()
    {
        // Executable when Name is not empty
        SaveCommand = new RelayCommand(Save, () => !string.IsNullOrEmpty(Name));
    }

    private string _name = string.Empty;
    public string Name
    {
        get => _name;
        set
        {
            if (_name == value) return;
            _name = value;
            // The input state changed, so re-evaluate the save command
            SaveCommand.RaiseCanExecuteChanged();
        }
    }

    private void Save() { /* Implement the save logic */ }
}

With this approach, only SaveCommand is re-evaluated, and the timing is explicit.
The RelayCommand in CommunityToolkit.Mvvm uses this approach, exposing an equivalent trigger through the NotifyCanExecuteChanged() method and the [NotifyCanExecuteChangedFor] attribute.


Notes


Alternatives / Comparison

Approach Pros Cons Best suited for
Delegate to RequerySuggested Minimal code; follows UI interactions automatically Re-queries the sources connected to RequerySuggested; opaque trigger; weak-reference caveat Executability tied mainly to UI interaction (focus, selection)
Raise CanExecuteChanged manually Re-evaluates only the target command; explicit trigger Requires an explicit raise per condition change Executability determined by view model properties
Call InvalidateRequerySuggested on demand Re-evaluates at any moment while delegating Cost of re-querying the sources connected to RequerySuggested; easy to forget Reflecting UI-independent changes under the delegating approach

Summary

The button fails to update because of a missing CanExecuteChanged notification, not because of the CanExecute result itself.
The selection criteria are as follows.

Raising every notification on the UI thread and keeping CanExecute lightweight are the prerequisites for not harming responsiveness.