跳到主要内容

WPF 适配器

WPF 只走 MVVM。Zeus.Presentation.Wpf 负责窗口生命周期和 Dispatcher 封送;界面状态通过 ChannelBindingSource、PointBindingSource、PointTableBindingSource、PointAlarmBindingSource 暴露给 ViewModel,再由 XAML 绑定。

public partial class MainWindow : Window
{
private readonly MainWindowViewModel _viewModel;

public MainWindow(IZeusHost host, MainWindowViewModel viewModel)
{
InitializeComponent();
this.AttachZeus(host);
_viewModel = viewModel;
DataContext = _viewModel;
Closed += (_, _) => _viewModel.Dispose();
}
}
<TextBlock Text="{Binding Meter.StateText}" />
<TextBlock Text="{Binding Meter.LastText}" />
<TextBlock Text="{Binding Temperature.Text}" />
<TextBlock Text="{Binding Temperature.AlarmState}" />

程序集:Zeus.Presentation.Wpf(目标框架 net8.0-windows)。

API​

API作用
Window.AttachZeus(...)默认窗口 Loaded 时启动,Closed 时释放
host.AttachManually()只创建挂接,不绑窗口;适合登录后进入主界面或托盘常驻
WpfUiDispatcher.Current()捕获当前 WPF Dispatcher,给 ViewModel 的绑定源使用
host.Bind(dispatcher)只传一次调度器,后面用 Channel / Point / Table / Alarms
channel.AsBindingSource(dispatcher)单条通道投影;多个源时优先用 host.Bind
points.AsBindingSource("temperature", dispatcher)单点投影;多个点时优先用 _ui.Point(...)
points.AsTableBindingSource(dispatcher)整张点表投影,按 BatchChanged 批量刷新
alarms.AsAlarmBindingSource(dispatcher)活动报警队列投影

WPF 包只保留 MVVM 所需能力。控件状态应从 ViewModel 属性或命令派生。

ViewModel 绑定源​

ViewModel 用 host.Bind(dispatcher) 只传一次调度器。后面的通道和点都从上下文取,不必每个点再写 dispatcher;Dispose 上下文即可释放全部投影。

public sealed class MainWindowViewModel : IDisposable
{
private readonly ZeusBindingContext _ui;

public MainWindowViewModel(IZeusHost host, IUiDispatcher dispatcher)
{
_ui = host.Bind(dispatcher);
Meter = _ui.Channel("meter");
Temperature = _ui.Point("temperature", value => $"{value:0.0} C");
}

public ChannelBindingSource Meter { get; }

public PointBindingSource Temperature { get; }

public void Dispose() => _ui.Dispose();
}

ChannelBindingSource 的常用属性:Name、State、StateText、LastText、LastHex、ReceivedCount。

PointBindingSource 的常用属性:Name、QualifiedName、Value、Text、Error、HasError、AlarmState、IsAlarmed、UpdatedAt、Writable。

命令与写回​

发送通道数据和点表写回都放在 ViewModel 命令里。推荐安装 CommunityToolkit.Mvvm,用 [RelayCommand] 生成 XAML 可绑定命令。

[RelayCommand]
private async Task SendAsync()
{
await _meter.WriteAsync(Encoding.UTF8.GetBytes(Payload));
}

[RelayCommand]
private async Task WriteSetpointAsync()
{
await Setpoint.WriteAsync(80.0);
}
<TextBox Text="{Binding Payload, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="发送" Command="{Binding SendCommand}" />

表格、趋势和报警​

一屏只有温度、设定值这类几个点时,继续用上面的 AsBindingSource("temperature")。DataGrid、总貌或一屏很多点时,改用整表绑定源:它只是把内核的 BatchChanged 投影成可绑定列表,一轮采集只进 UI 线程一次。不要给每个点各建一个投影,也不要在每个 Changed 里清空重加。

整表 DataGrid 绑定 PointTableBindingSource.All。注意它每轮会拷贝整表;点很少时,这比改几个 TextBlock 更重。写回输入框仍用单点投影,避免整表刷新盖掉正在编辑的值。点表事件本身与界面无关,对照见 点表与采集。

Points = host.Points.AsTableBindingSource(dispatcher);
Alarms = host.Alarms.AsAlarmBindingSource(dispatcher);
<DataGrid ItemsSource="{Binding Points.All}" />
<ItemsControl ItemsSource="{Binding Alarms.Active}" />

趋势图或第三方图表由 ViewModel 订阅 host.Points.BatchChanged 后自行维护序列;单点调试曲线才订 Changed。Zeus 不内置点历史缓存,也不绑定 WPF 图表库。

接入已有 Prism / DI 项目​

已有 Prism、DryIoc、Unity 或自建容器时,不需要让 Zeus 接管整个应用。推荐把 Zeus 注册成单例服务,ViewModel 依赖 IZeusHost、IChannelRegistry 或 IPointTable。

var zeus = ZeusHost.Create(builder =>
{
builder.AddSerialPort("meter", "COM3", 9600);
builder.AddAcquisition(TimeSpan.FromMilliseconds(500));
});

containerRegistry.RegisterInstance<IZeusHost>(zeus);
containerRegistry.RegisterInstance<IChannelRegistry>(zeus.Channels);
containerRegistry.RegisterInstance<IPointTable>(zeus.Points);
containerRegistry.RegisterInstance<IUiDispatcher>(WpfUiDispatcher.Current());
containerRegistry.Register<MainWindowViewModel>();

应用启动后调用 StartAsync,退出时释放宿主。ViewModel 通过构造函数接收 IZeusHost、IPointTable 或 IUiDispatcher,不要在窗口代码里手动 new ViewModel。

常见问题​

  • 属性没有刷新:确认 ViewModel 里给绑定源传了 WpfUiDispatcher.Current(),不要在后台线程捕获 Dispatcher。
  • 按钮状态不变:给 [RelayCommand] 配置 CanExecute,通道状态变化时调用生成命令的 NotifyCanExecuteChanged(),或暴露 ViewModel 布尔属性。
  • DataGrid 频繁闪烁:优先使用 AsTableBindingSource,不要在每个 Changed 事件里清空重加。
  • 已有容器怎么接:把 IZeusHost、IChannelRegistry 或 IPointTable 注册成单例,窗口只负责设置 DataContext。