diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceCreate.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceCreate.razor
index c410c116..3c0e6f2e 100644
--- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceCreate.razor
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceCreate.razor
@@ -3,13 +3,19 @@
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates
+@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
+@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
@using ZB.MOM.WW.ScadaBridge.TemplateEngine.Services
+@using ZB.MOM.WW.ScadaBridge.DeploymentManager
+@using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Dialogs
+@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDeployment)]
@inject ITemplateEngineRepository TemplateEngineRepository
@inject ISiteRepository SiteRepository
@inject ZB.MOM.WW.ScadaBridge.CentralUI.Auth.SiteScopeService SiteScope
@inject InstanceService InstanceService
+@inject IFlatteningPipeline FlatteningPipeline
@inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager NavigationManager
@@ -33,7 +39,9 @@
Template
-
+ { _createTemplateId = int.TryParse(e.Value?.ToString(), out var v) ? v : 0; await OnTemplateOrSiteChangedAsync(); }">
Select template...
@foreach (var t in _templates)
{
@@ -43,7 +51,9 @@
Site
-
+ { _createSiteId = int.TryParse(e.Value?.ToString(), out var v) ? v : 0; _createAreaId = 0; await OnTemplateOrSiteChangedAsync(); }">
Select site...
@foreach (var s in _sites)
{
@@ -65,12 +75,155 @@
{
@_formError
}
-
- Create
- Cancel
-
+ @if (!BindingsCardVisible)
+ {
+
+ Create
+ Cancel
+
+ }
+
+ @* Connection Bindings — the same module groups the Configure page shows, built
+ from a template+site PREVIEW flatten (no instance row exists yet). Group
+ headers only: per-member editing stays on Configure. Nothing is saved until
+ Create. Design: docs/plans/2026-08-21-create-instance-bindings-design.md *@
+ @if (BindingsCardVisible)
+ {
+
+
+ @if (_previewError is not null)
+ {
+
+ Could not preview this template at the selected site (@_previewError). You can create the instance and bind on Configure.
+
+ }
+ else if (!_previewLoading && _rowCount == 0)
+ {
+
Nothing to bind in this template.
+ }
+ else if (_siteConnections.Count == 0)
+ {
+
This site has no data connections — add one under Sites, or create now and bind later.
+ }
+ else if (!_skipBindings)
+ {
+
+ Bind all to
+
+ Select connection...
+ @foreach (var c in _siteConnections)
+ {
+ @c.Name (@c.Protocol)
+ }
+
+
+ OpenBrowser(null)">Browse…
+ Prefill groups
+ Fills the connection on every group and suggests an Object for blanks; never overwrites an Object you typed.
+
+ @foreach (var g in _groups)
+ {
+ var input = _inputs.GetValueOrDefault(g.Key);
+ var source = CreateBindingPlanner.SourceOf(g, input);
+
+ }
+ }
+
+
+
+ @* Address-space picker. SelectableNodeClasses adds Object so a module's
+ container node can be chosen, not just a leaf tag. *@
+
+ }
}
@@ -113,6 +266,11 @@
{
_createAreaId = aid;
}
+
+
+ // The query string can preselect a site; a template never is, so this
+ // no-ops today — kept so a future preselect gets the card for free.
+ await OnTemplateOrSiteChangedAsync();
}
catch (Exception ex)
{
diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceCreate.razor.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceCreate.razor.cs
new file mode 100644
index 00000000..643be712
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceCreate.razor.cs
@@ -0,0 +1,160 @@
+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;
+ }
+}
diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceCreate.razor.css b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceCreate.razor.css
new file mode 100644
index 00000000..4acba78c
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceCreate.razor.css
@@ -0,0 +1,14 @@
+/* Connection Bindings — group header rows (mirrors InstanceConfigure.razor.css,
+ design 2026-08-21). Create shows group headers only, so no .binding-group rule. */
+.binding-group-header {
+ flex-wrap: wrap;
+}
+
+.binding-group-header .form-select,
+.binding-group-header .form-control {
+ min-width: 0;
+}
+
+.binding-group-name {
+ min-width: 10rem;
+}