WPF – ICommand: disable-enable button during long process run with Task.Run()

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 INotifyPropertyChanged also updates CanExecute (it does not)
  • CommandManager.RequerySuggested only 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 _execute;
    private readonly Func _canExecute;

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

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

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

    public event EventHandler CanExecuteChanged;

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

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 INotifyPropertyChanged automatically updates commands
  • They don’t know that WPF’s command system only refreshes on UI events
  • They rely on default RelayCommand implementations that hide CanExecuteChanged
  • They treat async operations as “magic” without understanding thread affinity

Key takeaway:
WPF will not re-evaluate CanExecute unless you explicitly tell it to.

Leave a Comment