项目背景

工业 HMI 往往从少量按钮和状态灯开始,随后不断增加配方、权限、报警、趋势、设备插件和生产追踪。如果所有逻辑都堆在主窗口 ViewModel 中,项目会在功能增长后迅速失去边界。

这份匿名化示例展示一种面向长期维护的 WPF 结构:界面只表达操作意图,应用层编排用例,设备宿主负责通信生命周期,基础设施层保存数据和日志。

系统约束

  • 应用需要连续运行,页面切换不能影响设备连接;
  • 设备型号可能不同,但应共享启动、停止、复位和状态能力;
  • UI 必须在通信超时后仍可响应;
  • 配方修改要校验、留痕,并在下发前形成不可变快照;
  • 报警确认不等同于故障已经消失。

我的职责

示例职责覆盖解决方案分层、ViewModel 约定、设备插件接口、统一异常处理、数据库迁移和关键页面性能。目标不是追求抽象数量,而是让设备、业务和展示层能够独立测试与替换。

技术架构

Presentation
  Views ── ViewModels ── UI State


Application
  Commands ── Use Cases ── Validation


Domain / Devices
  DeviceHost ── Capabilities ── State Snapshot


Infrastructure
  Protocols ── EF Core / SQLite ── Logging

ViewModel 不持有串口、Socket 或数据库上下文。它依赖用例接口,接收为 UI 设计的状态模型。设备层也不知道按钮和页面是否存在,只发布状态与事件。

核心模块

DeviceHost 与 Capability

DeviceHost 管理连接、心跳、状态聚合和停止顺序。具体能力用小接口表达,例如 IMotionCapabilityIIoCapabilityIRecipeCapability。不支持某项能力的设备不会返回空对象,而是在注册阶段明确其能力集合。

配方工作流

编辑中的配方和已批准配方分开。下发操作使用版本化快照,并记录设备确认值。这样可以区分“数据库保存成功”和“设备已经应用”两个不同事实。

报警模型

报警至少包含代码、来源、严重级别、发生时间、活动状态、确认状态和复位条件。UI 可以过滤和确认,但只有设备状态恢复后,活动报警才真正结束。

技术难点

问题一:后台线程更新 UI

现象

设备事件频繁到达时,界面偶发跨线程异常;直接把每条采样都切回 Dispatcher,又会让消息队列积压。

原因

通信频率与界面刷新频率被当作同一件事。ViewModel 对每个原始事件逐一通知,造成大量属性变更和 UI 布局。

方案

设备层保留最新状态快照,展示层按固定节奏读取或合并变更。只有报警、状态转换等离散事件立即投递到 UI;高频采样采用限频更新。所有 UI 调度集中在一个适配器中,而不是散落在业务代码里。

问题二:异步命令无法正确取消

现象

操作员切换产品后,上一条配方下发任务仍可能继续执行,最终覆盖新状态。

原因

异步命令只防止重复点击,却没有把取消令牌传到协议层,也没有在完成时校验任务版本。

方案

每次业务操作生成操作 ID 和取消源。新操作开始前取消旧操作,协议调用全链路接受令牌;返回结果只有在操作 ID 仍匹配当前上下文时才允许提交到 UI 状态。

数据与迁移

SQLite 保存配方、用户操作记录和追踪索引,EF Core 负责映射与迁移。数据库上下文使用短生命周期,每个用例完成一次明确事务,避免把长生命周期 DbContext 绑定到 ViewModel。

最终结果

最终结构让页面、用例和设备驱动之间只通过明确接口交互。新设备优先实现能力接口,新页面复用应用用例;模拟设备可以覆盖连接失败、超时和异常状态,而不需要启动真实硬件。

项目复盘

MVVM 的价值不在于目录名称,而在于界面生命周期不再决定业务对象的生命周期。真实项目发布时,应补充性能数据、测试范围和本人实际负责的模块,并删除客户与设备敏感信息。