当前位置: 首页 > news >正文

【C#】观察者模式 + UI 线程调度、委托讲解

观察者模式 + UI 线程调度”的典型应用


A. 涉及的知识点(抽象)

  1. 观察者模式(Observer Pattern)

    • 发布者DemoDeviceService.cs 内部生成一帧数据 ScopeFrame,通过 OnScopeFrame?.Invoke(frame) 发布事件。
    • 订阅者:UI 在 Form1 里订阅 OnScopeFrame,收到后处理。
    • 作用:发布者和订阅者解耦,谁关心就订阅,不关心就不订阅。
  2. .NET 事件机制

    • event Action<ScopeFrame> OnScopeFrame; 是强类型事件。
    • 发布者只需要 Invoke(frame),订阅方自动收到。
    • 支持多订阅者(UI 可以同时有多个模块监听)。
  3. 线程调度与 UI 安全

    • 事件触发在后台任务线程里,但 WinForms 控件只能在 UI 线程更新。
    • 订阅处理时用 BeginInvoke 切回 UI 线程,保证线程安全。
  4. 数据帧抽象

    • 推送的数据被打包成 ScopeFrame,而不是裸数组,这样一帧数据的元信息(通道数、采样率、时间戳)都随事件传递,方便上层统一处理。
    • 这是“数据契约”的思想:上下游通过一个约定好的对象交互。

B. 可复用的“套路总结”

** 模板**:
👉 “后台产生数据 → 打包成帧/消息对象 → 事件广播 → UI 或其它订阅者收到 → 用 BeginInvoke/Dispatcher.Invoke 切回安全线程更新界面。”

步骤:

  1. 定义一个 数据承载类(类似 ScopeFrame)。
  2. 在后台服务类里定义 public event Action<T>
  3. 产生数据时调用 OnEvent?.Invoke(new T(...))
  4. UI 层 service.OnEvent += e => BeginInvoke(() => Handle(e));

C. 举个复用的例子:心率监控仪 UI

假设我们采集心电信号,每秒推送一次平均心率。

  1. 数据帧定义

    public class HeartRateFrame {public int BPM { get; }public DateTime Timestamp { get; }public HeartRateFrame(int bpm, DateTime ts) { BPM = bpm; Timestamp = ts; }
    }
    
  2. 后台服务

    public class HeartRateService {public event Action<HeartRateFrame> OnHeartRate;public async Task StartAsync(CancellationToken ct) {var rand = new Random();while (!ct.IsCancellationRequested) {int bpm = rand.Next(60, 100);OnHeartRate?.Invoke(new HeartRateFrame(bpm, DateTime.Now));await Task.Delay(1000, ct);}}
    }
    
  3. 前台订阅

    _hrService.OnHeartRate += f => BeginInvoke(new Action(() => lblHeartRate.Text = $"{f.BPM} BPM"));
    

D. 为什么要这样设计?

  • 松耦合:后台服务不依赖 UI,UI 也不用知道后台如何采集。
  • 可扩展:多个 UI 组件(实时曲线、报警灯、日志面板)都可以同时订阅 OnScopeFrameOnHeartRate
  • 安全:通过 BeginInvoke 保证 UI 线程安全。
  • 一致性ScopeFrame/HeartRateFrame 封装上下文信息,接口清晰。

总结
这个“ScopeFrame 订阅机制”本质是 观察者模式 + UI 线程切换 + 数据帧契约
复用时,你只要换掉帧里承载的数据类型(心率帧、温度帧、网络包帧……),其它套路一模一样。


补充

public event Action<T> SomethingHappened;

背后串起了 3 个核心知识点:委托(delegate)事件(event)观察者模式(发布/订阅)。下面用“教学版”把它讲透,并结合你现在的 OnScopeFrame 用法。


1) Action<T> 是什么?—— 委托(delegate)

  • 委托是“方法的类型”。它描述“能被调用的方法长什么样”。
  • Action<T> 是 .NET 内置的泛型委托:表示接受一个 T 参数、没有返回值的方法签名。
    常见的还有 Action(无参)、Action<T1,T2>Func<TIn,TOut>(有返回值)等。
  • 你项目里:Action<ScopeFrame> 就代表“入参是 ScopeFrame、无返回值”的回调。

小练习:任何能写成 void Handle(T x) 的方法/lambda,都能赋给 Action<T>

void HandleFrame(ScopeFrame f) { /* ... */ }
Action<ScopeFrame> handler = HandleFrame;
// 或者
Action<ScopeFrame> handler2 = f => Console.WriteLine(f.Length);

2) event 是什么?—— 事件(event)

  • event 基于委托,加了一层封装/访问控制

    • 类外部只能 += 订阅、-= 退订;不能直接调用、不能覆盖整个委托字段。
    • 类内部(发布者)才可以触发(Invoke)。
  • 目的:防止外部把你的回调列表清空/替换,或私自触发事件,保证类的不变式与封装性。

对比:

public Action<ScopeFrame> OnScopeFrame;      // 裸委托(不安全)
public event Action<ScopeFrame> OnScopeFrame; // 事件(外部只能 += / -=)

3) 它实现了什么模式?—— 观察者(发布/订阅)

  • 发布者(Subject):在合适时机 OnScopeFrame?.Invoke(frame)
  • 订阅者(Observer)_device.OnScopeFrame += f => BeginInvoke(() => PushFrameToDyn(f));
  • 多播委托:一个事件可以挂多个处理器,触发时会逐个调用(调用链)。

这正是你现在用的套路:设备服务“发布帧”,前台多个模块(示波、记录、告警…)都可以各自订阅


4) 触发与订阅:正确姿势

触发(发布者内部)

// C# 6+ 推荐写法:空条件调用,避免空引用
OnScopeFrame?.Invoke(frame);

订阅(UI/消费者)

_device.OnScopeFrame += f =>BeginInvoke(new Action(() => PushFrameToDyn(f))); // 切回 UI 线程

线程要点:事件经常在后台线程触发;WinForms/WPF 控件只能在UI 线程操作 → 用 BeginInvoke/Dispatcher.Invoke 切回。


5) 进阶要点(面试/实战都常用)

  • 异常隔离:某个订阅者抛异常会中断后续订阅者的调用。稳妥做法:发布者遍历调用列表,逐个 try/catch

    var handlers = OnScopeFrame; // 拷贝引用
    if (handlers != null)foreach (Action<ScopeFrame> h in handlers.GetInvocationList())try { h(frame); } catch (Exception ex) { Log(ex); }
    
  • 内存泄漏:长生命周期发布者(单例/后台服务)↔ 短生命周期订阅者(窗体/控件)——要记得 -= 退订;或采用“弱事件”方案。

  • 命名与约定:.NET 传统是 EventHandler<TEventArgs> 模式:
    event EventHandler<MyEventArgs> Something;,签名固定 (object sender, TEventArgs e)
    你现在用 Action<T> 更简洁,团队内部统一即可。

  • 泛型协变/逆变Action<in T>T逆变的(了解即可)。


6) “对照理解”

  • 现在的定义:

    public event Action<ScopeFrame> OnScopeFrame;
    

    表示“有一件事发生了(新的一帧数据)”,订阅者拿到 ScopeFrame 自己处理。

  • 触发点(设备服务里):

    var frame = new ScopeFrame(ch1, ch2, fs, sw.ElapsedTicks);
    OnScopeFrame?.Invoke(frame);
    

    ——把“一帧数据的契约”一起广播出去(通道数组、采样率、时间戳),订阅方解耦。

  • 订阅点(UI):

    _device.OnScopeFrame += f => BeginInvoke(() => PushFrameToDyn(f));
    

    ——收到就切回 UI 线程,批量刷新 ScottPlot。


7) 可复用的模板

// ① 定义事件(发布者)
public class DataSource<T>
{public event Action<T> OnData;public void Produce(T value) => OnData?.Invoke(value);
}// ② 订阅(消费者,WinForms 中)
_dataSource.OnData += v => BeginInvoke(new Action(() => Handle(v)));// ③ 退订(释放时)
_dataSource.OnData -= Handle; // 如果是具名方法

替换 T 就能复用到:日志行、温度帧、相机图像帧、网络包、心率、GPS……任何“源源不断的数据”。


8) 一个复用示例:温度监控

public sealed class TemperatureFrame
{public double Value { get; }public DateTime Time { get; }public TemperatureFrame(double v, DateTime t) { Value = v; Time = t; }
}public class TempService
{public event Action<TemperatureFrame> OnTemperature;public async Task StartAsync(CancellationToken ct){var rand = new Random();while (!ct.IsCancellationRequested){var f = new TemperatureFrame(20 + rand.NextDouble()*5, DateTime.Now);OnTemperature?.Invoke(f);await Task.Delay(1000, ct);}}
}// UI
_tempService.OnTemperature += f =>BeginInvoke(new Action(() => lblTemp.Text = $"{f.Value:F1} ℃"));

小结

public event Action<T> = 用强类型事件把“某类消息 T”广播出去;外部只能订阅/退订,不能乱触发;配合 UI 线程切换,天然实现“发布/订阅 + 解耦 + 线程安全”。

http://www.dtcms.com/a/345008.html

相关文章:

  • vuex如何在js文件中使用
  • NVIDIA GB200 架构详解及与 B200/H200/H100 的区别
  • 【芯芯相印】芯片设计生产全流程核心技术术语与实践指南:从架构定义到量产交付的完整图谱
  • NLP学习之Transformer(2)
  • 数据预处理学习笔记
  • Thunderbird 将推出在德国托管的加密电子邮件服务
  • Android Jetpack | Hilt
  • 快速了解深度学习
  • 数学建模--Topsis(Python)
  • 学习python第12天
  • 第5.3节:awk数据类型
  • gcc 和 make 命令
  • 机试备考笔记 17/31
  • 打工人日报20250822
  • Redis 部署模式深度对比与选型指南
  • 计算机毕设大数据方向:电信客户流失数据分析系统技术实现详解
  • ​如何用 Windows 10 ISO 文件重装系统?U盘安装教程(附安装包下载)
  • Kubernetes 调度器 详解
  • 加密货币与区块链:六大刑事重灾区
  • Vue3源码reactivity响应式篇之Reactive
  • 阿里云日志服务与Splunk集成方案(Splunk Add-on方式)实战
  • GitGithub相关(自用,持续更新update 8/23)
  • 通义万相:AI生视频提示词生成秘籍/指南
  • 高空作业智能安全带如何监控使用异常行为
  • Linux 下的网络编程
  • Linux笔记8——shell编程基础-2
  • ROS学习笔记1-幻宇机器人为模板
  • Windows11 家庭版永久解密BitLocker加密移动硬盘
  • 【Java并发编程】Java多线程深度解析:状态、通信与停止线程的全面指南
  • RK3506-PWM计数功能