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); } } }