diff --git a/DMS.WPF/ViewModels/Dialogs/ImportOpcUaDialogViewModel.cs b/DMS.WPF/ViewModels/Dialogs/ImportOpcUaDialogViewModel.cs index 47f38eb..60b36a1 100644 --- a/DMS.WPF/ViewModels/Dialogs/ImportOpcUaDialogViewModel.cs +++ b/DMS.WPF/ViewModels/Dialogs/ImportOpcUaDialogViewModel.cs @@ -3,7 +3,6 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DMS.Core.Enums; using DMS.Core.Helper; -using DMS.Core.Models; using DMS.Helper; using DMS.Infrastructure.Interfaces.Services; using DMS.Infrastructure.Models; @@ -12,198 +11,387 @@ using Opc.Ua; using Opc.Ua.Client; using System.Collections; using System.Collections.ObjectModel; -using static Dm.net.buffer.ByteArrayBuffer; namespace DMS.WPF.ViewModels.Dialogs; -public partial class ImportOpcUaDialogViewModel : DialogViewModelBase> +/// +/// OPC UA导入对话框的视图模型 +/// 负责处理OPC UA服务器连接、节点浏览和变量导入等功能 +/// +public partial class ImportOpcUaDialogViewModel : DialogViewModelBase>, IDisposable { + /// + /// OPC UA服务器端点URL + /// [ObservableProperty] private string _endpointUrl = "opc.tcp://127.0.0.1:4855"; // 默认值 + /// + /// OPC UA根节点 + /// [ObservableProperty] private OpcUaNodeItemViewModel _rootOpcUaNode; + /// + /// 当前选中节点下的所有变量集合 + /// [ObservableProperty] - private ObservableCollection _opcUaNodeVariables = new ObservableCollection(); + private ObservableCollection _opcUaNodeVariables = new(); + /// + /// 用户选择的变量列表 + /// [ObservableProperty] private IList _selectedVariables = new ArrayList(); + + /// + /// 是否全选变量 + /// [ObservableProperty] private bool _selectAllVariables; + /// + /// 是否已连接到OPC UA服务器 + /// [ObservableProperty] private bool _isConnected; + /// + /// 连接按钮显示文本 + /// [ObservableProperty] private string _connectButtonText = "连接服务器"; + /// + /// 连接按钮是否可用 + /// [ObservableProperty] private bool _isConnectButtonEnabled = true; + /// + /// 当前选中的OPC UA节点 + /// [ObservableProperty] - private OpcUaNodeItemViewModel _currentOpcUaNodeItem; - - private Session _session; + [NotifyCanExecuteChangedFor(nameof(FindCurrentNodeVariablesCommand))] // 当选中节点改变时通知查找变量命令更新可执行状态 + private OpcUaNodeItemViewModel _selectedNode; + /// + /// OPC UA服务接口实例 + /// private readonly IOpcUaService _opcUaService; + + /// + /// 对象映射器实例 + /// private readonly IMapper _mapper; - private CancellationTokenSource _cancellationTokenSource; + + /// + /// 取消令牌源,用于取消长时间运行的操作 + /// + private readonly CancellationTokenSource _cancellationTokenSource; + /// + /// 构造函数 + /// 初始化ImportOpcUaDialogViewModel实例 + /// + /// OPC UA服务接口实例 + /// 对象映射器实例 public ImportOpcUaDialogViewModel(IOpcUaService opcUaService, IMapper mapper) { - this._opcUaService = opcUaService; - this._mapper = mapper; + _opcUaService = opcUaService; + _mapper = mapper; + // 初始化根节点 RootOpcUaNode = new OpcUaNodeItemViewModel() { DisplayName = "根节点", NodeId = Objects.ObjectsFolder, IsExpanded = true }; + // 初始化取消令牌源 _cancellationTokenSource = new CancellationTokenSource(); - } + /// + /// 查找当前节点变量命令的可执行条件 + /// 只有在已连接且有选中节点时才可执行 + /// + public bool CanFindCurrentNodeVariables => IsConnected && SelectedNode != null; + + /// + /// 连接到OPC UA服务器命令 + /// 负责建立与OPC UA服务器的连接并加载根节点信息 + /// [RelayCommand] private async Task Connect() { try { - // 断开现有连接 - if (!_opcUaService.IsConnected) - { - await _opcUaService.ConnectAsync(EndpointUrl); - } + // 更新UI状态:禁用连接按钮并显示连接中状态 + IsConnectButtonEnabled = false; + ConnectButtonText = "连接中..."; + // 异步连接到OPC UA服务器 + await _opcUaService.ConnectAsync(EndpointUrl); + + // 检查连接是否成功建立 if (_opcUaService.IsConnected) { + // 更新连接状态 IsConnected = true; ConnectButtonText = "已连接"; - IsConnectButtonEnabled = false; + + // 浏览根节点并加载其子节点 + var children = await _opcUaService.BrowseNode(_mapper.Map(RootOpcUaNode)); + RootOpcUaNode.Children = _mapper.Map>(children); } - - - // 浏览根节点 - - var childrens = await _opcUaService.BrowseNode(_mapper.Map(RootOpcUaNode)); - RootOpcUaNode.Children = _mapper.Map>(childrens); - - + } + // 处理特定异常类型提供更友好的用户提示 + catch (UnauthorizedAccessException ex) + { + NotificationHelper.ShowError($"连接被拒绝,请检查用户名和密码: {ex.Message}"); + } + catch (TimeoutException ex) + { + NotificationHelper.ShowError($"连接超时,请检查服务器地址和网络连接: {ex.Message}"); } catch (Exception ex) { - IsConnected = false; - IsConnectButtonEnabled = false; - ConnectButtonText = "连接服务器"; NotificationHelper.ShowError($"连接 OPC UA 服务器失败: {EndpointUrl} - {ex.Message}", ex); } - } - - [RelayCommand] - private async void SecondaryButton() - { - await _opcUaService.DisconnectAsync(); - - Close(SelectedVariables.Cast().ToList()); - } - - [RelayCommand] - private async void PrimaryButton() - { - await _opcUaService.DisconnectAsync(); - - Close(OpcUaNodeVariables.ToList()); - } - - [RelayCommand] - private async void CloseButton() - { - await _opcUaService.DisconnectAsync(); + finally + { + // 确保按钮状态正确更新 + // 如果连接失败,恢复按钮为可用状态 + if (!IsConnected) + { + IsConnectButtonEnabled = true; + ConnectButtonText = "连接服务器"; + } + } } /// - /// 处理来自服务器的数据变化通知 + /// 次要按钮命令(取消导入) + /// 负责断开与OPC UA服务器的连接并关闭对话框,返回用户选择的变量 /// - private static void OnNotification(MonitoredItem item, MonitoredItemNotificationEventArgs e) + [RelayCommand] + private async Task SecondaryButton() { - foreach (var value in item.DequeueValues()) - { - Console.WriteLine( - $"[通知] {item.DisplayName}: {value.Value} | 时间戳: {value.SourceTimestamp.ToLocalTime()} | 状态: {value.StatusCode}"); - } - } - - - - - public async Task LoadNodeVariables(OpcUaNodeItemViewModel node) - { - try { - OpcUaNodeVariables.Clear(); - - // 加载节点的子项 - node.IsExpanded = true; - node.IsSelected = true; - CurrentOpcUaNodeItem = node; - await Browse(node); + // 断开与OPC UA服务器的连接 + await _opcUaService.DisconnectAsync(); + // 关闭对话框并返回用户选择的变量列表 + Close(SelectedVariables.Cast().ToList()); } catch (Exception ex) { - NotificationHelper.ShowError($"加载 OPC UA 节点变量失败: {node.NodeId} - {ex.Message}", ex); - } - } - - private async Task Browse(OpcUaNodeItemViewModel node, bool isScan = false) - { - var childrens = await _opcUaService.BrowseNode(_mapper.Map(node)); - foreach (var children in childrens) - { - var opcNodeItem = _mapper.Map(children); - if (children.NodeClass == NodeClass.Variable) - { - OpcUaNodeVariables.Add(new VariableItemViewModel - { - Name = children.DisplayName, // 修正:使用子节点的显示名称 - OpcUaNodeId = children.NodeId.ToString(), - Protocol = ProtocolType.OpcUa, - CSharpDataType = children.DataType, - IsActive = true // 默认选中 - }); - } - else - { - if (node.Children.FirstOrDefault(n => n.NodeId == opcNodeItem.NodeId) == null) - { - node.Children.Add(opcNodeItem); - } - - if (isScan) - { - Browse(opcNodeItem); - } - } + NotificationHelper.ShowError($"断开连接时发生错误: {ex.Message}", ex); } } + /// + /// 主要按钮命令(确认导入) + /// 负责断开与OPC UA服务器的连接并关闭对话框,返回所有加载的变量 + /// [RelayCommand] + private async Task PrimaryButton() + { + try + { + // 断开与OPC UA服务器的连接 + await _opcUaService.DisconnectAsync(); + // 关闭对话框并返回所有加载的变量 + Close(OpcUaNodeVariables.ToList()); + } + catch (Exception ex) + { + NotificationHelper.ShowError($"断开连接时发生错误: {ex.Message}", ex); + } + } + + /// + /// 关闭按钮命令 + /// 负责断开与OPC UA服务器的连接 + /// + [RelayCommand] + private async Task CloseButton() + { + try + { + // 断开与OPC UA服务器的连接 + await _opcUaService.DisconnectAsync(); + } + catch (Exception ex) + { + NotificationHelper.ShowError($"断开连接时发生错误: {ex.Message}", ex); + } + } + + /// + /// 查找当前节点变量命令 + /// 根据当前选中的节点查找其下的所有变量 + /// + [RelayCommand(CanExecute = nameof(CanFindCurrentNodeVariables))] private async Task FindCurrentNodeVariables() { try { - if (CurrentOpcUaNodeItem == null) + // 检查是否有选中的节点 + if (SelectedNode == null) { - NotificationHelper.ShowError($"请先选择左边的节点,然后再查找变量。"); + NotificationHelper.ShowError("请先选择左边的节点,然后再查找变量。"); return; } + // 清空当前变量列表 OpcUaNodeVariables.Clear(); - // 加载节点的子项 - CurrentOpcUaNodeItem.IsExpanded = true; - CurrentOpcUaNodeItem.IsSelected = true; + // 设置选中节点的状态 + SelectedNode.IsExpanded = true; + SelectedNode.IsSelected = true; - await Browse(CurrentOpcUaNodeItem, true); + // 异步浏览节点变量(递归模式) + await BrowseNodeVariablesAsync(SelectedNode, true); + } + catch (OperationCanceledException) + { + // 处理用户取消操作的情况 + NotificationHelper.ShowInfo("操作已被取消"); } catch (Exception ex) { - NotificationHelper.ShowError($"加载 OPC UA 节点变量失败: {CurrentOpcUaNodeItem.NodeId} - {ex.Message}", ex); + // 处理其他异常情况 + NotificationHelper.ShowError($"加载 OPC UA 节点变量失败: {SelectedNode?.NodeId} - {ex.Message}", ex); } } + + /// + /// 加载节点变量方法 + /// 根据指定节点加载其下的所有变量 + /// + /// 要加载变量的OPC UA节点 + public async Task LoadNodeVariables(OpcUaNodeItemViewModel node) + { + try + { + // 清空当前变量列表 + OpcUaNodeVariables.Clear(); + + // 设置节点状态 + node.IsExpanded = true; + node.IsSelected = true; + // 更新选中节点 + SelectedNode = node; + + // 异步浏览节点变量(非递归模式) + await BrowseNodeVariablesAsync(node); + } + catch (OperationCanceledException) + { + // 处理用户取消操作的情况 + NotificationHelper.ShowInfo("操作已被取消"); + } + catch (Exception ex) + { + // 处理其他异常情况 + NotificationHelper.ShowError($"加载 OPC UA 节点变量失败: {node?.NodeId} - {ex.Message}", ex); + } + } + + /// + /// 浏览节点变量异步方法 + /// 递归或非递归地浏览指定节点下的所有变量 + /// + /// 要浏览的节点 + /// 是否递归浏览子节点 + private async Task BrowseNodeVariablesAsync(OpcUaNodeItemViewModel node, bool isRecursive = false) + { + // 参数有效性检查 + if (node == null) return; + + try + { + // 检查是否有取消请求 + _cancellationTokenSource.Token.ThrowIfCancellationRequested(); + + // 异步浏览节点获取子节点列表 + var children = await _opcUaService.BrowseNode(_mapper.Map(node)); + + // 再次检查是否有取消请求 + _cancellationTokenSource.Token.ThrowIfCancellationRequested(); + + // 遍历所有子节点 + foreach (var child in children) + { + // 映射子节点为视图模型对象 + var nodeItem = _mapper.Map(child); + + // 判断节点类型是否为变量 + if (child.NodeClass == NodeClass.Variable) + { + // 创建并添加变量项到变量列表 + OpcUaNodeVariables.Add(new VariableItemViewModel + { + Name = child.DisplayName, // 变量名称 + OpcUaNodeId = child.NodeId.ToString(), // OPC UA节点ID + Protocol = ProtocolType.OpcUa, // 协议类型 + CSharpDataType = child.DataType, // C#数据类型 + IsActive = true // 默认激活状态 + }); + } + // 如果是递归模式且节点不是变量,则递归浏览子节点 + else if (isRecursive) + { + // 递归浏览子节点 + await BrowseNodeVariablesAsync(nodeItem, true); + } + // 非递归模式下,将非变量节点添加到节点树中 + else + { + // 避免重复添加相同节点 + if (!node.Children.Any(n => n.NodeId == nodeItem.NodeId)) + { + node.Children.Add(nodeItem); + } + } + } + } + catch (OperationCanceledException) + { + // 处理取消操作 + NlogHelper.Info("节点浏览操作已被取消"); + throw; // 重新抛出异常以保持调用链 + } + catch (Exception ex) + { + // 记录浏览节点失败的日志 + NlogHelper.Error($"浏览节点失败: {node.NodeId} - {ex.Message}", ex); + throw; // 重新抛出异常以保持调用链 + } + } + + /// + /// 处理来自服务器的数据变化通知 + /// 当监视的OPC UA节点数据发生变化时会被调用 + /// + /// 发生变化的监视项 + /// 监视项通知事件参数 + private static void OnNotification(MonitoredItem item, MonitoredItemNotificationEventArgs e) + { + // 遍历所有变化的值 + foreach (var value in item.DequeueValues()) + { + // 输出通知信息到控制台 + Console.WriteLine( + $@"[通知] {item.DisplayName}: {value.Value} | 时间戳: {value.SourceTimestamp.ToLocalTime()} | 状态: {value.StatusCode}"); + } + } + + /// + /// 释放资源方法 + /// 实现IDisposable接口,负责释放使用的资源 + /// + public void Dispose() + { + // 发出取消请求 + _cancellationTokenSource?.Cancel(); + // 释放取消令牌源资源 + _cancellationTokenSource?.Dispose(); + } } \ No newline at end of file