using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Dialogs; using ZB.MOM.WW.ScadaBridge.CentralUI.Services; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment; /// /// Code-behind for InstanceCreate.razor — the Connection Bindings card /// (design 2026-08-21). The card flattens the chosen template AGAINST the chosen /// site through IFlatteningPipeline.PreviewAsync (no instance row exists yet), /// groups the data-sourced attributes with the same /// the Configure page uses, and collects one connection + Object per group. The pure /// prefill / tail-strip / binding-list logic lives in /// so it is unit-pinned without rendering the page. /// public partial class InstanceCreate { // --- Connection Bindings card state (design 2026-08-21) --- private IReadOnlyList _groups = Array.Empty(); private int _rowCount; private List _siteConnections = new(); private readonly Dictionary _inputs = new(StringComparer.Ordinal); private int _rootConnectionId; private string? _rootObject; private bool _skipBindings; private bool _previewLoading; private string? _previewError; private string? _siteIdentifier; private bool BindingsCardVisible => _createTemplateId > 0 && _createSiteId > 0; private int UnboundGroups => _groups.Count(g => !IsGroupBound(g)); private bool IsGroupBound(BindingGroup g) => _inputs.TryGetValue(g.Key, out var i) && i.ConnectionId > 0 && !string.IsNullOrWhiteSpace(i.Object); private int BoundRowCount => _groups.Where(IsGroupBound).Sum(g => g.Members.Count); /// /// Called from the Template and Site selects' @onchange; re-runs the preview /// flatten and rebuilds the groups. No-ops until both a template and a site are chosen. /// private async Task OnTemplateOrSiteChangedAsync() { _groups = Array.Empty(); _inputs.Clear(); _previewError = null; _rowCount = 0; if (!BindingsCardVisible) return; _previewLoading = true; try { var site = _sites.FirstOrDefault(s => s.Id == _createSiteId); _siteIdentifier = site?.SiteIdentifier; _siteConnections = (await SiteRepository.GetDataConnectionsBySiteIdAsync(_createSiteId)).ToList(); var preview = await FlatteningPipeline.PreviewAsync(_createTemplateId, _createSiteId); if (preview.IsFailure) { _previewError = preview.Error; return; } // Same predicate as the deploy gate (ValidateConnectionBindingCompleteness: // DataSourceReference != null), so every row the gate will check has a row here. var rows = preview.Value.Attributes .Where(a => a.DefaultDataSourceReference is not null || a.DataSourceReference is not null) .Select(a => new BindingRow(a.CanonicalName, a.DefaultDataSourceReference ?? a.DataSourceReference, null, null)) .ToList(); _rowCount = rows.Count; _groups = BindingGroupBuilder.Build(rows, _siteConnections.Select(c => c.Id).ToHashSet()); // Groups whose defaults already carry an object prefix start "from template" — // the connection is still the operator's to pick. foreach (var g in _groups) { if (!g.IsInstanceGroup && g.InferredObject is { Length: > 0 }) _inputs[g.Key] = new CreateBindingPlanner.GroupInput(0, g.InferredObject); } } catch (Exception ex) { _previewError = ex.Message; } finally { _previewLoading = false; } } private void PrefillGroups() { foreach (var kv in CreateBindingPlanner.Prefill(_groups, _inputs, _rootConnectionId, _rootObject)) _inputs[kv.Key] = kv.Value; } private void SetGroupConnection(BindingGroup g, int id) => _inputs[g.Key] = (_inputs.GetValueOrDefault(g.Key) ?? new CreateBindingPlanner.GroupInput(0, null)) with { ConnectionId = id }; private void SetGroupObject(BindingGroup g, string? obj) => _inputs[g.Key] = (_inputs.GetValueOrDefault(g.Key) ?? new CreateBindingPlanner.GroupInput(0, null)) with { Object = obj, Suggested = false }; // ── Address-space browser ─────────────────────────────────────────────── /// Object as well as Variable: a module group binds to a container node. private static readonly BrowseNodeClass[] ObjectAndVariable = [BrowseNodeClass.Object, BrowseNodeClass.Variable]; private NodeBrowserDialog? _browserRef; private string _browserConnectionName = ""; private string? _browserInitial; private BindingGroup? _browserTarget; // null = the "Bind all to" root picker /// /// True when the selected data connection supports address-space browsing. OPC UA /// and MxGateway both implement IBrowsableDataConnection site-side; other /// protocols return a NotBrowsable failure, so the button stays disabled for them. /// (Mirrors the Configure page's identical gate.) /// private bool IsBrowsable(int connectionId) { if (connectionId <= 0) return false; var protocol = _siteConnections.FirstOrDefault(c => c.Id == connectionId)?.Protocol; return string.Equals(protocol, "OpcUa", StringComparison.OrdinalIgnoreCase) || string.Equals(protocol, "MxGateway", StringComparison.OrdinalIgnoreCase); } /// /// Opens the picker for one group, or for the root object when /// is null. Remembers the target so /// writes the pick back to the right input. /// private async Task OpenBrowser(BindingGroup? target) { var connId = target is null ? _rootConnectionId : _inputs.GetValueOrDefault(target.Key)?.ConnectionId ?? 0; var conn = _siteConnections.FirstOrDefault(c => c.Id == connId); if (conn is null) return; _browserTarget = target; _browserConnectionName = conn.Name; _browserInitial = string.IsNullOrWhiteSpace(_rootObject) ? null : _rootObject; if (_browserRef is not null) await _browserRef.ShowAsync(_siteIdentifier ?? "", conn.Name, _browserInitial); } /// /// A group pick is normalised through , /// so selecting a leaf tag under the module still yields the module's object. /// private void OnBrowserSelected(string nodeId) { if (_browserTarget is null) _rootObject = nodeId; else SetGroupObject(_browserTarget, CreateBindingPlanner.ObjectFromPick(_browserTarget, nodeId)); _browserTarget = null; } }