@* AB CIP address picker:
1. Static tag name + optional element index → tag[idx] or tag (always available).
2. (DriverOperator-gated, when the driver supports online discovery) Browse the controller
symbol tree captured by the universal DiscoveryDriverBrowser. *@
@implements IAsyncDisposable
@using Microsoft.AspNetCore.Authorization
@using ZB.MOM.WW.OtOpcUa.AdminUI.Browsing
@using ZB.MOM.WW.OtOpcUa.Commons.Browsing
@inject IBrowserSessionService BrowserService
@inject AuthenticationStateProvider AuthState
@inject IAuthorizationService AuthorizationService
Use dot notation for nested tags or UDT members.
Leave 0 for non-array tags (no index appended).
@if (_canBrowse)
{
@if (_token == Guid.Empty)
{
}
else
{
Browser open
}
@if (_openError is not null) { @TruncatedError() }
@if (_token != Guid.Empty)
{
Snapshot taken at open/refresh time — click Refresh to re-read the controller.
}
}
Result:@_built
@code {
[Parameter] public string CurrentAddress { get; set; } = "";
[Parameter] public EventCallback CurrentAddressChanged { get; set; }
/// Driver type used to open a universal browse session (defaults to AbCip).
[Parameter] public string DriverType { get; set; } = "AbCip";
/// Live accessor for the selected driver's DriverConfig JSON (browse connect config).
[Parameter] public Func GetConfigJson { get; set; } = () => "{}";
private string _tagName = "";
private int _elementIndex = 0;
private bool _useIndex = false;
private string _built = "";
private Guid _token = Guid.Empty;
private bool _opening;
private bool _canBrowse;
private string? _openError;
protected override async Task OnInitializedAsync()
{
var auth = await AuthState.GetAuthenticationStateAsync();
var authResult = await AuthorizationService.AuthorizeAsync(auth.User, null, AdminUiPolicies.DriverOperator);
// CanBrowse constructs a throwaway driver — evaluate once, not per render.
_canBrowse = authResult.Succeeded && BrowserService.CanBrowse(DriverType, GetConfigJson() ?? "{}");
_built = Build();
await CurrentAddressChanged.InvokeAsync(_built);
}
private async Task OpenBrowseAsync()
{
_opening = true; _openError = null; StateHasChanged();
try
{
var json = GetConfigJson() ?? "{}";
var result = await BrowserService.OpenAsync(DriverType, json, default);
if (result.Ok) _token = result.Token;
else _openError = result.Message;
}
finally { _opening = false; StateHasChanged(); }
}
private async Task RefreshBrowseAsync()
{
await CloseBrowseAsync();
await OpenBrowseAsync();
}
private async Task CloseBrowseAsync()
{
var t = _token; _token = Guid.Empty; StateHasChanged();
if (t != Guid.Empty) await BrowserService.CloseAsync(t);
}
private async Task OnTreeSelectAsync(BrowseNode node)
{
// Folders fire the same callback — commit only on a leaf (the captured NodeId is the CIP tag path).
if (node.Kind != BrowseNodeKind.Leaf) return;
_tagName = node.NodeId;
_useIndex = false;
await OnChangedAsync();
}
private async Task OnChangedAsync()
{
_built = Build();
await CurrentAddressChanged.InvokeAsync(_built);
}
private string Build()
{
if (string.IsNullOrWhiteSpace(_tagName))
return "";
return _useIndex ? $"{_tagName}[{_elementIndex}]" : _tagName;
}
private string TruncatedError() =>
_openError is null ? "" : (_openError.Length > 60 ? _openError[..60] + "…" : _openError);
public ValueTask DisposeAsync()
{
if (_token != Guid.Empty)
{
// Fire-and-forget — don't block circuit teardown on a slow capture.
_ = BrowserService.CloseAsync(_token);
}
return ValueTask.CompletedTask;
}
}