@* Create / edit modal for a raw-tree Device's connection endpoint (v3 endpoint→DeviceConfig split). Dispatches to the right per-driver device form by DriverType. Test-connect builds the merged Driver+Device config transiently (so unsaved edits probe correctly), via DriverDeviceConfigMerger — the same merge LoadMergedProbeConfigAsync uses. The coordinator wires this into RawTree via @bind-Visible. *@ @using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.DeviceForms @using ZB.MOM.WW.OtOpcUa.AdminUI.Uns @using ZB.MOM.WW.OtOpcUa.Commons.Types @using ZB.MOM.WW.OtOpcUa.Core.Abstractions @inject IRawTreeService Svc @if (Visible) { } @code { /// Whether the modal is shown (supports @bind-Visible). [Parameter] public bool Visible { get; set; } /// Two-way visibility callback. [Parameter] public EventCallback VisibleChanged { get; set; } /// The device to edit; null ⇒ create a new device under . [Parameter] public string? DeviceId { get; set; } /// The owning driver instance (required for create; resolved from the device on edit). [Parameter] public string? DriverInstanceId { get; set; } /// The driver type (required for create; joined in from the device on edit). [Parameter] public string? DriverType { get; set; } /// Raised after a successful create/update so the tree can refresh. [Parameter] public EventCallback OnSaved { get; set; } private bool _isNew; private string _driverType = ""; private string? _resolvedDriverInstanceId; private string _name = ""; private bool _enabled = true; private string _deviceConfigJson = "{}"; /// Parameters handed to the DynamicComponent-rendered device form. Cached on the value it /// carries and rebuilt when that changes (never build-once: see DriverConfigModal), with the callback /// created once (R04 H2). private Dictionary _deviceFormParameters => _deviceFormParameterCache.Get( [_deviceConfigJson], () => new() { ["DeviceConfigJson"] = _deviceConfigJson, ["DeviceConfigJsonChanged"] = _deviceConfigJsonChanged ??= EventCallback.Factory.Create(this, v => _deviceConfigJson = v), }); private readonly ParameterDictionaryCache _deviceFormParameterCache = new(); private EventCallback? _deviceConfigJsonChanged; private byte[] _rowVersion = []; private string _parentDriverConfig = "{}"; private bool _busy; private string? _loadError; private string? _saveError; private bool _openHandled; // Bumped on every open and close; LoadAsync re-checks it after each await so a superseded open can't // install one target's fields under another target's displayed modal (RawBrowseModal idiom). private int _openGeneration; private bool IsStale(int generation) => generation != _openGeneration || !Visible; protected override async Task OnParametersSetAsync() { if (Visible && !_openHandled) { _openHandled = true; await LoadAsync(++_openGeneration); } else if (!Visible) { _openHandled = false; _openGeneration++; } } private async Task LoadAsync(int generation) { _busy = false; _loadError = null; _saveError = null; _isNew = string.IsNullOrEmpty(DeviceId); if (_isNew) { _resolvedDriverInstanceId = DriverInstanceId; _driverType = DriverType ?? ""; _name = ""; // placeholder only: a pre-filled value made typing append to it (#577); the name is validated on save _enabled = true; _deviceConfigJson = "{}"; _rowVersion = []; } else { var dto = await Svc.LoadDeviceForEditAsync(DeviceId!); if (IsStale(generation)) { return; } if (dto is null) { _loadError = $"Device '{DeviceId}' was not found."; return; } _resolvedDriverInstanceId = dto.DriverInstanceId; _driverType = dto.DriverType; _name = dto.Name; _enabled = dto.Enabled; _deviceConfigJson = string.IsNullOrWhiteSpace(dto.DeviceConfig) ? "{}" : dto.DeviceConfig; _rowVersion = dto.RowVersion; } // The parent driver's DriverConfig — folded with the (possibly unsaved) DeviceConfig for Test-connect. if (!string.IsNullOrEmpty(_resolvedDriverInstanceId)) { var drv = await Svc.LoadDriverForEditAsync(_resolvedDriverInstanceId); if (IsStale(generation)) { return; } _parentDriverConfig = drv?.DriverConfig ?? "{}"; } } /// Builds the merged Driver+Device probe config from the CURRENT (possibly unsaved) form — /// same merge as LoadMergedProbeConfigAsync, so Test-connect works before the device is saved. private string BuildMergedProbeConfig() => DriverDeviceConfigMerger.Merge( _parentDriverConfig, new[] { new DriverDeviceConfigMerger.DeviceRow(_name, _deviceConfigJson) }, Array.Empty()); private async Task SaveAsync() { _busy = true; _saveError = null; try { UnsMutationResult result; if (_isNew) { if (string.IsNullOrEmpty(_resolvedDriverInstanceId)) { _saveError = "No owning driver instance was supplied."; return; } result = await Svc.CreateDeviceAsync(_resolvedDriverInstanceId, _name, _deviceConfigJson); } else { result = await Svc.UpdateDeviceAsync(DeviceId!, _name, _deviceConfigJson, _enabled, _rowVersion); } if (!result.Ok) { _saveError = result.Error ?? "Save failed."; return; } await OnSaved.InvokeAsync(); await SetVisibleAsync(false); } catch (Exception ex) { _saveError = ex.Message; } finally { _busy = false; } } private Task CloseAsync() => SetVisibleAsync(false); private async Task SetVisibleAsync(bool value) { Visible = value; _openHandled = value; _openGeneration++; await VisibleChanged.InvokeAsync(value); } }