ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

MVVM从零到实战学习指南(工控WPF专属):从概念到煎药上位机完整落地

MVVM从零到实战学习指南(工控WPF专属):从概念到煎药上位机完整落地 很多做工控上位机的朋友从WinForm转WPF第一个坎就是MVVM。刚接触的时候觉得多此一举不就是拖控件写事件吗为什么非要拆成三层等真正做过多锅位、多页面、长期维护的量产项目就会懂不用MVVM的WPF代码写到后面就是一锅粥界面和逻辑缠在一起改一个功能牵一发而动全身bug查起来效率极低。结合我自己做中药煎药工控项目的落地经验给大家整理一套面向工业控制场景的MVVM完整学习路线从概念到工具从组件到完整项目全程用煎药锅场景举例学完就能直接用到实际项目里。一、先搞懂本质MVVM到底是什么为什么工控必学1.1 从事件驱动到数据驱动WinForm开发是典型的事件驱动点击按钮→触发Click事件→在事件里写逻辑→手动改控件显示。界面和逻辑强绑定控件多了之后代码会极度混乱。MVVM是数据驱动界面绑定数据逻辑只改数据数据变了界面自动刷新。界面和逻辑完全解耦各司其职代码结构清晰可维护性、可测试性指数级提升。1.2 MVVM三层核心架构MVVM本质是把一个应用拆成三层每层职责单一互不干扰flowchart LR View[视图层 Viewbr/界面展示、用户交互br/只做绑定不写业务] --|数据绑定/命令绑定| ViewModel[视图模型层 ViewModelbr/业务逻辑、状态管理br/核心控制层] ViewModel --|数据读写| Model[模型层 Modelbr/数据实体、PLC通信br/硬件交互、数据封装] Model --|数据推送| ViewModelView视图层就是XAML界面只负责显示和用户操作不写任何业务逻辑所有交互都通过绑定完成。ViewModel视图模型层核心中间层包含所有业务逻辑、状态数据、命令实现。它不知道View的存在只对外暴露属性和命令View自己来绑定。Model模型层数据和底层交互层比如PLC通信类、数据实体类、配方管理类不涉及任何界面逻辑。1.3 为什么工控上位机尤其适合MVVM工控软件有几个鲜明特点刚好完美匹配MVVM的优势数据高频刷新温度、压力、状态每秒更新多次数据驱动模式比手动改控件高效、稳定得多。设备点位多十几口甚至几十口煎药锅每个锅对应一套状态用MVVM可以很好地复用代码。业务逻辑复杂工序流转、连锁保护、配方管理逻辑和界面分离后调试、测试、修改都方便。项目周期长量产项目需要长期维护迭代解耦的代码不会越写越乱。二、完整学习路线图6个阶段从入门到落地flowchart TD A[阶段1基础概念扫盲br/理解核心思想不着急写代码] -- B[阶段2工具选型与环境搭建br/选定框架搭好开发环境] B -- C[阶段3核心组件逐个击破br/属性、命令、消息总线逐个实战] C -- D[阶段4完整项目落地br/从零搭建单锅煎药上位机] D -- E[阶段5工控场景进阶优化br/高频刷新、线程、多锅扩展] E -- F[阶段6最佳实践与避坑br/总结踩坑经验形成规范]三、阶段1基础概念扫盲1天搞定这个阶段不用急着写代码先把核心概念理解透避免后面写四不像。INotifyPropertyChanged接口属性通知的底层原理ViewModel能通知界面更新的核心就是这个接口。数据绑定WPF绑定的基础语法、单向绑定、双向绑定、更新触发时机。Command命令为什么用Command代替Click事件命令的CanExecute机制是什么。解耦思想为什么View不能直接访问ViewModel之外的东西为什么要分层。学习建议不用死记硬背知道“是什么、为什么”就行后面写代码的时候会反复加深理解。四、阶段2工具选型为什么优先CommunityToolkit.MvvmMVVM只是一种架构思想有很多现成的框架可以用不用自己从零实现。针对工控场景首推微软官方的CommunityToolkit.Mvvm没有之一。框架特点适配性CommunityToolkit.Mvvm微软官方维护、极致轻量、有源生成器、性能优化好非常适合工控启动快、占用低、稳定Prism功能重、模块化导航依赖注入适合大型企业级软件工控用太臃肿MvvmLight轻量但已停止维护老项目可以用新项目不推荐自己手写实现完全可控但工作量大、容易出bug不推荐重复造轮子CommunityToolkit.Mvvm的核心优势完全命中工控需求官方稳定微软维护兼容.NET 6/7/8/9长期支持有保障极致轻量只有MVVM核心功能没有多余依赖工控软件启动快、内存占用低源生成器特性自动生成代码不用手写大量重复的字段属性高性能针对高频数据更新做了优化适合工控秒级刷新场景弱引用消息总线解耦通信层和业务层不会造成内存泄漏五、阶段3核心组件逐个击破煎药场景实战这个阶段是核心三个核心组件每个都结合煎药锅场景写一遍基本就入门了。3.1 ObservableObject属性通知基类这是所有ViewModel的基类实现了INotifyPropertyChanged接口封装了SetProperty方法。基础写法using CommunityToolkit.Mvvm.ComponentModel; public class DecoctionPotViewModel : ObservableObject { private double _currentTemp; public double CurrentTemp { get _currentTemp; set SetProperty(ref _currentTemp, value); } }SetProperty会自动判断值是否变化变化了就触发PropertyChanged事件界面自动刷新。推荐写法源生成器特性这是最省代码的方式加一个[ObservableProperty]特性编译器自动生成完整属性。using CommunityToolkit.Mvvm.ComponentModel; // 注意类必须是partial才能用源生成器 public partial class DecoctionPotViewModel : ObservableObject { [ObservableProperty] private double _currentTemp; [ObservableProperty] private string _statusText 待机就绪; [ObservableProperty] private string _statusColor #666666; }生成规则字段名_xxx→ 自动生成首字母大写的公开属性Xxx。进阶属性变更回调温度变化后要判断是否超温、自动切换状态可以写OnXXXChanged方法源生成器会自动调用partial void OnCurrentTempChanged(double value) { if (value 100) { StatusText 沸腾保沸; StatusColor #4CAF50; } else if (value 50) { StatusText 武火升温; StatusColor #FF8C00; } }3.2 RelayCommand命令绑定按钮操作不用写Click事件通过Command绑定到ViewModel彻底解耦界面和逻辑。基础写法using CommunityToolkit.Mvvm.Input; public IRelayCommand StartCommand { get; } public DecoctionPotViewModel() { StartCommand new RelayCommand(Start, () !IsRunning); } private void Start() { // 启动煎煮逻辑 IsRunning true; }推荐写法特性自动生成直接给方法加[RelayCommand]特性自动生成对应的命令属性public partial class DecoctionPotViewModel : ObservableObject { [ObservableProperty] private bool _isRunning; // 自动生成 StartCommand 属性 [RelayCommand] private void Start() { IsRunning true; StatusText 武火升温; StatusColor #FF8C00; } // 带CanExecute控制按钮是否可用 [RelayCommand(CanExecute nameof(CanStop))] private void Stop() { IsRunning false; StatusText 待机就绪; StatusColor #666666; } private bool CanStop() IsRunning; }界面直接绑定即可后台代码不用写任何事件Button Content启动煎煮 Command{Binding StartCommand}/ Button Content停止 Command{Binding StopCommand}/异步命令工控里和PLC通信、读写数据库都是异步操作用AsyncRelayCommand不会阻塞UI线程[RelayCommand] private async Task DownloadRecipeAsync() { // 异步从PLC下载处方参数 await Task.Delay(500); }3.3 WeakReferenceMessenger消息总线工控核心这是工控场景最实用的功能用来解耦PLC通信层和ViewModel。通信层收到数据后发消息ViewModel接收消息更新自己双方不用互相引用架构更干净。定义消息类/// summary /// 温度更新消息PLC采集后推送 /// /summary public class TempUpdatedMessage { public int PotId { get; set; } public double Temp { get; set; } }发送消息PLC通信层using CommunityToolkit.Mvvm.Messaging; // 模拟PLC采集到温度广播消息 WeakReferenceMessenger.Default.Send(new TempUpdatedMessage { PotId 1, Temp 85.5 });接收消息ViewModel让ViewModel继承ObservableRecipient注册消息处理public partial class DecoctionPotViewModel : ObservableRecipient { [ObservableProperty] private double _currentTemp; public DecoctionPotViewModel() { // 注册接收温度更新消息 WeakReferenceMessenger.Default.RegisterTempUpdatedMessage(this, (recipient, message) { if (message.PotId 1) { CurrentTemp message.Temp; } }); } }关键优势弱引用机制ViewModel销毁后消息会自动断开不会造成内存泄漏这是很多自研消息总线做不到的。六、阶段4完整实战从零搭建单锅煎药上位机三个组件学完就可以做一个完整的最小可运行项目了全程用煎药锅场景4步就能跑起来。第一步新建项目并安装包打开VS新建WPF项目.NET 6/8都可以NuGet安装CommunityToolkit.Mvvm第二步编写ViewModelusing CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using System.Windows.Threading; namespace MvvmDemo; public partial class MainViewModel : ObservableObject { private readonly DispatcherTimer _timer; public MainViewModel() { _timer new DispatcherTimer { Interval TimeSpan.FromSeconds(1) }; _timer.Tick (_, _) OnTimerTick(); } [ObservableProperty] private double _currentTemp 25.0; [ObservableProperty] private string _statusText 待机就绪; [ObservableProperty] private string _statusColor #666666; [ObservableProperty] private bool _isRunning; [RelayCommand] private void Start() { if (IsRunning) return; IsRunning true; _timer.Start(); StatusText 武火升温; StatusColor #FF8C00; } [RelayCommand(CanExecute nameof(IsRunning))] private void Stop() { if (!IsRunning) return; IsRunning false; _timer.Stop(); StatusText 待机就绪; StatusColor #666666; } private void OnTimerTick() { if (CurrentTemp 100) { CurrentTemp 2; } else { StatusText 沸腾保沸; StatusColor #4CAF50; } } }第三步编写XAML界面Window x:ClassMvvmDemo.MainWindow xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml Title煎药锅MVVM演示 Height350 Width400 Grid Margin30 StackPanel Spacing20 TextBlock Text1号煎药锅 FontSize22 HorizontalAlignmentCenter/ Border Background{Binding StatusColor} CornerRadius12 Padding30 TextBlock Text{Binding CurrentTemp, StringFormat{0:F1} ℃} FontSize56 ForegroundWhite HorizontalAlignmentCenter/ /Border TextBlock Text{Binding StatusText} FontSize16 HorizontalAlignmentCenter/ StackPanel OrientationHorizontal Spacing20 HorizontalAlignmentCenter Button Content启动煎煮 Command{Binding StartCommand} Width120 Height40 FontSize14/ Button Content停止 Command{Binding StopCommand} Width120 Height40 FontSize14/ /StackPanel /StackPanel /Grid /Window第四步绑定DataContextpublic partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext new MainViewModel(); } }按F5运行点击启动温度每秒自动上涨状态文字和背景颜色自动切换全程后台代码没有任何操作控件的逻辑。这就是MVVM的魅力逻辑只关心数据界面自己响应数据变化。七、阶段5工控场景进阶优化基础版跑通之后要应对真实工业场景还有几个必须掌握的优化点。5.1 高频数据节流工控温度、压力一秒钟刷新好几次没必要每次都触发界面更新。加一个简单的阈值判断变化量超过0.1℃再更新既不影响显示又大幅降低UI压力private double _lastTemp; private void UpdateTemp(double value) { if (Math.Abs(value - _lastTemp) 0.1) { _lastTemp value; CurrentTemp value; } }5.2 跨线程更新UIPLC通信一般在后台线程直接更新ViewModel属性可能报线程错误。用Dispatcher调度到UI线程Application.Current.Dispatcher.InvokeAsync(() { CurrentTemp temp; }, DispatcherPriority.Background);5.3 多锅位扩展有了MVVM扩展多锅位非常简单封装一个PotViewModel每个锅对应一个实例主ViewModel里放一个ObservableCollectionPotViewModel界面用ItemsControl绑定集合自动生成所有锅的界面一套代码复用N次比WinForm拖N套控件高效太多。八、阶段6最佳实践与避坑指南结合工控项目踩过的坑总结几个最容易犯的错误类不加partial用源生成器特性时类一定要声明为partial否则编译报错。消息重复注册ViewModel多次实例化时不要重复注册消息。继承ObservableRecipient后用IsActive控制激活/停用更规范。在ViewModel里操作控件这是新手最容易犯的错误一旦在ViewModel里找控件、改控件就回到了事件驱动的老路完全失去了MVVM的意义。过度设计单锅小项目、界面简单的不用硬套完整MVVM。只有多设备、多页面、长期维护的项目收益才最大。忽略内存泄漏用强引用事件、强引用消息总线页面关闭后资源释放不掉工控软件长时间运行会越来越卡。CommunityToolkit.Mvvm的弱引用机制能很好地规避这个问题。总结MVVM不是什么高深的技术本质就是一种分层解耦的编码思想。对于工控上位机开发来说它不是炫技而是提升代码质量、降低维护成本的实用工具。学习的时候不用追求一步到位先从单个ViewModel用起来慢慢体会数据驱动的好处再逐步引入消息总线、多模块拆分。等你用MVVM做完一个完整的煎药产线项目再回头看WinForm那种事件嵌套的代码就会明白为什么这是WPF开发的行业标准。
返回列表