首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >WPF 上位机的工程化实践:模块化隔离硬件,反应式驯服数据流

WPF 上位机的工程化实践:模块化隔离硬件,反应式驯服数据流

原创
作者头像
用户12339161
发布于 2026-10-26 09:54:06
发布于 2026-10-26 09:54:06
480
举报

上位机与普通桌面应用的差别在于:硬件型号会换、协议会改、采样频率会变,而 UI 必须始终稳定地反映设备状态。传统 WPF 上位机把串口句柄、轮询定时器和 UI 更新揉进一个 ViewModel,换一个 PLC 品牌就要重写界面层。模块化解决“结构耦合”,反应式解决“时序与线程耦合”,两者结合,才能让上位机在扩展性与响应性上同时达标。以下基于 Prism + ReactiveUI。

模块化:用 IModule 把硬件关进独立房间

每个设备模块只暴露契约,Shell 对具体驱动一无所知。模块在注册阶段完成服务绑定,在初始化阶段把视图注入 Region:

代码语言:javascript
复制
public sealed class PlcModule : IModule
{
    public void RegisterTypes(IContainerRegistry c)
    {
        c.RegisterSingleton<IDeviceDriver, ModbusTcpDriver>();
        c.Register<DeviceViewModel>();
        c.Register<Views.DeviceView>();
    }

    public void OnInitialized(IContainerProvider p)
    {
        p.Resolve<IRegionManager>()
         .RegisterViewWithRegion("MainRegion", typeof(Views.DeviceView));
    }
}

IDeviceDriver 抽象“连接、采样、下发”三个语义,并把采样暴露为冷/热流:

代码语言:javascript
复制
public interface IDeviceDriver
{
    string DeviceId { get; }
    IObservable<DeviceSample> Samples { get; }   // 反应式的天然入口
    Task ConnectAsync(CancellationToken ct);
    Task DisconnectAsync();
    Task WriteAsync(DeviceCommand cmd, CancellationToken ct);
}

更换 PLC 品牌时,只需新增一个实现并在模块中替换注册行,ViewModel 与 Shell 零改动。跨模块禁止直接引用,一律通过 IEventAggregator 或共享事件流通信。

反应式:把采样流变成可调度、可组合的数据管道

上位机的本质是对持续变化的数据流做出反应。WhenAnyValue 把属性变化暴露为 IObservable<T>,ReactiveCommand 把异步操作与可用性绑定,调度器把后台采样切回 UI 线程:

代码语言:javascript
复制
public class DeviceViewModel : ReactiveObject, IDisposable
{
    private readonly IDeviceDriver _driver;
    private readonly IEventAggregator _events;
    private readonly CompositeDisposable _subs = new();

    [Reactive] public double Value { get; private set; }
    [Reactive] public DeviceState State { get; private set; }
    [Reactive] public bool IsAlarm { get; private set; }

    public ReactiveCommand<Unit, Unit> ConnectCommand { get; }
    public ReactiveCommand<Unit, Unit> DisconnectCommand { get; }

    public DeviceViewModel(IDeviceDriver driver, IEventAggregator events)
    {
        _driver = driver;
        _events = events;

        var canConnect = this.WhenAnyValue(x => x.State)
            .Select(s => s == DeviceState.Disconnected);
        var canDisconnect = this.WhenAnyValue(x => x.State)
            .Select(s => s is DeviceState.Running or DeviceState.Alarm);

        ConnectCommand = ReactiveCommand.CreateFromTask(ConnectAsync, canConnect);
        DisconnectCommand = ReactiveCommand.CreateFromTask(DisconnectAsync, canDisconnect);

        // 采样流:Sample 限频,ObserveOn 切回 UI 线程,避免高频刷新卡死界面
        _driver.Samples
            .Sample(TimeSpan.FromMilliseconds(100))
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(s => { Value = s.Value; State = s.State; })
            .DisposeWith(_subs);

        // 报警流:滑动窗口,1 秒内持续超限才触发,抑制瞬时抖动
        _driver.Samples
            .Select(s => s.Value)
            .Buffer(TimeSpan.FromSeconds(1), 10)
            .Where(b => b.Count > 0 && b.All(v => v > 85.0))
            .Throttle(TimeSpan.FromSeconds(2))
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(_ => IsAlarm = true)
            .DisposeWith(_subs);
    }

    private async Task ConnectAsync()
    {
        await _driver.ConnectAsync(CancellationToken.None);
        State = DeviceState.Running;
        _events.GetEvent<DeviceConnectedEvent>().Publish(_driver.DeviceId);
    }

    public void Dispose() => _subs.Dispose();   // 视图销毁时解除全部订阅
}

四个工程要点:Sample 抑制高频采样造成的 UI 刷新压力;Buffer + Where 用滑动窗口实现“持续超限才报警”,比单点阈值更抗抖动;ObserveOn 保证属性通知在 UI 线程执行;DisposeWith + Dispose 确保订阅随视图生命周期回收——上位机长期运行,订阅泄漏是最隐蔽的生产事故。

协同:新增报警策略无需触碰采集与 UI

假设新增“温度超限自动停机”策略:把它做成独立 SafetyModule,订阅共享采样事件流,满足条件后发布急停事件:

代码语言:javascript
复制
public sealed class SafetyModule : IModule
{
    public void RegisterTypes(IContainerRegistry c) { }
    public void OnInitialized(IContainerProvider p)
    {
        var bus = p.Resolve<IEventAggregator>();
        var lifetime = p.Resolve<IDisposable>();   // 模块生命周期

        bus.GetEvent<DeviceSampleEvent>().GetEventStream()
           .Where(e => e.Value > 85)
           .Throttle(TimeSpan.FromSeconds(1))
           .SelectMany(e => Observable.FromAsync(
               ct => p.Resolve<ISafetyService>().EmergencyStopAsync(e.DeviceId, ct)))
           .Subscribe(_ => bus.GetEvent<EmergencyStopEvent>().Publish(),
                      ex => p.Resolve<ILogger>().LogError(ex, "safety stop failed"))
           .DisposeWith(lifetime);
    }
}

采集模块与 UI 模块一行不改。模块边界由 IModule 守住,数据边界由 Observable 流守住。新增能力 = 新增模块 + 订阅已有事件流,这正是模块化与反应式协同的兑现点。

结语

上位机架构的价值不在于用了多少框架,而在于变更发生时需要触碰多少文件。当更换硬件只需改一个模块类、调整报警逻辑只需改一条流操作符、订阅生命周期自动随模块回收时,这套组合才真正兑现了工程承诺。模块化定结构,反应式定数据流,二者缺一,上位机都只是“能跑”,而非“可演进”。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 模块化:用 IModule 把硬件关进独立房间
  • 反应式:把采样流变成可调度、可组合的数据管道
  • 协同:新增报警策略无需触碰采集与 UI
  • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档