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.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using ZB.MOM.WW.LmxOpcUa.Client.Shared;
|
using ZB.MOM.WW.LmxOpcUa.Client.Shared;
|
||||||
@@ -26,6 +27,20 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private SecurityMode _selectedSecurityMode = SecurityMode.None;
|
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>
|
/// <summary>All available security modes.</summary>
|
||||||
public IReadOnlyList<SecurityMode> SecurityModes { get; } = Enum.GetValues<SecurityMode>();
|
public IReadOnlyList<SecurityMode> SecurityModes { get; } = Enum.GetValues<SecurityMode>();
|
||||||
|
|
||||||
@@ -51,6 +66,15 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private int _subscriptionCount;
|
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 BrowseTreeViewModel BrowseTree { get; }
|
||||||
public ReadWriteViewModel ReadWrite { get; }
|
public ReadWriteViewModel ReadWrite { get; }
|
||||||
public SubscriptionsViewModel Subscriptions { get; }
|
public SubscriptionsViewModel Subscriptions { get; }
|
||||||
@@ -135,7 +159,11 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
EndpointUrl = EndpointUrl,
|
EndpointUrl = EndpointUrl,
|
||||||
Username = Username,
|
Username = Username,
|
||||||
Password = Password,
|
Password = Password,
|
||||||
SecurityMode = SelectedSecurityMode
|
SecurityMode = SelectedSecurityMode,
|
||||||
|
FailoverUrls = ParseFailoverUrls(FailoverUrls),
|
||||||
|
SessionTimeoutSeconds = SessionTimeoutSeconds,
|
||||||
|
AutoAcceptCertificates = AutoAcceptCertificates,
|
||||||
|
CertificateStorePath = CertificateStorePath
|
||||||
};
|
};
|
||||||
settings.Validate();
|
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>
|
/// <summary>
|
||||||
/// Clears all subscriptions and resets state.
|
/// Clears all subscriptions and resets state.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -4,7 +4,13 @@
|
|||||||
x:Class="ZB.MOM.WW.LmxOpcUa.Client.UI.Views.BrowseTreeView"
|
x:Class="ZB.MOM.WW.LmxOpcUa.Client.UI.Views.BrowseTreeView"
|
||||||
x:DataType="vm:BrowseTreeViewModel">
|
x:DataType="vm:BrowseTreeViewModel">
|
||||||
<TreeView ItemsSource="{Binding RootNodes}"
|
<TreeView ItemsSource="{Binding RootNodes}"
|
||||||
Name="BrowseTree">
|
Name="BrowseTree"
|
||||||
|
SelectionMode="Multiple">
|
||||||
|
<TreeView.Styles>
|
||||||
|
<Style Selector="TreeViewItem" x:DataType="vm:TreeNodeViewModel">
|
||||||
|
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
|
||||||
|
</Style>
|
||||||
|
</TreeView.Styles>
|
||||||
<TreeView.ItemTemplate>
|
<TreeView.ItemTemplate>
|
||||||
<TreeDataTemplate ItemsSource="{Binding Children}">
|
<TreeDataTemplate ItemsSource="{Binding Children}">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
|
|||||||
@@ -30,6 +30,22 @@
|
|||||||
<Button Content="Disconnect"
|
<Button Content="Disconnect"
|
||||||
Command="{Binding DisconnectCommand}" />
|
Command="{Binding DisconnectCommand}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<TextBox Text="{Binding FailoverUrls}"
|
||||||
|
Width="300"
|
||||||
|
Watermark="Failover URLs (comma-separated)" />
|
||||||
|
<TextBlock Text="Timeout (s):" VerticalAlignment="Center" />
|
||||||
|
<NumericUpDown Value="{Binding SessionTimeoutSeconds}"
|
||||||
|
Minimum="1" Maximum="3600"
|
||||||
|
Width="90"
|
||||||
|
FormatString="0" />
|
||||||
|
<CheckBox IsChecked="{Binding AutoAcceptCertificates}"
|
||||||
|
Content="Auto-accept certificates"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<TextBox Text="{Binding CertificateStorePath}"
|
||||||
|
Width="200"
|
||||||
|
Watermark="Certificate store path" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Redundancy Info -->
|
<!-- Redundancy Info -->
|
||||||
<StackPanel Orientation="Horizontal"
|
<StackPanel Orientation="Horizontal"
|
||||||
@@ -61,7 +77,18 @@
|
|||||||
<DockPanel>
|
<DockPanel>
|
||||||
<TextBlock DockPanel.Dock="Top" Text="Browse Tree" FontWeight="Bold" Margin="8,8,8,4" />
|
<TextBlock DockPanel.Dock="Top" Text="Browse Tree" FontWeight="Bold" Margin="8,8,8,4" />
|
||||||
<views:BrowseTreeView DataContext="{Binding BrowseTree}"
|
<views:BrowseTreeView DataContext="{Binding BrowseTree}"
|
||||||
Name="BrowseTreePanel" />
|
Name="BrowseTreePanel">
|
||||||
|
<views:BrowseTreeView.ContextMenu>
|
||||||
|
<ContextMenu Name="TreeContextMenu">
|
||||||
|
<MenuItem Header="Subscribe"
|
||||||
|
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).SubscribeSelectedNodesCommand}"
|
||||||
|
IsEnabled="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).IsConnected}" />
|
||||||
|
<MenuItem Header="View History"
|
||||||
|
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).ViewHistoryForSelectedNodeCommand}"
|
||||||
|
IsEnabled="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).IsHistoryEnabledForSelection}" />
|
||||||
|
</ContextMenu>
|
||||||
|
</views:BrowseTreeView.ContextMenu>
|
||||||
|
</views:BrowseTreeView>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -70,7 +97,8 @@
|
|||||||
|
|
||||||
<!-- Right: Tab panels -->
|
<!-- Right: Tab panels -->
|
||||||
<TabControl Grid.Column="2"
|
<TabControl Grid.Column="2"
|
||||||
IsEnabled="{Binding IsConnected}">
|
IsEnabled="{Binding IsConnected}"
|
||||||
|
SelectedIndex="{Binding SelectedTabIndex}">
|
||||||
<TabItem Header="Read/Write">
|
<TabItem Header="Read/Write">
|
||||||
<views:ReadWriteView DataContext="{Binding ReadWrite}" />
|
<views:ReadWriteView DataContext="{Binding ReadWrite}" />
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|||||||
@@ -15,20 +15,44 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
base.OnLoaded(e);
|
base.OnLoaded(e);
|
||||||
|
|
||||||
// Wire up tree selection to the main ViewModel
|
|
||||||
var browseTreeView = this.FindControl<BrowseTreeView>("BrowseTreePanel");
|
var browseTreeView = this.FindControl<BrowseTreeView>("BrowseTreePanel");
|
||||||
var treeView = browseTreeView?.FindControl<TreeView>("BrowseTree");
|
var treeView = browseTreeView?.FindControl<TreeView>("BrowseTree");
|
||||||
if (treeView != null)
|
if (treeView != null)
|
||||||
{
|
{
|
||||||
treeView.SelectionChanged += OnTreeSelectionChanged;
|
treeView.SelectionChanged += OnTreeSelectionChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wire up context menu opening to sync selection and check history
|
||||||
|
var contextMenu = this.FindControl<ContextMenu>("TreeContextMenu");
|
||||||
|
if (contextMenu != null)
|
||||||
|
{
|
||||||
|
contextMenu.Opening += OnTreeContextMenuOpening;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTreeSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
private void OnTreeSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (DataContext is MainWindowViewModel vm && sender is TreeView treeView)
|
if (DataContext is not MainWindowViewModel vm || sender is not TreeView treeView) return;
|
||||||
|
|
||||||
|
// Update single selection for read/write and history panels
|
||||||
|
vm.SelectedTreeNode = treeView.SelectedItem as TreeNodeViewModel;
|
||||||
|
|
||||||
|
// Sync multi-selection collection
|
||||||
|
vm.SelectedTreeNodes.Clear();
|
||||||
|
foreach (var item in treeView.SelectedItems)
|
||||||
{
|
{
|
||||||
vm.SelectedTreeNode = treeView.SelectedItem as TreeNodeViewModel;
|
if (item is TreeNodeViewModel node)
|
||||||
|
{
|
||||||
|
vm.SelectedTreeNodes.Add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTreeContextMenuOpening(object? sender, System.ComponentModel.CancelEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is MainWindowViewModel vm)
|
||||||
|
{
|
||||||
|
vm.UpdateHistoryEnabledForSelection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
|||||||
public int HistoryReadAggregateCallCount { get; private set; }
|
public int HistoryReadAggregateCallCount { get; private set; }
|
||||||
public int GetRedundancyInfoCallCount { get; private set; }
|
public int GetRedundancyInfoCallCount { get; private set; }
|
||||||
|
|
||||||
|
public ConnectionSettings? LastConnectionSettings { get; private set; }
|
||||||
public NodeId? LastReadNodeId { get; private set; }
|
public NodeId? LastReadNodeId { get; private set; }
|
||||||
public NodeId? LastWriteNodeId { get; private set; }
|
public NodeId? LastWriteNodeId { get; private set; }
|
||||||
public object? LastWriteValue { get; private set; }
|
public object? LastWriteValue { get; private set; }
|
||||||
@@ -60,6 +61,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
|||||||
public Task<ConnectionInfo> ConnectAsync(ConnectionSettings settings, CancellationToken ct = default)
|
public Task<ConnectionInfo> ConnectAsync(ConnectionSettings settings, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
ConnectCallCount++;
|
ConnectCallCount++;
|
||||||
|
LastConnectionSettings = settings;
|
||||||
if (ConnectException != null) throw ConnectException;
|
if (ConnectException != null) throw ConnectException;
|
||||||
IsConnected = true;
|
IsConnected = true;
|
||||||
CurrentConnectionInfo = ConnectResult;
|
CurrentConnectionInfo = ConnectResult;
|
||||||
|
|||||||
@@ -187,4 +187,142 @@ public class MainWindowViewModelTests
|
|||||||
changed.ShouldContain(nameof(MainWindowViewModel.ConnectionState));
|
changed.ShouldContain(nameof(MainWindowViewModel.ConnectionState));
|
||||||
changed.ShouldContain(nameof(MainWindowViewModel.IsConnected));
|
changed.ShouldContain(nameof(MainWindowViewModel.IsConnected));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DefaultState_HasCorrectAdvancedSettings()
|
||||||
|
{
|
||||||
|
_vm.FailoverUrls.ShouldBeNull();
|
||||||
|
_vm.SessionTimeoutSeconds.ShouldBe(60);
|
||||||
|
_vm.AutoAcceptCertificates.ShouldBeTrue();
|
||||||
|
_vm.CertificateStorePath.ShouldContain("LmxOpcUaClient");
|
||||||
|
_vm.CertificateStorePath.ShouldContain("pki");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ConnectCommand_MapsFailoverUrlsToSettings()
|
||||||
|
{
|
||||||
|
_vm.FailoverUrls = "opc.tcp://backup1:4840, opc.tcp://backup2:4840";
|
||||||
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
_service.LastConnectionSettings.ShouldNotBeNull();
|
||||||
|
_service.LastConnectionSettings!.FailoverUrls.ShouldNotBeNull();
|
||||||
|
_service.LastConnectionSettings.FailoverUrls!.Length.ShouldBe(2);
|
||||||
|
_service.LastConnectionSettings.FailoverUrls[0].ShouldBe("opc.tcp://backup1:4840");
|
||||||
|
_service.LastConnectionSettings.FailoverUrls[1].ShouldBe("opc.tcp://backup2:4840");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ConnectCommand_MapsEmptyFailoverUrlsToNull()
|
||||||
|
{
|
||||||
|
_vm.FailoverUrls = "";
|
||||||
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
_service.LastConnectionSettings.ShouldNotBeNull();
|
||||||
|
_service.LastConnectionSettings!.FailoverUrls.ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ConnectCommand_MapsSessionTimeoutToSettings()
|
||||||
|
{
|
||||||
|
_vm.SessionTimeoutSeconds = 120;
|
||||||
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
_service.LastConnectionSettings.ShouldNotBeNull();
|
||||||
|
_service.LastConnectionSettings!.SessionTimeoutSeconds.ShouldBe(120);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ConnectCommand_MapsAutoAcceptCertificatesToSettings()
|
||||||
|
{
|
||||||
|
_vm.AutoAcceptCertificates = false;
|
||||||
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
_service.LastConnectionSettings.ShouldNotBeNull();
|
||||||
|
_service.LastConnectionSettings!.AutoAcceptCertificates.ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ConnectCommand_MapsCertificateStorePathToSettings()
|
||||||
|
{
|
||||||
|
_vm.CertificateStorePath = "/custom/pki/path";
|
||||||
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
_service.LastConnectionSettings.ShouldNotBeNull();
|
||||||
|
_service.LastConnectionSettings!.CertificateStorePath.ShouldBe("/custom/pki/path");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SubscribeSelectedNodesCommand_SubscribesAndSwitchesToTab()
|
||||||
|
{
|
||||||
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
var node1 = _vm.BrowseTree.RootNodes[0];
|
||||||
|
_vm.SelectedTreeNodes.Add(node1);
|
||||||
|
|
||||||
|
await _vm.SubscribeSelectedNodesCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
_vm.Subscriptions.ActiveSubscriptions.Count.ShouldBe(1);
|
||||||
|
_vm.Subscriptions.ActiveSubscriptions[0].NodeId.ShouldBe(node1.NodeId);
|
||||||
|
_vm.SelectedTabIndex.ShouldBe(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SubscribeSelectedNodesCommand_DoesNothing_WhenNoSelection()
|
||||||
|
{
|
||||||
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
await _vm.SubscribeSelectedNodesCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
_vm.Subscriptions.ActiveSubscriptions.ShouldBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ViewHistoryForSelectedNodeCommand_SetsNodeAndSwitchesToTab()
|
||||||
|
{
|
||||||
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
var node = _vm.BrowseTree.RootNodes[0];
|
||||||
|
_vm.SelectedTreeNodes.Add(node);
|
||||||
|
|
||||||
|
_vm.ViewHistoryForSelectedNodeCommand.Execute(null);
|
||||||
|
|
||||||
|
_vm.History.SelectedNodeId.ShouldBe(node.NodeId);
|
||||||
|
_vm.SelectedTabIndex.ShouldBe(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateHistoryEnabledForSelection_TrueForVariableNode()
|
||||||
|
{
|
||||||
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
var variableNode = new TreeNodeViewModel(
|
||||||
|
"ns=2;s=Var1", "Var1", "Variable", false, _service, new SynchronousUiDispatcher());
|
||||||
|
_vm.SelectedTreeNodes.Add(variableNode);
|
||||||
|
|
||||||
|
_vm.UpdateHistoryEnabledForSelection();
|
||||||
|
|
||||||
|
_vm.IsHistoryEnabledForSelection.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateHistoryEnabledForSelection_FalseForObjectNode()
|
||||||
|
{
|
||||||
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
var objectNode = new TreeNodeViewModel(
|
||||||
|
"ns=2;s=Obj1", "Obj1", "Object", true, _service, new SynchronousUiDispatcher());
|
||||||
|
_vm.SelectedTreeNodes.Add(objectNode);
|
||||||
|
|
||||||
|
_vm.UpdateHistoryEnabledForSelection();
|
||||||
|
|
||||||
|
_vm.IsHistoryEnabledForSelection.ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateHistoryEnabledForSelection_FalseWhenDisconnected()
|
||||||
|
{
|
||||||
|
_vm.UpdateHistoryEnabledForSelection();
|
||||||
|
|
||||||
|
_vm.IsHistoryEnabledForSelection.ShouldBeFalse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,4 +132,40 @@ public class SubscriptionsViewModelTests
|
|||||||
{
|
{
|
||||||
_vm.NewInterval.ShouldBe(1000);
|
_vm.NewInterval.ShouldBe(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddSubscriptionForNodeAsync_AddsSubscription()
|
||||||
|
{
|
||||||
|
_vm.IsConnected = true;
|
||||||
|
|
||||||
|
await _vm.AddSubscriptionForNodeAsync("ns=2;s=TestNode");
|
||||||
|
|
||||||
|
_vm.ActiveSubscriptions.Count.ShouldBe(1);
|
||||||
|
_vm.ActiveSubscriptions[0].NodeId.ShouldBe("ns=2;s=TestNode");
|
||||||
|
_vm.SubscriptionCount.ShouldBe(1);
|
||||||
|
_service.SubscribeCallCount.ShouldBe(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddSubscriptionForNodeAsync_SkipsDuplicate()
|
||||||
|
{
|
||||||
|
_vm.IsConnected = true;
|
||||||
|
|
||||||
|
await _vm.AddSubscriptionForNodeAsync("ns=2;s=TestNode");
|
||||||
|
await _vm.AddSubscriptionForNodeAsync("ns=2;s=TestNode");
|
||||||
|
|
||||||
|
_vm.ActiveSubscriptions.Count.ShouldBe(1);
|
||||||
|
_service.SubscribeCallCount.ShouldBe(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddSubscriptionForNodeAsync_DoesNothing_WhenDisconnected()
|
||||||
|
{
|
||||||
|
_vm.IsConnected = false;
|
||||||
|
|
||||||
|
await _vm.AddSubscriptionForNodeAsync("ns=2;s=TestNode");
|
||||||
|
|
||||||
|
_vm.ActiveSubscriptions.ShouldBeEmpty();
|
||||||
|
_service.SubscribeCallCount.ShouldBe(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user