:为什么你的 WPF 程序一改 UI 就崩?——线程亲和性与 Dispatcher)
上一篇我们解决了串口数据粘包/半包的问题。这一篇接着讲数据切出来之后怎么解析怎么通知 UI为什么一更新 UI 就崩溃——涉及两个必懂的概念事件和线程。这里以我实现的项目为例我使用ESP32-S3一种芯片和DHT11(一种温湿度传感器)配合使用来传输数据给上位机我的电脑。一.Analysis分析数据你可能要问了数据不是已经拿到了吗为什么还要分析因为字符串只能看不能用(1)想做比较温度 30 报警→ 需要double类型(2)想存数据库字段拆开存→ 需要独立的Temp/Humidity(3)想画曲线取数值→ 需要double类型所以要把25.0,60.0这种原始字符串结构化成SensorData { Temp25.0, Humidity60.0 }。如下图25.0,60.0是原始数据t:25 h:60是处理后的两条数据。原理因为我们知道前面的数据是温度 而后面的数据是湿度而Split函数可以将一条数据切成两半方案使用Split函数分别得到温度和湿度数据并返回给UI以显示更新代码public SensorData Analysis(string str) { string[] strarr str.Split(,); //以逗号为分隔的标志 将数据一分为二 且抛弃逗号 if (double.TryParse(strarr[0], out double temp) double.TryParse(strarr[1], out double humidity)) //转换成功返回数据 { Debug.WriteLine($t:{temp} h:{humidity}); return new SensorData(temp, humidity); //返回温湿度数据 } return null; //将string类型转换成double 转换失败则返回null。防止脏数据流入 public class SensorData //创建一个新数据类型 用来存储温湿度变量 { public double Temp { set; get; } public double Humidity { set; get; } public SensorData() { } public SensorData(double temp, double humidity) { Temp temp; Humidity humidity; } }二.event 事件 绑定说到这里大家可能就好奇了虽然我们现在可以接收数据也可以解析数据得到直接可以使用的数据了 但是这个获取数据的过程难道要一直while通过不停的循环来实现实际上确实可以通过while循环这一过程来实现。不过我们有更好的办法接下来我们会用到两个重要的方法(1)event(事件)(2)DataReceived(官方api的特定事件)事件主体分为两部分 分别是发布者和订阅者发布者负责当达成某种条件时通知订阅者运行订阅者当收到通知的时候 运行本身代码DataReceived官方写的事件条件当接口收到数据的时候 通知订阅者因此我们可以通过事件实现如下效果知道大概流程即可下面有具体代码第一次事件ComPortService的DataArrived→ 触发DataPond.DataHandle第二次事件DataPond的DisplaySensorData→ 触发MainWindow.DataUpdate这个就是更新UI的函数 和SqliteRepository.QueueAdd看到这里想必聪明的你肯定已经发现了 。如果我们使用 事件和DataReceived 只需要静静等待原始数据道到来原始数据就会像流水线上的产品一样自动变成我们想要的结构化数据。①第一级事件定义 触发 订阅// 发布者ComPortService public event Actionstring DataArrived; // 定义事件谁能收到接收 string 的回调 private void UpData(object sender, SerialDataReceivedEventArgs e) { DataArrived?.Invoke(ReadData()); // 串口收到数据 → 触发事件自动通知订阅者 } // 在构造函数里登记串口一有数据就调 UpData _serialPort.DataReceived UpData; // 订阅者DataPond public DataPond(ICommunicationService icomPortService) { icom icomPortService; icom.DataArrived DataHandle; // 订阅串口来数据了就调 DataHandle }② 第二级事件定义 触发// 发布者DataPond public event ActionSensorData DisplaySensorData; // 定义事件通知拿到 SensorData 了 public void DataHandle(string tempData) { sb.Append(tempData); while (true) { int position sb.ToString().IndexOf(\n); if (position 0) break; if (position 0) { sb.Remove(0, 1); continue; } string frame sb.ToString(0, position).Trim(); sb.Remove(0, position 1); var result tempHumidityAnalysis.Analysis(frame); if (result ! null) DisplaySensorData?.Invoke(result); // 解析成功 → 触发事件通知下游 } }③ 第二级事件的订阅者两个互不知情// 订阅者 1UI 层 // MainWindow.xaml.cs dataPond.DisplaySensorData DataUpdate; // 订阅有数据了就调 DataUpdate // 订阅者 2持久层 // SqliteRepository.cs public SqliteRepository(DataPond dp) { dataPond dp; dataPond.DisplaySensorData QueueAdd; // 订阅有数据了就入队后续写数据库 }三.在event事件里面更新ui导致的线程问题你可能已经猜到了 只要把更新ui也放到事件里面这样连带更新ui都变成自动化的流水线了。但如果真的这么做代码会报错这是为什么呢报错原因只有创造UI的线程才能修改它的属性所以我们只有了解线程才能理解这个问题线程是什么呢线程 操作系统调度的最小执行单元。一个线程 一条独立的执行流。进程 一家工厂线程 工厂的一条流水线共享资源 但各自独立干活一个进程可以有多个线程它们共享内存变量、对象但各自有独立的执行位置当前执行到哪一行代码。这次的UI代码为例,主要涉及的两条线程分别是 UI线程 和 串口回调线程(1)串口回调线程:当串口接收到数据时DataReceived事件被触发 → 调用UpData→UpData触发DataArrived→ 调用DataHandle→DataHandle触发DisplaySensorData→ 调用DataUpdate。整条链都在这一条线程上执行。(2)UI线程:创建UI控件的地方。显而易见串口线程和UI 线程不是同一条。由于WPF 控件内部状态不是线程安全的——如果允许多线程同时修改会出现数据竞争、渲染错乱。所以 WPF 强制规定只有创建控件的线程通常是 UI 线程才能修改它。修改方案其实非常简单只需要使用Dispatcher.BeginInvoke()即可。Dispatcher.BeginInvoke():会把函数回调给UI线程运行。修改UI代码public void DataUpdate(SensorData data) { Dispatcher.BeginInvoke(() { TempUI.Content 温度 data.Temp.ToString()℃; HumidityUI.Content 湿度 data.Humidity.ToString()%RH; DateTime nowtime DateTime.Now; _tempValues.Add(new DateTimePoint(nowtime, data.Temp)); _humidityValues.Add(new DateTimePoint(nowtime, data.Humidity)); if (_tempValues.Count 100) { _tempValues.RemoveAt(0); _humidityValues.RemoveAt(0); } }); }四.完整代码反抗者z/SimpleHMI-TempHumidityhttps://gitee.com/rebel-z/simple-hmi五.总结这一篇讲了三个东西解析字符串 → 结构化。用Split(,)double.TryParse失败返回 null 防脏数据。事件不用 while 轮询。发布者定义事件 触发订阅者登记。你项目里两层事件链串口→切帧层切帧层→UI/持久层。跨线程 UI控件有线程亲和性非 UI 线程改控件会崩。用Dispatcher.BeginInvoke把动作切回 UI 线程。一个容易忽略的点我用的是BeginInvoke不是Invoke。区别是Invoke会阻塞调用线程这里是串口线程等 UI 更新完才返回BeginInvoke只是把动作塞进 UI 线程的消息队列立刻返回不阻塞串口线程。串口线程不卡才能及时处理下一帧数据。下一篇讲串口数据怎么不丢地落盘到数据库——引入BlockingCollection队列做生产者-消费者解耦。我是反抗者Z一个正在往 C# 上位机方向走的开发者。如果这篇对你有帮助欢迎在 Gitee 点个 Star。系列文章C# 上位机开发一串口数据总是「半截」或「粘一坨」——粘包/半包彻底解决-CSDN博客https://blog.csdn.net/YYDS1683/article/details/167038364?spm1001.2014.3001.5501C# 上位机开发二为什么你的 WPF 程序一改 UI 就崩——线程亲和性与 Dispatcher-CSDN博客https://blog.csdn.net/YYDS1683/article/details/167076907?spm1001.2014.3001.5501C# 上位机开发三串口丢帧、CPU 冒烟、连接竞态——一个队列全搞定-CSDN博客https://blog.csdn.net/YYDS1683/article/details/167084164?sharetypeblogdetailsharerId167084164sharereferPCsharesourceYYDS1683spm1011.2480.3001.8118