icommand 是状态驱动的契约机制,非语法糖;relaycommand 构造需匹配 action 签名,canexecutechanged 必须绑定 commandmanager.requerysuggested,datacontext 必须正确设置,否则命令失效。

WPF 的 ICommand 不是“加个绑定就能跑”的语法糖,它是一套状态驱动的契约机制。用错地方、漏掉关键链路,按钮就永远灰着,或者点了没反应还查不出错。
RelayCommand 构造时必须传 Action
常见错误是写 new RelayCommand(DoSave),其中 DoSave() 是无参方法。WPF 绑定时默认传 null 作参数,而你的 Execute 方法如果签名是 Action(非 Action<object></object>),运行时会因委托类型不匹配静默失败——XAML 不报错,但点击无效。
- 正确写法:
new RelayCommand<object>(param => DoSave((string)param))</object>或直接接受object并做判空/转型 - 如果你不需要参数,也得声明为
Action<object></object>,内部忽略即可:new RelayCommand<object>(_ => DoSave())</object> -
RelayCommand<t></t>更安全,但要求 XAML 中CommandParameter类型严格匹配,否则CanExecute返回false且无提示
CanExecuteChanged 必须挂到 CommandManager.RequerySuggested
这是 90% 新手卡住的地方:按钮不随数据变化启停,CanExecute 只在初始化时调一次。根本原因不是逻辑写错了,而是没让 WPF 知道“这个命令的状态可能变了”。
- 手动触发刷新的最低成本方式:
CommandManager.InvalidateRequerySuggested()—— 但它会全局重查所有命令,性能差 - 真正该做的:在
RelayCommand的CanExecuteChanged事件里 +=CommandManager.RequerySuggested(不是自己 new EventHandler) - 如果你用了 Community Toolkit 的
RelayCommand,它已内置该逻辑;但手写或老版本 NuGet 包,这行必须自己补 - 注意:只有当 ViewModel 属性变更触发了
INotifyPropertyChanged,且你在CanExecute里实际读了那个属性,RequerySuggested才有意义
DataContext 错了,Command 绑定就彻底失效
WPF 不会“猜”你的 ViewModel 在哪。Command="{Binding SaveCommand}" 查找路径完全依赖当前元素的 DataContext,而不是类名、资源字典或父级窗口的代码隐藏。
- 最稳做法:在窗体构造函数末尾加
this.DataContext = new MainViewModel(); - 避免用
Resources定义 ViewModel 实例再 StaticResource 绑定——容易引发生命周期混乱和内存泄漏 - UserControl 或弹窗中,务必显式设置
DataContext,不要依赖继承;嵌套时尤其容易断链 - 调试技巧:在 XAML 里临时加
Text="{Binding}"到 TextBlock,看输出窗口是否显示 ViewModel 类型名
ICommand 执行中不能直接操作 UI 控件
命令执行逻辑属于 ViewModel 层,它的职责是改变状态,不是操控控件。写 textBox.Focus() 或 listView.ScrollIntoView(item) 看似快,实则破坏 MVVM,且在某些导航场景下会抛出跨线程异常。
- 正确路径:ViewModel 暴露一个
SelectedItem或FocusRequested属性,View 层监听该属性变化并执行 UI 操作 - 更解耦的方式:定义
INavigationService或IFocusService接口,在 ViewModel 构造时注入,执行时调_focusService.RequestFocus("searchBox") - 异步命令(
IAsyncRelayCommand)尤其要注意:UI 操作不能放在await后面,除非你明确切回 UI 线程(Dispatcher.Invoke),否则大概率崩溃
最关键的细节藏在 CanExecuteChanged 的注册时机和 DataContext 的传递路径里——这两处一错,整个命令链就断成哑巴,连异常都不会抛。别急着写逻辑,先用最简 ViewModel 和单个 Button 验证通路是否畅通。











