Widget Data Storage
Widget placements — the per-page positioning and configuration of widget instances — are persisted separately from the full app definition, enabling efficient layout updates without rewriting the entire app JSON. Widget placements live in AIExt_WidgetPlacements.
Widget Definition vs. Widget Placement
Recall the two-layer widget model from Guide3:
- Widget Definition — the registered widget type (DataGrid, Button, Chart). Defined by the platform, not stored per-app.
- Widget Placement — a specific instance of a widget on a page, with its position, configuration, and overrides. Stored per-app per-page in the database.
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.