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;
///
/// Represents a single node in the OPC UA browse tree with lazy-load support.
///
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;
///
/// Private constructor for the placeholder sentinel only.
///
private TreeNodeViewModel()
{
NodeId = string.Empty;
DisplayName = "Loading...";
NodeClass = string.Empty;
HasChildren = false;
}
/// Initializes a new tree node view model.
/// The OPC UA node identifier.
/// The display name for this node.
/// The OPC UA node class.
/// Whether this node has child nodes.
/// The OPC UA client service for browsing.
/// The UI dispatcher for thread-safe updates.
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);
}
/// The string NodeId of this node.
public string NodeId { get; }
/// The display name shown in the tree.
public string DisplayName { get; }
/// The OPC UA node class (Object, Variable, etc.).
public string NodeClass { get; }
/// Whether this node has child references.
public bool HasChildren { get; }
/// Child nodes (may contain a placeholder sentinel before first expand).
public ObservableCollection Children { get; } = [];
///
/// Returns whether this node instance is the placeholder sentinel.
///
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);
}
}
}