回顧一下WPF原生實現命令
前言
最近在學習Stylet中Command="{s:Action 方法名}"的設計與實現,但要弄明白這個之前,必須對原生實現命令比較熟悉,一想我也很久沒有自己實現原生的命令了,之前都是用Community.Mvvm庫來實現,所以今天先來回顧一下,在WPF中如何實現原生的命令。
借助AI使用原生的WPF寫法實現了一個跟Stylet例子Hello一樣的效果:

WPF中如何使用命令
WPF命令是實現用戶界面交互的核心機制,通過實現ICommand接口來封裝可執行的操作。命令支持松耦合的UI設計,可以綁定到按鈕、菜單等控件,實現統一的執行邏輯。WPF提供了豐富的內置命令如ApplicationCommands、NavigationCommands等,同時也支持自定義命令,便于實現撤銷/重做、數據綁定等復雜功能。
現在先來看看這個例子中是如何使用命令的吧!!
public class RelayCommand : ICommand
{
private readonly Action<object?> _execute;
private readonly Predicate<object?>? _canExecute;
public RelayCommand(Action<object?> execute, Predicate<object?>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public bool CanExecute(object? parameter)
{
return _canExecute == null || _canExecute(parameter);
}
public void Execute(object? parameter)
{
_execute(parameter);
}
public void RaiseCanExecuteChanged()
{
CommandManager.InvalidateRequerySuggested();
}
}
這個例子中自己實現了一個實現ICommand接口的RelayCommand類。
先來看看ICommand接口:
public interface ICommand
{
event EventHandler? CanExecuteChanged;
bool CanExecute(object? parameter);
void Execute(object? parameter);
}
這個ICommand接口起到了什么作用呢?
- 統一命令規范:定義了命令的標準結構,包含執行方法Execute和狀態判斷方法CanExecute
- 實現命令綁定:允許UI控件(如Button、MenuItem)通過Command屬性綁定到具體命令實現
- 控制可用性:CanExecute方法動態控制控件的啟用/禁用狀態,CanExecuteChanged事件通知UI更新狀態
- 參數傳遞:通過parameter參數在UI和命令邏輯間傳遞數據
- 解耦UI與業務邏輯:將界面操作與具體實現分離,提高代碼的可維護性和可測試性
在RelayCommand中:
private readonly Action<object?> _execute;
private readonly Predicate<object?>? _canExecute;
_execute (Action<object?>): 存儲要執行的操作委托
_canExecute (Predicate<object?>?): 存儲判斷命令是否可執行的謂詞委托,可為 null
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
這里出現了一個CommandManager:

WPF 中的 CommandManager 是一個幫助類,位于System.Windows.Input命名空間。它并不負責“執行命令”,而是為整個命令系統(RoutedCommand / RoutedUICommand)提供基礎支撐,核心職責可以概括為四類:
1、處理路由命令的 4 個附加事件
CommandManager 預定義了 4 個 static 的 RoutedEvent,都是附加事件,所有 UIElement 都可以通過它們監聽或引發命令相關路由事件:
| 附加事件 | 觸發時機 | 典型用途 |
|---|---|---|
| PreviewCanExecuteEvent | 準備詢問某命令能否執行時觸發(隧道) | 用于全局或父級攔截“能否執行”判斷 |
| CanExecuteEvent | 同上,但為冒泡階段 | 本地邏輯判斷命令當前是否可用 |
| PreviewExecutedEvent | 準備執行命令時觸發(隧道) | 做執行前的統一攔截,例如日志、撤銷棧 |
| ExecutedEvent | 同上,但為冒泡階段 | 實際執行業務邏輯(如 Save、Cut、Paste) |
這里出現了隧道與冒泡兩個概念,該如何理解呢?
在 WPF 路由事件體系中,隧道(Tunneling)與冒泡(Bubbling)是指事件在可視化樹上傳遞的兩個方向,想象成“從上到下”還是“從下到上”即可。與命令系統結合時,理解這兩個方向就等于知道“誰先被通知”、“誰可以打斷誰”。
樹結構:
Window → Grid → StackPanel → Button
這是典型的一棵可視化樹。
隧道(Preview……)→ 從根向葉
PreviewCanExecute / PreviewExecuted 這類以 Preview 開頭的事件,先由 Window 收到,再依次 Grid、StackPanel,最后才到達實際聲明 CommandBinding / 聲明 InputBindings 的那個 Button。
作用:你可以在高層(例如 Window 一級)攔截事件,做“統一處理”或“統一否決”,比如給所有按鈕加日志、在全局禁止某些快捷鍵等。只要沿途某級標記 e.Handled = true,它就終止繼續向下傳遞。
冒泡(……無 Preview)→ 從葉向根
隧道階段結束后如果仍然 Handled == false,則進入冒泡階段。方向反過來:Button 先收到,再依次 StackPanel、Grid、Window。
作用:一般在最具體元素(Button)里決定命令是否可用或執行,而父容器只做輔助行為,如更新狀態欄、刷新菜單對勾等。同樣可以用 e.Handled = true 阻止再向上傳。
2、提供 4 組 Add xxx Handler / Remove xxx Handler 的快捷方法
這些只是對 UIElement.AddHandler、RemoveHandler 的二次封裝,方便掛接或注銷上述 4 種附加事件,省去記憶事件標識符或強制轉換類型的麻煩。
3、維護全局命令“有效性”通知:RequerySuggested
事件定義:public static event EventHandler RequerySuggested;
作用:當系統條件變化(鍵盤焦點變化、文本被修改、網絡狀態變更等)時,所有命令需要重新詢問“是否能執行”。WPF 內部的按鈕、菜單項等在訂閱此事件后,就會再次調用 ICommand.CanExecute 來決定 IsEnabled。
手動觸發:CommandManager.InvalidateRequerySuggested(); 會立即引發該事件,從而強制刷新所有綁定命令的可執行狀態。
4、提供“類級別” CommandBinding / InputBinding 注冊
RegisterClassCommandBinding(Type type, CommandBinding commandBinding)
為指定類型(而不僅是某個實例)注冊 CommandBinding,在所有實例共享同一組綁定邏輯,等同于在靜態構造函數里寫:
CommandManager.RegisterClassCommandBinding(
typeof(MyControl),
new CommandBinding(ApplicationCommands.Save, OnSaveExecuted, OnSaveCanExecute));
RegisterClassInputBinding(Type type, InputBinding inputBinding)
同樣道理,為某個控件類統一注冊快捷鍵:
CommandManager.RegisterClassInputBinding(
typeof(MyWindow),
new KeyBinding(ApplicationCommands.Save, Key.S, ModifierKeys.Control));
現在來看看整體流程:
<Button Content="Say Hello"
Command="{Binding SayHelloCommand}"
Height="30"
FontSize="14"/>
在View中綁定這個命令。
剛開始這個命令不可執行:

是因為在ViewModel中是這樣寫的,首先在構造函數中這樣寫:
public ShellViewModel()
{
SayHelloCommand = new RelayCommand(
execute: _ => ShowHelloMessage(),
canExecute: _ => CanSayHello
);
}
其中控制是否能執行的,設置了一個屬性來管理:
public bool CanSayHello => !string.IsNullOrEmpty(Name);
命令執行的方法為:
private void ShowHelloMessage()
{
MessageBox.Show($"Hello, {Name}", "Hello, Native WPF", MessageBoxButton.OK, MessageBoxImage.Information);
}
剛開始Name屬性為空,所以CanSayHello為false,所以命令不能執行。
為什么輸入東西就可以變成執行了呢?
public string Name
{
get => _name;
set
{
if (SetProperty(ref _name, value))
{
((RelayCommand)SayHelloCommand).RaiseCanExecuteChanged();
}
}
}
在RelayCommand中有一個RaiseCanExecuteChanged方法:
public void RaiseCanExecuteChanged()
{
CommandManager.InvalidateRequerySuggested();
}
CommandManager.InvalidateRequerySuggested(); 是 WPF 中用于強制刷新命令的可執行狀態的方法。所有綁定了ICommand的控件(如 Button、MenuItem 等)馬上重新評估自己的 CanExecute 狀態。
然后因為Name不為空,CanSayHello為True,這個命令就可以執行了。
點擊按鈕就會觸發RelayCommand中的Execute方法:

在ViewModel的構造函數中。實例化了一個RelayCommand對象,并且將_ => ShowHelloMessage()這個委托賦值給了execute,所以觸發命令之后就會執行ShowHelloMessage方法。

以上就是使用WPF原生的方法實現的一個使用命令的例子。

浙公網安備 33010602011771號