Prism基础_命令(DelegateCommand)详解(工业级上位机专篇)
在工业级WPF上位机开发中,命令(Command)是MVVM模式中处理用户交互的核心机制。按钮点击、菜单选择、设备启停、报警确认、参数保存等操作,都应该通过Command实现,而不是在View的Code-Behind中写事件处理代码。
Prism提供的DelegateCommand(以及泛型DelegateCommand<T>)完美封装了ICommand接口,让你以简洁、声明式的方式将ViewModel中的逻辑绑定到UI控件,同时自动支持CanExecute动态启用/禁用按钮。
这解决了工业痛点:
- 设备“启动”按钮只有在“无故障 + 已连接PLC”时才可用。
- 异步操作(如下发配方到PLC)不会阻塞UI。
- 权限控制、状态联动实时生效。
- 代码可测试、可维护。
1. DelegateCommand 基础用法
所有命令都定义在ViewModel中,继承BindableBase的ViewModel里直接使用。
usingPrism.Commands;usingPrism.Mvvm;publicclassMachineControlViewModel:BindableBase{// 1. 无参数命令(最常用)publicDelegateCommandStartMachineCommand{get;privateset;}publicDelegateCommandStopMachineCommand{get;privateset;}// 2. 带参数命令(例如选择产线ID)publicDelegateCommand<int>SelectLineCommand{get;privateset;}publicMachineControlViewModel(){// 初始化命令StartMachineCommand=newDelegateCommand(ExecuteStartMachine,CanExecuteStartMachine);StopMachineCommand=newDelegateCommand(ExecuteStopMachine);SelectLineCommand=newDelegateCommand<int>(ExecuteSelectLine);}// 执行方法privatevoidExecuteStartMachine(){// 发送PLC启动指令(通过注入的IPlcService)// _plcService.WriteStartBit();MachineStatus="启动中...";}privatevoidExecuteStopMachine(){MachineStatus="已停止";}privatevoidExecuteSelectLine(intlineId){CurrentLineId=lineId;// 加载对应产线参数}// CanExecute 方法(决定按钮是否可用)privateboolCanExecuteStartMachine(){returnIsPlcConnected&&!HasFault&&MachineStatus!="启动中...";}}2. XAML中的绑定方式
<!-- 按钮绑定命令 --><ButtonContent="启动设备"Command="{Binding StartMachineCommand}"Width="120"Height="40"/><!-- 带参数的命令(例如ListBox选中项) --><ListBoxItemsSource="{Binding ProductionLines}"><ListBox.ItemTemplate><DataTemplate><ButtonContent="选择此产线"Command="{Binding DataContext.SelectLineCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"CommandParameter="{Binding LineId}"/></DataTemplate></ListBox.ItemTemplate></ListBox>进阶:结合Converter实现按钮文字动态变化(启动/停止切换)。
3. 动态启用/禁用(CanExecute 核心价值)
工业上位机中,按钮状态必须随实时状态变化自动更新。
Prism的DelegateCommand支持以下方式自动刷新CanExecute:
方式一:手动调用 RaiseCanExecuteChanged()
privatevoidOnPlcStatusChanged(){StartMachineCommand.RaiseCanExecuteChanged();// 通知UI重新评估CanExecute}方式二:使用 ObservesProperty(Prism强大特性,强烈推荐)
publicMachineControlViewModel(){StartMachineCommand=newDelegateCommand(ExecuteStartMachine,CanExecuteStartMachine).ObservesProperty(()=>IsPlcConnected)// 观察属性变化.ObservesProperty(()=>HasFault).ObservesProperty(()=>MachineStatus);}当被观察的属性通过SetProperty变化时,CanExecute会自动重新计算,按钮自动Enable/Disable。
进阶:观察集合属性(Count等)
- 可手动订阅
CollectionChanged事件后调用RaiseCanExecuteChanged()。 - 或通过
ObservesProperty(() => MyCollection.Count)(部分版本支持嵌套属性)。
4. 异步命令(工业必备:PLC下发、报表生成等耗时操作)
Prism支持异步执行,避免UI卡顿。
publicDelegateCommandSaveRecipeCommand{get;privateset;}publicMachineControlViewModel(){// 异步命令SaveRecipeCommand=newDelegateCommand(async()=>awaitExecuteSaveRecipeAsync());// 或带CanExecute// SaveRecipeCommand = new DelegateCommand(ExecuteSaveRecipeAsync, CanSaveRecipe)// .ObservesProperty(() => IsDirty);}// 异步执行方法privateasyncTaskExecuteSaveRecipeAsync(){try{IsBusy=true;// 显示加载动画await_plcService.WriteRecipeAsync(CurrentRecipe);// 异步下发PLCawaitTask.Delay(500);// 模拟等待MachineStatus="配方保存成功";}catch(Exceptionex){// 日志 + 弹窗提示_dialogService.Show("保存失败",ex.Message);}finally{IsBusy=false;}}注意:
- 默认异步命令不允许并行执行(防止重复点击)。
- 需要并行时可调用
.EnableParallelExecution()(Prism较新版本支持)。
5. 工业级高级用法与最佳实践
权限控制:
publicDelegateCommandEmergencyStopCommand{get;}// CanExecute 中加入用户角色判断privateboolCanEmergencyStop()=>CurrentUser?.HasPermission("EmergencyStop")==true;全局复合命令(CompositeCommand):
- 用于“全局急停”:多个模块的子命令同时执行。
CompositeCommand聚合多个DelegateCommand,任一子命令不可执行则整体不可执行。
与绑定通知结合:
- CanExecute依赖的属性必须使用
SetProperty更新。 - 结合
IValueConverter实现按钮颜色/文字随状态变化。
- CanExecute依赖的属性必须使用
与EventAggregator结合:
- 执行命令后发布事件,让其他模块(报警、趋势)响应。
最佳实践:
- 命令初始化放在ViewModel构造函数中。
- Execute方法保持简洁,复杂逻辑委托给注入的服务(IPlcService、IRecipeService)。
- 避免在CanExecute中做耗时操作(应预计算状态)。
- 长时间运行的上位机:注意在模块卸载时释放命令相关资源。
- 测试:ViewModel单元测试中可直接调用
command.Execute()和command.CanExecute()。
6. 完整工业示例(设备控制面板)
publicclassDevicePanelViewModel:BindableBase{privatebool_isPlcConnected;publicboolIsPlcConnected{get=>_isPlcConnected;set=>SetProperty(ref_isPlcConnected,value);}publicDelegateCommandStartCommand{get;privateset;}publicDevicePanelViewModel(IPlcServiceplcService){StartCommand=newDelegateCommand(async()=>awaitplcService.StartDeviceAsync(),CanStart).ObservesProperty(()=>IsPlcConnected).ObservesProperty(()=>HasFault);}privateboolCanStart()=>IsPlcConnected&&!HasFault;}对应XAML中按钮会根据PLC连接状态和故障状态自动灰显/亮显。
掌握DelegateCommand后,你的工业上位机交互就具备了真正的MVVM纯净性:UI只负责展示和触发命令,所有业务逻辑和状态判断都在ViewModel中。
立即实践建议:
- 在你的MachineMonitorViewModel中添加
StartMachineCommand和StopMachineCommand。 - 使用
ObservesProperty让按钮根据IsRunning、HasFault等属性自动启用/禁用。 - 实现一个异步
SaveParameterCommand,模拟向PLC写入参数。 - 运行测试:改变PLC模拟状态,观察按钮是否实时变化。
完成后,界面操作将非常流畅、专业,像真正的工厂HMI一样可靠。
下一节我们将进入Prism弹窗对象(IDialogService)详解,学习如何优雅地弹出参数设置、报警确认、配方编辑等工业常用弹窗,并实现主窗口与弹窗之间的参数传递和返回值处理。
有任何命令不刷新、异步异常、CanExecute不生效等问题,随时贴出代码,我会帮你排查优化。
Prism ObservesProperty 高级用法详解(工业级上位机专篇)
ObservesProperty是Prism DelegateCommand最强大、最实用的特性之一。它让命令的CanExecute(按钮是否可用)能够自动响应ViewModel中属性的变化,无需手动调用RaiseCanExecuteChanged(),极大简化了工业上位机中“状态联动”场景的代码。
在工厂监控系统中,这意味着:
- “启动设备”按钮只有在PLC已连接 && 无故障 && 参数已校验时才亮起。
- 报警确认按钮根据报警等级动态启用。
- 保存配方按钮在“有修改(IsDirty)”时才可用。
- 所有状态实时联动,操作员体验接近原生HMI。
1. 基础回顾与正确写法
publicclassMachineControlViewModel:BindableBase{privatebool_isPlcConnected;publicboolIsPlcConnected{get=>_isPlcConnected;set=>SetProperty(ref_isPlcConnected,value);}privatebool_hasFault;publicboolHasFault{get=>_hasFault;set=>SetProperty(ref_hasFault,value);}publicDelegateCommandStartMachineCommand{get;privateset;}publicMachineControlViewModel(){StartMachineCommand=newDelegateCommand(ExecuteStart,CanExecuteStart).ObservesProperty(()=>IsPlcConnected)// 关键:链式调用.ObservesProperty(()=>HasFault);}privatevoidExecuteStart(){/* PLC启动指令 */}privateboolCanExecuteStart(){returnIsPlcConnected&&!HasFault;}}XAML中按钮会自动根据状态灰显/亮显,无需额外代码。
链式调用:可以连续.ObservesProperty()观察多个属性。
2. 高级用法一:嵌套属性(Nested Properties)——最常用工业技巧
Prism 支持观察属性链(只要链上每个对象都实现INotifyPropertyChanged)。
工业场景示例:观察子对象(PLC状态对象、当前报警对象)的属性。
// ViewModel 中privatePlcStatus_plcStatus=newPlcStatus();publicPlcStatusPlcStatus{get=>_plcStatus;set=>SetProperty(ref_plcStatus,value);}privateAlarmItem_currentAlarm;publicAlarmItemCurrentAlarm{get=>_currentAlarm;set=>SetProperty(ref_currentAlarm,value);}// PlcStatus 类也要继承 BindableBase 或实现 INPCpublicclassPlcStatus:BindableBase{privatebool_isConnected;publicboolIsConnected{get=>_isConnected;set=>SetProperty(ref_isConnected,value);}privatestring_errorCode;publicstringErrorCode{get=>_errorCode;set=>SetProperty(ref_errorCode,value);}}命令观察嵌套属性:
StartMachineCommand=newDelegateCommand(ExecuteStart,CanExecuteStart).ObservesProperty(()=>PlcStatus.IsConnected)// 嵌套1.ObservesProperty(()=>PlcStatus.ErrorCode)// 嵌套2.ObservesProperty(()=>CurrentAlarm.Severity);// 嵌套3机制说明:
- Prism 会监听整个属性链上的
PropertyChanged事件。 - 只要链上任意一层属性变化(IsConnected、ErrorCode、Severity),都会触发
RaiseCanExecuteChanged()。 - 这对模块化上位机特别友好:PLC数据服务更新子对象属性时,命令状态自动刷新。
注意:如果中间对象(如PlcStatus)整个被替换(SetProperty),也需要观察父属性() => PlcStatus才能完整捕获。
3. 高级用法二:ObservesCanExecute(Prism 8+ 推荐简化写法)
当CanExecute方法逻辑简单(直接返回某个属性或表达式)时,可省略单独的CanExecute方法。
// 方式一:传统 + ObservesPropertypublicDelegateCommandSaveCommand{get;}// 方式二:使用 ObservesCanExecute(更简洁)publicMachineControlViewModel(){SaveCommand=newDelegateCommand(ExecuteSave).ObservesCanExecute(()=>IsDirty&&IsPlcConnected);// 直接传入表达式}优点:
- 无需单独写
bool CanExecuteSave()方法。 - 表达式支持逻辑运算(
&&、||、!等)。 - 内部仍会自动处理属性变化监听。
适用场景:简单状态判断;复杂逻辑仍推荐单独CanExecute方法 +ObservesProperty(可读性更好,便于调试)。
4. 高级用法三:集合相关观察(ObservesCollection workaround)
Prism 原生没有ObservesCollection,但工业上位机常需根据列表状态控制命令(例如选中项数量 > 0 时才能“批量删除”)。
推荐解决方案:
privateObservableCollection<RecipeItem>_recipes=new();publicObservableCollection<RecipeItem>Recipes{get=>_recipes;}publicDelegateCommandDeleteSelectedCommand{get;privateset;}publicMachineControlViewModel(){DeleteSelectedCommand=newDelegateCommand(ExecuteDelete,CanDelete);// 手动订阅集合变化Recipes.CollectionChanged+=(s,e)=>DeleteSelectedCommand.RaiseCanExecuteChanged();// 同时观察选中项属性DeleteSelectedCommand.ObservesProperty(()=>SelectedRecipe);}privateboolCanDelete()=>SelectedRecipe!=null||Recipes.Count>0;进阶:可封装一个扩展方法或基类ViewModel,自动为ObservableCollection属性添加监听。
5. 高级用法四:与复合命令(CompositeCommand)结合
全局急停命令可聚合多个模块的子命令,并观察全局状态:
publicCompositeCommandGlobalEmergencyStopCommand{get;}=newCompositeCommand();publicMachineControlViewModel(){varlocalStop=newDelegateCommand(ExecuteLocalStop,CanLocalStop).ObservesProperty(()=>IsPlcConnected);GlobalEmergencyStopCommand.RegisterCommand(localStop);}6. 工业级最佳实践与注意事项
- 性能:只观察真正影响
CanExecute的属性,避免观察过多无关属性。 - 线程安全:属性变化必须在UI线程(通过
IDispatcher或 Prism 的绑定机制)。 - 嵌套深度:支持较深链,但建议保持在 2-3 层,避免调试困难。
- 模块化场景:在不同模块的 ViewModel 中使用
ObservesProperty,结合EventAggregator实现跨模块状态联动。 - 测试:单元测试中可直接调用
command.CanExecute(null)和command.RaiseCanExecuteChanged()验证。 - 与 IValueConverter 结合:按钮文字/颜色可通过 Converter 进一步美化(例如“启动”/“停止”切换)。
- 替代方案:当观察非常复杂时,可在属性 Setter 中手动调用
RaiseCanExecuteChanged()(作为补充)。 - Prism 版本:Prism 8+ 行为更稳定,推荐使用最新版。
7. 完整工业示例(产线控制面板)
publicclassLineControlViewModel:BindableBase{publicPlcStatusPlcStatus{get;}=newPlcStatus();publicObservableCollection<AlarmItem>ActiveAlarms{get;}=new();publicDelegateCommandStartLineCommand{get;privateset;}publicDelegateCommandAcknowledgeAllAlarmsCommand{get;privateset;}publicLineControlViewModel(){StartLineCommand=newDelegateCommand(ExecuteStartLine,CanStartLine).ObservesProperty(()=>PlcStatus.IsConnected).ObservesProperty(()=>PlcStatus.ErrorCode).ObservesProperty(()=>ActiveAlarms.Count);// 集合Count间接观察(需手动辅助)AcknowledgeAllAlarmsCommand=newDelegateCommand(ExecuteAckAll).ObservesCanExecute(()=>ActiveAlarms.Count>0);}privateboolCanStartLine()=>PlcStatus.IsConnected&&PlcStatus.ErrorCode=="0"&&ActiveAlarms.Count==0;}掌握ObservesProperty高级用法后,你的命令系统将具备真正的“响应式”能力:状态一变,按钮立即响应,极大提升上位机的专业度和可靠性。
立即实践建议:
- 在现有 ViewModel 中为一个命令添加 2-3 个
ObservesProperty(包含嵌套属性)。 - 用
ObservesCanExecute重构一个简单 CanExecute 方法。 - 测试 PLC 模拟数据变化,观察按钮状态是否实时更新。
- 尝试为
ObservableCollection添加手动监听。
下一节我们将进入Prism弹窗对象(IDialogService)详解,学习如何用弹窗实现参数设置、报警确认、配方编辑等工业常用交互,并结合命令实现完整流程。
如果你在实际使用中遇到ObservesProperty不触发、嵌套属性无效、集合不响应等问题,请贴出你的 ViewModel 代码,我会帮你精准排查和优化。
现在就动手把你的“启动”按钮用嵌套属性观察起来吧!让它真正随PLC状态实时联动。准备好了告诉我,我们继续前进!
现在就动手把你的设备启停按钮用DelegateCommand实现起来吧!感受Prism命令带来的清晰架构。准备好了告诉我,我们继续下一部分!