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
- Framework: .NET 6 or later / WPF (the same applies to .NET Framework 4.5 and later)
- Language: C# / XAML (samples assume nullable reference types are enabled; on C# 7 or earlier, drop the nullable annotations)
- Target feature: a custom
RelayCommandimplementingSystem.Windows.Input.ICommand, bound throughButton.Command - Architecture: MVVM (command logic lives in the view model)
- Namespaces:
System,System.Windows.Input
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.
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.
- Delegate to
CommandManager.RequerySuggested— forward theCanExecuteChangedsubscription toCommandManager.RequerySuggested. This rides on the re-evaluation triggered by UI interactions with minimal code. For UI-independent conditions, callCommandManager.InvalidateRequerySuggested()to force a re-evaluation. - Raise
CanExecuteChangedmanually — keep a dedicated event and raise it explicitly when the condition changes. Re-evaluation is limited to the command in question, and the trigger is fully under control.
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
RequerySuggestedholds handlers by weak reference:CommandManager.RequerySuggestedkeeps registered handlers as weak references. In the delegating approach the handler registered withRequerySuggestedis created and held by the command source (such as aButton) itself, so it is not collected while that source stays alive in the visual tree, and this is usually fine; but a custom implementation that registers a handler directly withRequerySuggestedmust keep a separate strong reference, or the handler is collected and re-evaluation stops.- Call
InvalidateRequerySuggestedon the UI thread: the re-evaluation this API prompts is processed on the UI thread by theCommandManager, and the target command sources (UI elements) live on the UI thread as well. The call therefore assumes the UI thread; when state changes on a background thread, marshal to the UI thread with theDispatcherbefore calling it. - Raise manual events on the UI thread as well:
RaiseCanExecuteChangedsynchronously invokes the button-side handler, which updates a UI element. Raising it from another thread touches UI elements off the UI thread, so marshal to the UI thread with theDispatcher. - Delegation re-evaluates the sources connected to
RequerySuggested:InvalidateRequerySuggestedmakes the command sources connected toRequerySuggestedre-queryCanExecute. Heavy work insideCanExecutemakes frequent re-evaluation harm responsiveness, so keepCanExecutelightweight. - Do not leave
CanExecuteChangeddeclared but unraised: the opening example, which declaresCanExecuteChangedwithout ever raising it, compiles cleanly yet is a classic reason the state stays frozen.
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.
- When executability is tied to UI interactions such as focus or selection: delegate to
CommandManager.RequerySuggested. It needs the least code and follows UI interactions automatically. - When executability is determined by view model properties: raise
CanExecuteChangedmanually. Only the target command is re-evaluated, exactly when the condition changes. Using a framework such asCommunityToolkit.Mvvmamounts to this approach. - When reflecting an asynchronous completion under the delegating approach: call
CommandManager.InvalidateRequerySuggested()at that moment.
Raising every notification on the UI thread and keeping CanExecute lightweight are the prerequisites for not harming responsiveness.