解决TreeView的TreeView_SelectedItemChanged事件被多次调用的问题

This commit is contained in:
2025-09-02 13:09:00 +08:00
parent 254ab63837
commit 9b7c91eae9
6 changed files with 208 additions and 251 deletions

View File

@@ -1,6 +1,7 @@
using AutoMapper; using AutoMapper;
using DMS.Application.DTOs; using DMS.Application.DTOs;
using DMS.Core.Models; using DMS.Core.Models;
using DMS.Infrastructure.Models;
using DMS.WPF.ViewModels.Items; using DMS.WPF.ViewModels.Items;
namespace DMS.WPF.Profiles namespace DMS.WPF.Profiles
@@ -13,6 +14,8 @@ namespace DMS.WPF.Profiles
.ReverseMap(); .ReverseMap();
CreateMap<Variable, VariableItemViewModel>() CreateMap<Variable, VariableItemViewModel>()
.ReverseMap(); .ReverseMap();
CreateMap<OpcUaNode, OpcUaNodeItemViewModel>()
.ReverseMap();
CreateMap<VariableItemViewModel, VariableItemViewModel>(); CreateMap<VariableItemViewModel, VariableItemViewModel>();

View File

@@ -1,5 +1,8 @@
using AutoMapper;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using DMS.Core.Enums;
using DMS.Core.Helper;
using DMS.Core.Models; using DMS.Core.Models;
using DMS.Helper; using DMS.Helper;
using DMS.Infrastructure.Interfaces.Services; using DMS.Infrastructure.Interfaces.Services;
@@ -17,10 +20,10 @@ public partial class ImportOpcUaDialogViewModel : DialogViewModelBase<List<Varia
private string _endpointUrl = "opc.tcp://127.0.0.1:4855"; // 默认值 private string _endpointUrl = "opc.tcp://127.0.0.1:4855"; // 默认值
[ObservableProperty] [ObservableProperty]
private OpcUaNode _rootOpcUaNode; private OpcUaNodeItemViewModel _rootOpcUaNode;
[ObservableProperty] [ObservableProperty]
private ObservableCollection<Variable> _selectedNodeVariables; private ObservableCollection<VariableItemViewModel> _selectedNodeVariables;
public List<Variable> SelectedVariables { get; set; } = new List<Variable>(); public List<Variable> SelectedVariables { get; set; } = new List<Variable>();
@@ -39,15 +42,15 @@ public partial class ImportOpcUaDialogViewModel : DialogViewModelBase<List<Varia
private Session _session; private Session _session;
private readonly IOpcUaService _opcUaService; private readonly IOpcUaService _opcUaService;
private readonly IMapper _mapper;
private CancellationTokenSource _cancellationTokenSource; private CancellationTokenSource _cancellationTokenSource;
public ImportOpcUaDialogViewModel(IOpcUaService opcUaService) public ImportOpcUaDialogViewModel(IOpcUaService opcUaService,IMapper mapper)
{ {
//OpcUaNodes = new ObservableCollection<OpcUaNode>(); SelectedNodeVariables = new ObservableCollection<VariableItemViewModel>();
SelectedNodeVariables = new ObservableCollection<Variable>();
this._opcUaService = opcUaService; this._opcUaService = opcUaService;
RootOpcUaNode = new OpcUaNode() { DisplayName = "根节点", NodeId = Objects.ObjectsFolder }; this._mapper = mapper;
RootOpcUaNode = new OpcUaNodeItemViewModel() { DisplayName = "根节点", NodeId = Objects.ObjectsFolder, IsExpanded = true };
_cancellationTokenSource = new CancellationTokenSource(); _cancellationTokenSource = new CancellationTokenSource();
} }
@@ -65,6 +68,7 @@ public partial class ImportOpcUaDialogViewModel : DialogViewModelBase<List<Varia
if (_opcUaService.IsConnected) if (_opcUaService.IsConnected)
{ {
IsConnected=true;
ConnectButtonText = "已连接"; ConnectButtonText = "已连接";
IsConnectButtonEnabled = false; IsConnectButtonEnabled = false;
} }
@@ -72,7 +76,8 @@ public partial class ImportOpcUaDialogViewModel : DialogViewModelBase<List<Varia
// 浏览根节点 // 浏览根节点
await _opcUaService.BrowseNode(RootOpcUaNode); var childrens= await _opcUaService.BrowseNode(_mapper.Map<OpcUaNode>(RootOpcUaNode));
RootOpcUaNode.Children = _mapper.Map<ObservableCollection<OpcUaNodeItemViewModel>>(childrens);
} }
@@ -85,6 +90,18 @@ public partial class ImportOpcUaDialogViewModel : DialogViewModelBase<List<Varia
} }
} }
[RelayCommand]
private async void PrimaryButton()
{
await _opcUaService.DisconnectAsync();
}
[RelayCommand]
private async void CloseButton()
{
await _opcUaService.DisconnectAsync();
}
/// <summary> /// <summary>
/// 处理来自服务器的数据变化通知 /// 处理来自服务器的数据变化通知
/// </summary> /// </summary>
@@ -97,176 +114,69 @@ public partial class ImportOpcUaDialogViewModel : DialogViewModelBase<List<Varia
} }
} }
//private async Task BrowseNodes(ObservableCollection<OpcUaNode> nodes, NodeId parentNodeId)
//{
// try
// {
// Opc.Ua.ReferenceDescriptionCollection references;
// byte[] continuationPoint = null;
// _session.Browse(
// null, // RequestHeader
// new ViewDescription(),
// parentNodeId,
// 0u,
// BrowseDirection.Forward,
// Opc.Ua.ReferenceTypeIds.HierarchicalReferences,
// true,
// (uint)Opc.Ua.NodeClass.Object | (uint)Opc.Ua.NodeClass.Variable,
// out continuationPoint,
// out references
// );
// foreach (var rd in references) private bool _isLoadingNodeVariables = false;
// {
// NodeType nodeType = NodeType.Folder; // 默认是文件夹
// if ((rd.NodeClass & NodeClass.Variable) != 0)
// {
// nodeType = NodeType.Variable;
// }
// else if ((rd.NodeClass & NodeClass.Object) != 0)
// {
// nodeType = NodeType.Object;
// }
// var opcUaNode = new OpcUaNode(rd.DisplayName.Text, (NodeId)rd.NodeId, nodeType); public async Task LoadNodeVariables(OpcUaNodeItemViewModel node)
// nodes.Add(opcUaNode);
// // 如果是文件夹或对象,添加一个虚拟子节点,用于懒加载
// if (nodeType == NodeType.Folder || nodeType == NodeType.Object)
// {
// opcUaNode.Children.Add(new OpcUaNode("Loading...", NodeId.Null, NodeType.Folder)); // 虚拟节点
// }
// }
// }
// catch (Exception ex)
// {
// NlogHelper.Error($"浏览 OPC UA 节点失败: {parentNodeId} - {ex.Message}", ex);
// NotificationHelper.ShowError($"浏览 OPC UA 节点失败: {parentNodeId} - {ex.Message}", ex);
// }
//}
//public async Task LoadNodeVariables(OpcUaNode node)
//{
//if (node.NodeType == NodeType.Variable)
//{
// // 如果是变量节点,直接显示它
// SelectedNodeVariables.Clear();
// SelectedNodeVariables.Add(new Variable
// {
// Name = node.DisplayName,
// NodeId = node.NodeId.ToString(),
// OpcUaNodeId = node.NodeId.ToString(),
// ProtocolType = ProtocolType.OpcUA,
// IsActive = true // 默认选中
// });
// return;
//}
//if (node.IsLoaded || node.IsLoading)
//{
// return; // 已经加载或正在加载
//}
//node.IsLoading = true;
//node.Children.Clear(); // 清除虚拟节点
//try
//{
// Opc.Ua.ReferenceDescriptionCollection references;
// byte[] continuationPoint = null;
// _session.Browse(
// null, // RequestHeader
// new ViewDescription(),
// node.NodeId,
// 0u,
// BrowseDirection.Forward,
// Opc.Ua.ReferenceTypeIds.HierarchicalReferences,
// true,
// (uint)Opc.Ua.NodeClass.Object | (uint)Opc.Ua.NodeClass.Variable,
// out continuationPoint,
// out references
// );
// foreach (var rd in references)
// {
// NodeType nodeType = NodeType.Folder;
// if ((rd.NodeClass & NodeClass.Variable) != 0)
// {
// nodeType = NodeType.Variable;
// }
// else if ((rd.NodeClass & NodeClass.Object) != 0)
// {
// nodeType = NodeType.Object;
// }
// var opcUaNode = new OpcUaNode(rd.DisplayName.Text, (NodeId)rd.NodeId, nodeType);
// node.Children.Add(opcUaNode);
// if (nodeType == NodeType.Folder || nodeType == NodeType.Object)
// {
// opcUaNode.Children.Add(new OpcUaNode("Loading...", NodeId.Null, NodeType.Folder)); // 虚拟节点
// }
// // 如果是变量,添加到右侧列表
// if (nodeType == NodeType.Variable)
// {
// // Read the DataType attribute
// ReadValueId readValueId = new ReadValueId
// {
// NodeId = opcUaNode.NodeId,
// AttributeId = Attributes.DataType,
// // You might need to specify IndexRange and DataEncoding if dealing with arrays or specific encodings
// };
// DataValueCollection results;
// DiagnosticInfoCollection diagnosticInfos;
// _session.Read(
// null, // RequestHeader
// 0, // MaxAge
// TimestampsToReturn.Source,
// new ReadValueIdCollection { readValueId },
// out results,
// out diagnosticInfos
// );
// string dataType = string.Empty;
// if (results != null && results.Count > 0 && results[0].Value != null)
// {
// // Convert the NodeId of the DataType to a readable string
// NodeId dataTypeNodeId = (NodeId)results[0].Value;
// dataType = _session.NodeCache.GetDisplayText(dataTypeNodeId);
// }
// SelectedNodeVariables.Add(new Variable
// {
// Name = opcUaNode.DisplayName,
// OpcUaNodeId = opcUaNode.NodeId.ToString(),
// ProtocolType = ProtocolType.OpcUA,
// IsActive = true, // Default selected
// DataType = dataType // Assign the read DataType
// });
// }
// }
// node.IsLoaded = true;
//}
//catch (Exception ex)
//{
// NlogHelper.Error($"加载 OPC UA 节点变量失败: {node.NodeId} - {ex.Message}", ex);
// NotificationHelper.ShowError($"加载 OPC UA 节点变量失败: {node.NodeId} - {ex.Message}", ex);
//}
//finally
//{
// node.IsLoading = false;
//}
//}
public ObservableCollection<Variable> GetSelectedVariables()
{ {
return new ObservableCollection<Variable>(SelectedVariables); // 防止重复加载
if (_isLoadingNodeVariables)
return;
_isLoadingNodeVariables = true;
try
{
SelectedNodeVariables.Clear();
if (node.NodeClass == NodeClass.Variable)
{
// 如果是变量节点,直接显示它
SelectedNodeVariables.Add(new VariableItemViewModel
{
Name = node.DisplayName,
OpcUaNodeId = node.NodeId.ToString(),
Protocol = ProtocolType.OpcUa,
IsActive = true // 默认选中
});
return;
}
// 加载节点的子项
node.IsExpanded = true;
node.IsSelected = true;
var childrens = await _opcUaService.BrowseNode(_mapper.Map<OpcUaNode>(node));
foreach (var children in childrens)
{
var opcNodeItem = _mapper.Map<OpcUaNodeItemViewModel>(children);
if (children.NodeClass == NodeClass.Variable)
{
SelectedNodeVariables.Add(new VariableItemViewModel
{
Name = children.DisplayName, // 修正:使用子节点的显示名称
OpcUaNodeId = children.NodeId.ToString(),
Protocol = ProtocolType.OpcUa,
IsActive = true // 默认选中
});
}
else
{
node.Children.Add(opcNodeItem);
} }
} }
}
catch (Exception ex)
{
NlogHelper.Error($"加载 OPC UA 节点变量失败: {node.NodeId} - {ex.Message}", ex);
NotificationHelper.ShowError($"加载 OPC UA 节点变量失败: {node.NodeId} - {ex.Message}", ex);
}
finally
{
_isLoadingNodeVariables = false;
}
}
}

View File

@@ -0,0 +1,49 @@
using CommunityToolkit.Mvvm.ComponentModel;
using DMS.Infrastructure.Models;
using Opc.Ua;
using System.Collections.ObjectModel;
namespace DMS.WPF.ViewModels.Items
{
/// <summary>
/// OPC UA节点的视图模型。
/// </summary>
public partial class OpcUaNodeItemViewModel : ObservableObject
{
[ObservableProperty]
private NodeId? _nodeId;
[ObservableProperty]
private string? _displayName;
[ObservableProperty]
private NodeClass _nodeClass;
[ObservableProperty]
private object? _value;
[ObservableProperty]
private OpcUaNodeItemViewModel? _parentNode;
[ObservableProperty]
private ObservableCollection<OpcUaNodeItemViewModel> _children = new ObservableCollection<OpcUaNodeItemViewModel>();
[ObservableProperty]
private bool _isExpanded;
[ObservableProperty]
private bool _isSelected;
/// <summary>
/// 默认构造函数(用于设计时支持)。
/// </summary>
public OpcUaNodeItemViewModel()
{
// 设计时数据支持
}
}
}

View File

@@ -1,50 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Opc.Ua;
using System.Collections.ObjectModel;
namespace DMS.WPF.ViewModels.Items
{
public partial class OpcUaNodeViewModel : ObservableObject
{
[ObservableProperty]
private string _displayName;
[ObservableProperty]
private NodeId _nodeId;
[ObservableProperty]
private NodeType _nodeType;
[ObservableProperty]
private bool _isExpanded;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private bool _isLoaded;
public ObservableCollection<OpcUaNodeViewModel> Children { get; set; }
public OpcUaNodeViewModel(string displayName, NodeId nodeId, NodeType nodeType)
{
DisplayName = displayName;
NodeId = nodeId;
NodeType = nodeType;
Children = new ObservableCollection<OpcUaNodeViewModel>();
// 如果是文件夹或对象,添加一个虚拟子节点,用于懒加载
if (nodeType == NodeType.Folder || nodeType == NodeType.Object)
{
Children.Add(new OpcUaNodeViewModel("Loading...", NodeId.Null, NodeType.Folder)); // 虚拟节点
}
}
}
public enum NodeType
{
Folder,
Object,
Variable
}
}

View File

@@ -8,10 +8,10 @@
xmlns:vm="clr-namespace:DMS.WPF.ViewModels.Dialogs" xmlns:vm="clr-namespace:DMS.WPF.ViewModels.Dialogs"
Title="从OPC UA服务器导入变量" Title="从OPC UA服务器导入变量"
d:DataContext="{d:DesignInstance vm:ImportOpcUaDialogViewModel}" d:DataContext="{d:DesignInstance vm:ImportOpcUaDialogViewModel}"
PrimaryButtonClick="ContentDialog_PrimaryButtonClick" CloseButtonCommand="{Binding CloseButtonCommand}"
CloseButtonText="取消"
PrimaryButtonCommand="{Binding PrimaryButtonCommand}"
PrimaryButtonText="导入" PrimaryButtonText="导入"
SecondaryButtonClick="ContentDialog_SecondaryButtonClick"
SecondaryButtonText="取消"
mc:Ignorable="d"> mc:Ignorable="d">
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
@@ -47,6 +47,7 @@
<!-- 节点树 --> <!-- 节点树 -->
<TreeView <TreeView
Name="treeView"
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
Margin="0,0,10,0" Margin="0,0,10,0"
@@ -67,6 +68,18 @@
<TextBlock Text="{Binding DisplayName}" /> <TextBlock Text="{Binding DisplayName}" />
</HierarchicalDataTemplate> </HierarchicalDataTemplate>
</TreeView.ItemTemplate> </TreeView.ItemTemplate>
<TreeView.ItemContainerStyle>
<Style BasedOn="{StaticResource {x:Type TreeViewItem}}" TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
</TreeView> </TreeView>
<!-- 变量列表 --> <!-- 变量列表 -->

View File

@@ -1,7 +1,13 @@
using DMS.Helper;
using DMS.Services;
using DMS.WPF.Helper;
using DMS.WPF.ViewModels.Dialogs;
using DMS.WPF.ViewModels.Items;
using iNKORE.UI.WPF.Modern.Controls;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using DMS.WPF.ViewModels.Dialogs; using System.Windows.Threading;
using iNKORE.UI.WPF.Modern.Controls;
namespace DMS.WPF.Views.Dialogs; namespace DMS.WPF.Views.Dialogs;
@@ -10,32 +16,58 @@ namespace DMS.WPF.Views.Dialogs;
/// </summary> /// </summary>
public partial class ImportOpcUaDialog : ContentDialog public partial class ImportOpcUaDialog : ContentDialog
{ {
private const int ContentAreaMaxWidth = 1200;
private const int ContentAreaMaxHeight = 800;
public ImportOpcUaDialog() public ImportOpcUaDialog()
{ {
InitializeComponent(); InitializeComponent();
this.Opened += OnOpened;
} }
private void ContentDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) private void OnOpened(ContentDialog sender, ContentDialogOpenedEventArgs args)
{ {
// 在这里处理导入逻辑,例如获取选中的变量 //修改对话框内容的最大宽度和最大高度
// ViewModel.ImportSelectedVariables(); var backgroundElementBorder = VisualTreeFinder.FindVisualChildByName<Border>(this, "BackgroundElement");
} backgroundElementBorder.MaxWidth = ContentAreaMaxWidth;
backgroundElementBorder.MaxWidth = ContentAreaMaxHeight;
private void ContentDialog_SecondaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
// 处理取消逻辑
} }
private async void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) private async void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{ {
// if (e.NewValue is OpcUaNode selectedNode) try
// { {
// await ViewModel.LoadNodeVariables(selectedNode); //防止多次调用
// } object selectedObj = this.treeView.SelectedItem;
if (SelectedItemChanged != null)
{
Dispatcher.BeginInvoke(DispatcherPriority.Background, SelectedItemChanged, selectedObj);
}
}
catch (Exception ex)
{
NotificationHelper.ShowError($"选择节点时发生了错误:{ex.Message}");
} }
}
//事件
public async void SelectedItemChanged(object selectedObj)
{
if (selectedObj is OpcUaNodeItemViewModel selectedNode)
{
if (this.DataContext is ImportOpcUaDialogViewModel viewModel)
{
await viewModel.LoadNodeVariables(selectedNode);
}
}
}
private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs args) private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs args)
{ {
// if (args.AddedItems!=null && args.AddedItems.Count>0) // if (args.AddedItems!=null && args.AddedItems.Count>0)