Alright — this is one of those topics where WPF really starts to feel different from WinForms or typical event-driven UI.
Let’s build this in layers so you don’t just learn syntax, but actually understand how commands behave in real systems.
1. Big Picture
Why WPF uses commands instead of events
In classic UI (WinForms, WebForms):
button.Click += (s, e) => Save();This works… until your app grows.
The problem with events
- Logic lives in code-behind (UI layer)
- Hard to test (requires UI)
- Hard to reuse (logic tied to a specific button)
- Hard to coordinate state (enable/disable logic scattered)
In real systems (dashboards, machine control, enterprise tools), you need:
- Centralized logic
- UI-independent behavior
- Testable actions
- Dynamic UI state (enabled/disabled based on conditions)
👉 That’s why WPF introduces Commands
2. Beginner Mental Model
Think of a command as an “action object”
Instead of:
“Button calls method”
Think:
“Button triggers an object that represents an action”
Flow in WPF
UI (Button)
↓
Command (ICommand)
↓
ViewModel logicSo:
- UI does NOT contain logic
- ViewModel owns behavior
- Command is the bridge
3. Basic Example
ViewModel
public class MainViewModel
{
public ICommand SayHelloCommand { get; }
public MainViewModel()
{
SayHelloCommand = new RelayCommand(SayHello);
}
private void SayHello()
{
MessageBox.Show("Hello from ViewModel!");
}
}XAML
<Button Content="Say Hello"
Command="{Binding SayHelloCommand}" />No Click event. No code-behind.
Simple RelayCommand
This is the most common implementation.
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool>? _canExecute;
public RelayCommand(Action execute, Func<bool>? canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object? parameter)
=> _canExecute?.Invoke() ?? true;
public void Execute(object? parameter)
=> _execute();
public event EventHandler? CanExecuteChanged;
public void RaiseCanExecuteChanged()
=> CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}4. How It Differs from Events
Event-based approach
<Button Click="OnSaveClicked" />private void OnSaveClicked(object sender, RoutedEventArgs e)
{
Save();
}Problems
- UI tightly coupled to logic
- Hard to test
Save() - No built-in way to control button state
Command-based approach
<Button Command="{Binding SaveCommand}" />Now:
- UI is dumb
- ViewModel controls behavior
- Logic is reusable + testable
5. How It Really Works in WPF
Let’s go deeper.
ICommand contract
public interface ICommand
{
bool CanExecute(object parameter);
void Execute(object parameter);
event EventHandler CanExecuteChanged;
}Execution flow
When user clicks button:
WPF checks:
csharpif (command.CanExecute(parameter))If true → calls:
csharpcommand.Execute(parameter)
UI state (enabled / disabled)
WPF automatically disables the button when:
CanExecute == falseNo manual UI code needed.
How UI knows when to re-check?
Through:
CanExecuteChangedWhen this event fires:
👉 WPF re-evaluates CanExecute() 👉 UI updates automatically
Example
SaveCommand = new RelayCommand(Save, CanSave);
private bool CanSave()
{
return !string.IsNullOrEmpty(Name);
}When Name changes:
SaveCommand.RaiseCanExecuteChanged();CommandParameter
Pass data from UI → command
<Button Content="Delete"
Command="{Binding DeleteCommand}"
CommandParameter="{Binding SelectedItem}" />In command:
public void Execute(object parameter)
{
var item = (MyItem)parameter;
}6. Real-World Example
Scenario: Save form
ViewModel
public class FormViewModel : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged();
SaveCommand.RaiseCanExecuteChanged();
}
}
public RelayCommand SaveCommand { get; }
public FormViewModel()
{
SaveCommand = new RelayCommand(Save, CanSave);
}
private void Save()
{
// Save logic
}
private bool CanSave()
{
return !string.IsNullOrWhiteSpace(Name);
}
}XAML
<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="Save"
Command="{Binding SaveCommand}" />Behavior
| State | Button |
|---|---|
| Name empty | Disabled |
| Name valid | Enabled |
👉 No UI logic. Pure ViewModel.
7. Common Mistakes
❌ Forgetting to raise CanExecuteChanged
// UI won't update
Name = "abc";✔ Fix:
SaveCommand.RaiseCanExecuteChanged();❌ Doing heavy logic inside command constructor
Commands should be thin wrappers, not business logic containers.
❌ Mixing events + commands randomly
- Leads to inconsistent architecture
- Hard to maintain
❌ Using commands for everything
Not all interactions need commands:
- Low-level UI events (mouse move, drag)
- Visual-only behavior
8. Practical Guidance
When to use commands
✔ Use commands when:
- It’s a user action (Save, Delete, Submit)
- Logic belongs in ViewModel
- You need enable/disable logic
- You want testability
When to use events
✔ Use events when:
- UI-specific behavior (animations, drag, focus)
- No business logic involved
Structuring commands in ViewModels
In real systems:
public ICommand SaveCommand { get; }
public ICommand DeleteCommand { get; }
public ICommand RefreshCommand { get; }Or group them:
public class CustomerCommands
{
public ICommand Save { get; }
public ICommand Delete { get; }
}Scaling in large applications
In enterprise apps:
- Use generic
RelayCommand<T> - Use Command factories
- Centralize command logic patterns
- Integrate with validation systems
Testability
You can test:
Assert.False(vm.SaveCommand.CanExecute(null));
vm.Name = "Valid";
Assert.True(vm.SaveCommand.CanExecute(null));No UI required.
9. Summary
Key ideas to remember
- Commands replace event handlers for clean architecture
ICommand= action abstraction- UI triggers → ViewModel handles logic
Execute= what to doCanExecute= whether it’s allowedCanExecuteChanged= when UI should update
Mental shift
Old thinking:
“Button calls method”
WPF thinking:
“Button triggers a command, ViewModel decides behavior”
Why this matters in real systems
In large WPF apps (like machine control or dashboards):
- You avoid UI spaghetti
- You centralize behavior
- You gain testability
- You get automatic UI state management
If you want next step, I’d suggest:
👉 Deep dive: CommandManager & automatic requery behavior This is where WPF gets non-obvious in production systems.