Add tree context menu, missing connection settings, and fix lazy-load browse
Right-click on browse tree nodes to Subscribe (multi-select) or View History (Variable nodes only), with automatic tab switching. Add missing UI controls for failover URLs, session timeout, auto-accept certificates, and certificate store path. Fix tree expansion by adding two-way IsExpanded binding on TreeViewItem. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ZB.MOM.WW.LmxOpcUa.Client.Shared;
|
||||
@@ -26,6 +27,20 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
[ObservableProperty]
|
||||
private SecurityMode _selectedSecurityMode = SecurityMode.None;
|
||||
|
||||
[ObservableProperty]
|
||||
private string? _failoverUrls;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _sessionTimeoutSeconds = 60;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _autoAcceptCertificates = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _certificateStorePath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"LmxOpcUaClient", "pki");
|
||||
|
||||
/// <summary>All available security modes.</summary>
|
||||
public IReadOnlyList<SecurityMode> SecurityModes { get; } = Enum.GetValues<SecurityMode>();
|
||||
|
||||
@@ -51,6 +66,15 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
[ObservableProperty]
|
||||
private int _subscriptionCount;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _selectedTabIndex;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isHistoryEnabledForSelection;
|
||||
|
||||
/// <summary>The currently selected tree nodes (supports multi-select).</summary>
|
||||
public ObservableCollection<TreeNodeViewModel> SelectedTreeNodes { get; } = new();
|
||||
|
||||
public BrowseTreeViewModel BrowseTree { get; }
|
||||
public ReadWriteViewModel ReadWrite { get; }
|
||||
public SubscriptionsViewModel Subscriptions { get; }
|
||||
@@ -135,7 +159,11 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
EndpointUrl = EndpointUrl,
|
||||
Username = Username,
|
||||
Password = Password,
|
||||
SecurityMode = SelectedSecurityMode
|
||||
SecurityMode = SelectedSecurityMode,
|
||||
FailoverUrls = ParseFailoverUrls(FailoverUrls),
|
||||
SessionTimeoutSeconds = SessionTimeoutSeconds,
|
||||
AutoAcceptCertificates = AutoAcceptCertificates,
|
||||
CertificateStorePath = CertificateStorePath
|
||||
};
|
||||
settings.Validate();
|
||||
|
||||
@@ -195,4 +223,56 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes all selected tree nodes and switches to the Subscriptions tab.
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private async Task SubscribeSelectedNodesAsync()
|
||||
{
|
||||
if (SelectedTreeNodes.Count == 0 || !IsConnected) return;
|
||||
|
||||
var nodes = SelectedTreeNodes.ToList();
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
await Subscriptions.AddSubscriptionForNodeAsync(node.NodeId);
|
||||
}
|
||||
|
||||
SubscriptionCount = Subscriptions.SubscriptionCount;
|
||||
SelectedTabIndex = 1; // Subscriptions tab
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the history tab's selected node and switches to the History tab.
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void ViewHistoryForSelectedNode()
|
||||
{
|
||||
if (SelectedTreeNodes.Count == 0 || !IsConnected) return;
|
||||
|
||||
var node = SelectedTreeNodes[0];
|
||||
History.SelectedNodeId = node.NodeId;
|
||||
SelectedTabIndex = 3; // History tab
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates whether "View History" should be enabled based on the selected node's type.
|
||||
/// Only Variable nodes can have history.
|
||||
/// </summary>
|
||||
public void UpdateHistoryEnabledForSelection()
|
||||
{
|
||||
IsHistoryEnabledForSelection = IsConnected
|
||||
&& SelectedTreeNodes.Count > 0
|
||||
&& SelectedTreeNodes[0].NodeClass == "Variable";
|
||||
}
|
||||
|
||||
private static string[]? ParseFailoverUrls(string? csv)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(csv))
|
||||
return null;
|
||||
|
||||
return csv.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(u => !string.IsNullOrEmpty(u))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,33 @@ public partial class SubscriptionsViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to a node by ID (used by context menu). Skips if already subscribed.
|
||||
/// </summary>
|
||||
public async Task AddSubscriptionForNodeAsync(string nodeIdStr, int intervalMs = 1000)
|
||||
{
|
||||
if (!IsConnected || string.IsNullOrWhiteSpace(nodeIdStr)) return;
|
||||
|
||||
// Skip if already subscribed
|
||||
if (ActiveSubscriptions.Any(s => s.NodeId == nodeIdStr)) return;
|
||||
|
||||
try
|
||||
{
|
||||
var nodeId = NodeId.Parse(nodeIdStr);
|
||||
await _service.SubscribeAsync(nodeId, intervalMs);
|
||||
|
||||
_dispatcher.Post(() =>
|
||||
{
|
||||
ActiveSubscriptions.Add(new SubscriptionItemViewModel(nodeIdStr, intervalMs));
|
||||
SubscriptionCount = ActiveSubscriptions.Count;
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Subscription failed
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all subscriptions and resets state.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user