@* Lazy tree component with per-node text filter. Driver-agnostic — consumes IBrowserSessionService for root/expand. Selected node is bound back to parent via OnNodeSelected EventCallback. *@ @using ZB.MOM.WW.OtOpcUa.AdminUI.Browsing @using ZB.MOM.WW.OtOpcUa.Commons.Browsing @inject IBrowserSessionService BrowserService
@if (_loading) {
Loading…
} else if (_error is not null) {
@_error
} else if (_roots is null || _roots.Count == 0) {
No nodes.
} else { @foreach (var n in _roots) { @RenderNode(n, 0) } }
@code { /// The browse-session token returned by IBrowserSessionService.OpenAsync. [Parameter, EditorRequired] public Guid SessionToken { get; set; } /// The currently-selected node's NodeId, for visual selection highlighting. [Parameter] public string SelectedNodeId { get; set; } = ""; /// Fired when the user clicks a leaf (or any node — caller decides what to do with it). [Parameter] public EventCallback OnNodeSelected { get; set; } private bool _loading = true; private string? _error; private List? _roots; protected override async Task OnInitializedAsync() { await LoadRootAsync(); } private async Task LoadRootAsync() { try { var roots = await BrowserService.RootAsync(SessionToken, default); _roots = roots.Select(n => new TreeItem(n)).ToList(); } catch (Exception ex) { _error = ex.Message; } finally { _loading = false; } } private async Task ToggleAsync(TreeItem item) { item.Expanded = !item.Expanded; if (item.Expanded && !item.Loaded) await ExpandAsync(item); } private async Task ExpandAsync(TreeItem item) { if (item.Loaded || item.Loading) return; item.Loading = true; StateHasChanged(); try { var kids = await BrowserService.ExpandAsync(SessionToken, item.Node.NodeId, default); item.Children = kids.Select(k => new TreeItem(k)).ToList(); item.Loaded = true; } catch (Exception ex) { item.Error = ex.Message; } finally { item.Loading = false; StateHasChanged(); } } private async Task SelectAsync(TreeItem item) { SelectedNodeId = item.Node.NodeId; await OnNodeSelected.InvokeAsync(item.Node); } private RenderFragment RenderNode(TreeItem item, int depth) => __builder => { var indent = $"padding-left:{depth * 18}px"; var selectedCls = SelectedNodeId == item.Node.NodeId ? "bg-primary-subtle" : "";
@if (item.Node.Kind == BrowseNodeKind.Folder && item.Node.HasChildrenHint) { } else { } @item.Node.DisplayName @if (item.Node.Kind == BrowseNodeKind.Leaf) { leaf }
@if (item.Expanded && item.Loading) {
Loading…
} else if (item.Expanded && item.Error is not null) {
@item.Error
} else if (item.Expanded && item.Loaded && item.Children is { Count: > 0 }) { @foreach (var c in FilterChildren(item)) { @RenderNode(c, depth + 1) } } }; private static IEnumerable FilterChildren(TreeItem item) { if (item.Children is null) yield break; var f = item.Filter?.Trim(); foreach (var c in item.Children) { if (string.IsNullOrEmpty(f)) { yield return c; continue; } if (c.Node.DisplayName.Contains(f, StringComparison.OrdinalIgnoreCase) || c.Node.NodeId.Contains(f, StringComparison.OrdinalIgnoreCase)) yield return c; } } private sealed class TreeItem(BrowseNode node) { public BrowseNode Node { get; } = node; public bool Expanded { get; set; } public bool Loaded { get; set; } public bool Loading { get; set; } public string? Error { get; set; } public List? Children { get; set; } public string? Filter { get; set; } } }