Files
lmxopcua/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/TreeNodeViewModel.cs
T
Joseph Doherty 64e3fbe035
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
docs: backfill XML documentation across 756 files
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public
members surfaced by commentchecker — resolves 5,847 of 5,869 issues
(99.6%) across three /fixdocs passes.
2026-05-28 08:10:17 -04:00

118 lines
3.7 KiB
C#

using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using ZB.MOM.WW.OtOpcUa.Client.Shared;
using ZB.MOM.WW.OtOpcUa.Client.UI.Services;
namespace ZB.MOM.WW.OtOpcUa.Client.UI.ViewModels;
/// <summary>
/// Represents a single node in the OPC UA browse tree with lazy-load support.
/// </summary>
public partial class TreeNodeViewModel : ObservableObject
{
private static readonly TreeNodeViewModel PlaceholderSentinel = new();
private readonly IUiDispatcher? _dispatcher;
private readonly IOpcUaClientService? _service;
private bool _hasLoadedChildren;
[ObservableProperty] private bool _isExpanded;
[ObservableProperty] private bool _isLoading;
/// <summary>
/// Private constructor for the placeholder sentinel only.
/// </summary>
private TreeNodeViewModel()
{
NodeId = string.Empty;
DisplayName = "Loading...";
NodeClass = string.Empty;
HasChildren = false;
}
/// <summary>Initializes a new tree node view model.</summary>
/// <param name="nodeId">The OPC UA node identifier.</param>
/// <param name="displayName">The display name for this node.</param>
/// <param name="nodeClass">The OPC UA node class.</param>
/// <param name="hasChildren">Whether this node has child nodes.</param>
/// <param name="service">The OPC UA client service for browsing.</param>
/// <param name="dispatcher">The UI dispatcher for thread-safe updates.</param>
public TreeNodeViewModel(
string nodeId,
string displayName,
string nodeClass,
bool hasChildren,
IOpcUaClientService service,
IUiDispatcher dispatcher)
{
NodeId = nodeId;
DisplayName = displayName;
NodeClass = nodeClass;
HasChildren = hasChildren;
_service = service;
_dispatcher = dispatcher;
if (hasChildren) Children.Add(PlaceholderSentinel);
}
/// <summary>The string NodeId of this node.</summary>
public string NodeId { get; }
/// <summary>The display name shown in the tree.</summary>
public string DisplayName { get; }
/// <summary>The OPC UA node class (Object, Variable, etc.).</summary>
public string NodeClass { get; }
/// <summary>Whether this node has child references.</summary>
public bool HasChildren { get; }
/// <summary>Child nodes (may contain a placeholder sentinel before first expand).</summary>
public ObservableCollection<TreeNodeViewModel> Children { get; } = [];
/// <summary>
/// Returns whether this node instance is the placeholder sentinel.
/// </summary>
internal bool IsPlaceholder => ReferenceEquals(this, PlaceholderSentinel);
partial void OnIsExpandedChanged(bool value)
{
if (value && !_hasLoadedChildren && HasChildren) _ = LoadChildrenAsync();
}
private async Task LoadChildrenAsync()
{
if (_service == null || _dispatcher == null) return;
_hasLoadedChildren = true;
IsLoading = true;
try
{
var nodeId = Opc.Ua.NodeId.Parse(NodeId);
var results = await _service.BrowseAsync(nodeId);
_dispatcher.Post(() =>
{
Children.Clear();
foreach (var result in results)
Children.Add(new TreeNodeViewModel(
result.NodeId,
result.DisplayName,
result.NodeClass,
result.HasChildren,
_service,
_dispatcher));
});
}
catch
{
_dispatcher.Post(() => Children.Clear());
}
finally
{
_dispatcher.Post(() => IsLoading = false);
}
}
}