Summary
This incident involved a WPF MVVM application where a button bound to an ICommand correctly disabled during a long-running operation, but failed to re-enable once the operation completed—until the user interacted with the UI. The root cause was that the command’s CanExecute state was never refreshed after IsBusy changed.
Root Cause
The underlying issue was the absence of a CanExecuteChanged notification.
In WPF, changing a property used by CanExecute does not automatically trigger a re-evaluation of the command unless:
- The command raises CanExecuteChanged
- Or the command uses CommandManager.RequerySuggested, which fires only on UI events (e.g., mouse move, focus change)
Because the ViewModel set IsBusy = false without raising CanExecuteChanged, the button stayed disabled until the next UI interaction.
Why This Happens in Real Systems
This pattern is extremely common in MVVM applications because:
- Developers assume
INotifyPropertyChangedalso updatesCanExecute(it does not) CommandManager.RequerySuggestedonly fires on UI activity, not on background task completion- Async operations complete on a thread pool thread, which does not trigger WPF’s command system
Real-World Impact
Failing to raise CanExecuteChanged leads to:
- Buttons that remain disabled after async operations
- Confusing UX, where controls re-enable only after random UI interactions
- Race conditions, where UI state does not reflect ViewModel state
- Support tickets claiming the app is frozen or unresponsive
Example or Code (if necessary and relevant)
A corrected RelayCommand implementation must expose a method to raise CanExecuteChanged:
public class RelayCommand : ICommand
{
private readonly Action
And the ViewModel must call it:
IsBusy = false;
((RelayCommand)DoProcessCommand).RaiseCanExecuteChanged();
How Senior Engineers Fix It
Experienced engineers solve this by:
- Using a RelayCommand that exposes RaiseCanExecuteChanged
- Calling RaiseCanExecuteChanged whenever IsBusy changes
- Ensuring async operations never block the UI thread
- Using patterns like:
- AsyncCommand wrappers
IProgress<T>for UI updates- Centralized command state management
They also ensure that:
- PropertyChanged does not replace CanExecuteChanged
- Async void is avoided except for command handlers
Why Juniors Miss It
Less experienced developers often overlook this because:
- They assume
INotifyPropertyChangedautomatically updates commands - They don’t know that WPF’s command system only refreshes on UI events
- They rely on default
RelayCommandimplementations that hideCanExecuteChanged - They treat async operations as “magic” without understanding thread affinity
Key takeaway:
WPF will not re-evaluate CanExecute unless you explicitly tell it to.