DataChanged 事件
OpcUaSubscription.DataChanged 事件在 MI 数据变化时由服务端推送触发.
public event EventHandler<DataChangeEventArgs> DataChanged;
关联事件
- 全局统一通道见
ua.Events.DataChange. - OPC UA 报警 / 条件事件 (协议事件) 见 事件订阅.
DataChangeEventArgs
| 字段 | 类型 | 说明 |
|---|---|---|
MonitoredItemHandle | uint | 触发的 MI 本地句柄 |
NodeId | string | 该 MI 的 NodeId |
ValueString | string | 值的字符串表示 (在 C 层同步抽取, 跨线程安全) |
DataTypeName | string | 内置数据类型枚举名 (如 Double, Int32) |
Status | StatusCode | DataValue 的 Status |
SourceTimestamp | DateTime? | 数据源时间戳 |
ServerTimestamp | DateTime? | 服务端时间戳 |
设计权衡
历史上回调里曾暴露 DataValue 原始指针, 但因为 native 内存生命周期跨线程, 经常 use-after-free 闪退. 现在 SDK 在 Publish 线程内同步抽取所有字段为扁平 C# 对象 (字符串 + 枚举 + DateTime?), 跨线程使用零风险, 但代价是不能拿原始 Variant 做高级操作.
如果需要原始 Variant (如解析复杂 ExtensionObject), 在回调里用 ua.Read(e.NodeId) 同步重读一次 — 比维护 native 生命周期可靠.
例子
简单日志
sub.DataChanged += (s, e) =>
Console.WriteLine($"{e.NodeId} = {e.ValueString} ({e.DataTypeName}) @ {e.SourceTimestamp}");
写文件 / 数据库
sub.DataChanged += (s, e) =>
{
lock (_writeLock)
{
File.AppendAllText("data.csv",
$"{e.SourceTimestamp:O},{e.NodeId},{e.ValueString}\n");
}
};
UI 绑定 (WPF / WinForms)
sub.DataChanged += (s, e) =>
{
Dispatcher.Invoke(() =>
{
labelTemp.Text = e.ValueString;
labelStatus.Foreground = e.Status == StatusCode.Good
? Brushes.Green : Brushes.Red;
});
};
必须 Dispatcher.Invoke
否则跨线程 UI 操作抛 InvalidOperationException.
线程模型
- 回调在 C 层 Publish 线程执行, 单线程串行 (同一订阅内部不会并发)
- 回调里不要执行长操作 (>100 ms), 否则会阻塞 Publish 队列
- 回调里抛异常会被 SDK 吃掉 (catch + 写 SDK 日志), 不会传播到 UI 线程
- 跨订阅的回调在不同 Publish 线程, 可能并发 — 共享数据自己加锁
与 ua.Events.DataChange 的关系
// 方式 A: 单订阅级
sub.DataChanged += (s, e) => /* ... */;
// 方式 B: 全局统一通道 (所有订阅汇集)
ua.Events.DataChange += (s, e) => /* ... */;
两个事件对同一数据点都会触发, 选一个用即可.