Portal Community

Widget Definition vs. Widget Placement

Recall the two-layer widget model from Guide3:

Only Widget Placements are stored in the database. Widget Definitions are registered in the widget registry at startup.

Widget Placement Storage

-- AIExt_WidgetPlacements
CREATE TABLE AIExt_WidgetPlacements (
    Id            BIGINT          PRIMARY KEY,
    TenantId      NVARCHAR(100)   NOT NULL,
    AppId         NVARCHAR(100)   NOT NULL,
    PageId        NVARCHAR(100)   NOT NULL,
    PaneId        NVARCHAR(100)   NOT NULL,
    WidgetId      NVARCHAR(100)   NOT NULL,       -- Unique within the page
    WidgetType    NVARCHAR(100)   NOT NULL,       -- e.g., "DataGrid", "Button"
    SortOrder     INT             NOT NULL,
    GridCol       INT             NOT NULL,       -- Layout position
    GridRow       INT             NOT NULL,
    ColSpan       INT             NOT NULL,
    RowSpan       INT             NOT NULL,
    ConfigJson    NVARCHAR(MAX)   NOT NULL,       -- Widget config + overrides
    CreatedAt     DATETIME2       NOT NULL,
    UpdatedAt     DATETIME2       NOT NULL,
    UNIQUE (TenantId, AppId, PageId, WidgetId)
);

SaveLayoutAsync — Granular Page Layout Persistence

The designer's drag-and-drop operations use SaveLayoutAsync to persist layout changes for a single page without rewriting the full app definition. This is far more efficient for frequent designer interactions:

// Service method — replaces widget placements for a single page
Task SaveLayoutAsync(string tenantId, string appId, string pageId,
                     IList placements, string changedByUserId);

// Implementation strategy:
// 1. Delete all existing placements for (tenantId, appId, pageId)
// 2. Insert the new placements from the list
// 3. Invalidate the Redis cache for this app
// 4. Write audit log entry
// Done in a single transaction — atomic

Widget Config JSON Structure

Each widget placement's ConfigJson stores the complete widget configuration including app-level overrides and placement-level overrides:

// ConfigJson for a DataGrid widget placement
{
  "dataSource": "GetLeads",
  "params": { "tenantId": "{{ context.tenantId }}" },
  "columns": [
    { "field": "name", "header": "Name", "sortable": true },
    { "field": "status", "header": "Status" },
    { "field": "dealValue", "header": "Deal Value", "visibleTo": ["manager"] }
  ],
  "rowAction": { "type": "navigate", "target": "/leads/{{ row.id }}" },
  "visibilityExpression": null,
  "visibleTo": [],
  "breakpoints": {
    "mobile": { "colSpan": 12, "visible": false },
    "tablet": { "colSpan": 12 },
    "desktop": { "colSpan": 8 }
  }
}

Relationship Between WidgetPlacements and AppDefinitions

The AIExt_AppDefinitions table stores a complete snapshot of the app including embedded widget placements. The AIExt_WidgetPlacements table is the normalized, live version used for incremental designer updates. On export, the service reads from AIExt_WidgetPlacements (current state) and serializes to the bundle. On full app save, both tables are updated in sync.