首页 / 资讯中心 / 文章详情

从零搭建半导体设备上位机(三):CommunityToolkit.Mvvm 与 Prism 混用,哪些用源生成器,哪些手写

从零搭建半导体设备上位机(三):CommunityToolkit.Mvvm 与 Prism 混用,哪些用源生成器,哪些手写 ★ FEATURED ARTICLE
CommunityToolkit.Mvvm 与 Prism 混用哪些用源生成器哪些手写这是「从零搭建半导体设备上位机」系列第3篇。上一篇说主 ViewModel 是单例、注册集中在一个方法里。这一篇处理一个更麻烦的问题项目里同时存在 Prism 的 MVVM 体系和 CommunityToolkit.Mvvm到底该怎么分。同一个项目里有三套基类把整个解决方案扫一遍按属性通知怎么来的分类结果是这样的做法文件数继承 Prism 的BindableBase100用 Prism 的DelegateCommand37用[ObservableProperty]源生成器23继承自写基类ViewModel/ViewModelNew23用[RelayCommand]源生成器9继承ObservableObject3一百个文件用 Prism九个文件用源生成器。这不是混用这是主干是 PrismCommunityToolkit 是后来局部接进来的。而且中间还夹着两套自写基类Core/Mvvm/ViewModel.cs继承ObservableObject和Shared/Common/ViewModelNew.cs。加上 Prism 的ViewModelBase项目里一共三套基类。这一篇不打算说这样很乱应该统一——统一是正确但昂贵的建议现实里没人有时间重写一百个文件。我想说清楚的是在必须混用的前提下哪些地方该用哪一套以及混用会踩什么坑。一、先搞清楚两套东西的边界Prism 提供什么BindableBaseSetProperty/RaisePropertyChanged手写属性通知。DelegateCommand命令支持ObservesProperty属性变化时自动重新求值CanExecute和ObservesCanExecute。IConfirmNavigationRequest导航前拦截可以弹确定离开吗。INavigationAwareOnNavigatedTo/OnNavigatedFrom。CommunityToolkit.Mvvm 提供什么[ObservableProperty]字段上加特性编译期生成属性 通知。[RelayCommand]方法上加特性编译期生成ICommand。ObservableObject/ObservableRecipient基类。WeakReferenceMessenger消息总线。边界在哪里一句话Prism 提供的是框架能力导航、区域、对话框CommunityToolkit 提供的是写代码的省力工具源生成器。所以正确的用法不是二选一而是分层需要和 Prism 导航/区域协作的 ViewModel→ 继承BindableBase命令用DelegateCommand。纯数据模型、DTO、叶子 ViewModel不参与导航、不弹对话框→ 用[ObservableProperty]/[RelayCommand]。实测下来这个划分和项目现状基本吻合23 个用源生成器的文件绝大多数在Models/和DTO/下而不是ViewModels/下。二、源生成器的三个真实限制用[ObservableProperty]之前得知道它不能干什么。这三条都是踩过的。限制一类必须是partialpublicpartialclassAxisInfoBase:ObservableObject{[ObservableProperty]privatedouble_speed;}忘了写partial编译器报的是找不到Speed属性而不是你的类不是 partial。这个错误信息指向 XAML 绑定很容易往错的方向查。项目约定Models/下凡是带[ObservableProperty]的类类名一律partial不用想。限制二生成属性名和字段名的对应规则容易记错[ObservableProperty] private double _speed;生成Speed。[ObservableProperty] private double speed;也生成Speed。但[ObservableProperty] private double _axisSpeed;生成的是AxisSpeed。规则是去掉前导下划线首字母大写。听上去简单实际项目里出现过_chAMaxSpeed这种字段——生成出来是ChAMaxSpeed而 XAML 里写的是CHAMaxSpeed绑定静默失效界面数字不动也不报错。WPF 的绑定失败是静默的这是它最坑的地方之一。后来在调试配置里打开了绑定错误输出到 Output 窗口这类问题才变得可见。限制三[RelayCommand]生成的命令名是方法名 Command[RelayCommand]privateasyncTaskSaveAsync(){...}生成SaveCommand不是SaveAsyncCommand。方法名里的Async后缀会被吃掉。这条踩过一次XAML 里写Command{Binding SaveAsyncCommand}编译通过、运行不报错、按钮点了没反应。查了半小时才发现命令名少了个Async。三、混用最容易出的事通知链断裂比上面三条更隐蔽的问题是基类混用导致属性通知链断掉。场景是这样的某个 ViewModel 继承BindableBase里面有一个集合属性集合元素用的是[ObservableProperty]的模型类。// ViewModelPrism 体系publicclassPageChamberViewModel:BindableBase{publicObservableCollectionChamberInfoChambers{get;}new();}// 元素CommunityToolkit 体系publicpartialclassChamberInfo:ObservableObject{[ObservableProperty]privatedouble_pressure;}这个组合是能工作的——元素自己通知自己界面上的{Binding Pressure}会更新。但一旦你想在 ViewModel 里监听元素变化chamber.PropertyChanged(s,e){/* 汇总处理 */};ObservableObject和BindableBase都实现了INotifyPropertyChanged事件签名一致所以这段代码能编译能跑。问题出在SetProperty的相等性判断上。两套基类的SetProperty实现细节不同对double的NaN、对-0.0与0.0的比较行为不完全一致。项目里出现过一次压力值从 0 变成 -0.0界面不刷新的怪事就是这里。真正稳妥的做法不是去研究两套实现的差异而是在边界处统一跨体系传值的地方用显式的RaisePropertyChanged不依赖SetProperty的短路逻辑。四、那两套自写基类是怎么回事Core/Mvvm/ViewModel.cs和Shared/Common/ViewModelNew.cs都是继承ObservableObject的薄封装。为什么会有两个因为它们是不同时期加的ViewModel最早的加了IsBusy和一个统一的Title属性。ViewModelNew后来发现IsBusy不够用需要加载中/保存中/测试中三种状态又加了一个。结果就是现在有的页面继承BindableBase有的继承ViewModel有的继承ViewModelNew还有的直接继承ObservableObject。这是典型的技术债但它的代价比想象中低——因为三套基类都只做属性通知没有复杂的生命周期逻辑。真正的代价是新人接手时要问三次我该继承哪个。如果要收敛正确顺序是先合并两个自写基类改动小、收益明确再考虑要不要统一到ObservableObject改动大、收益一般。不要一次性推翻 Prism 体系——IConfirmNavigationRequest和DelegateCommand.ObservesProperty这两个能力CommunityToolkit 没有对等物换掉的成本远大于收益。踩坑记录坑一绑定失败不报错前面提过CHAMaxSpeed写成ChAMaxSpeed之后界面数字永远不动。查的时候先怀疑了 PLC 没给数据、又怀疑了转换器最后才发现是属性名。现在所有 WPF 项目的第一个配置项就是打开绑定诊断在App.xaml.cs里设置PresentationTraceSources.DataBindingSource的级别让绑定错误打到 Output。这条配置救过的时间比任何重构都多。坑二[RelayCommand]的 CanExecute 不会自动重算Prism 的DelegateCommand有ObservesProperty属性变了自动重新求值。[RelayCommand]的CanExecute需要显式调用NotifyCanExecuteChanged()。有一次按钮该灰的时候没灰操作员连点三下把同一个动作下发给了 PLC 三次。工控软件里按钮该灰没灰不是体验问题是安全问题。后来定规矩凡是能下发硬件动作的命令CanExecute必须显式通知且在方法入口再做一次幂等判断。坑三两套 Messenger 都在用Prism 有IEventAggregatorCommunityToolkit 有WeakReferenceMessenger。项目里两个都在用。后果是排查谁收到了这个消息时要看两个地方。有一次报警消息用WeakReferenceMessenger发订阅方用的是IEventAggregator消息发出去没人接界面上没有报警——但日志里明明记了。约定跨模块通信用 Prism 的IEventAggregator因为它和 Region 生命周期一致模块内部用直接事件或WeakReferenceMessenger。一个消息只走一条通道。本篇小结混用的正确姿势是分层不是二选一参与导航的 ViewModel 走 Prism纯数据模型走源生成器。源生成器有三个硬限制类必须partial、属性名规则容易记错、[RelayCommand]会吃掉Async后缀。跨体系边界要用显式RaisePropertyChanged不要依赖SetProperty的短路判断。两套 Messenger 不能混着发消息一个消息只走一条通道。下一篇换到最重的那块那个 526 行的App.xaml.cs单实例锁、启动流程、两套配置文件以及开发模式和生产模式行为不一样带来的坑。下期预告第4篇单实例、启动流程与配置文件App 里那 526 行到底在干什么
阅读完成 · 觉得有帮助?
咨询建站