title: "Display Construction — Types, Dashboards, Canvas, Symbols & Dynamics" tags: [display, canvas, dashboard, symbol, navigation, hmi, ui, layout, dynamics, codebehind] description: "Build FrameworX displays: understand display types (content pages, headers, sidebars, dialogs), create Dashboard and Canvas layouts, place and scale symbols properly, add visual dynamics, configure navigation, and write CodeBehind logic" version: "5.1" author: "Tatsoft"
Build complete FrameworX displays: choose the right display type for the task, create Dashboard or Canvas content pages, place controls and symbols with proper sizing and tag binding, add visual dynamics, configure navigation, and write CodeBehind for display logic.
Use when:
Do NOT use when:
get_objects + write_objects)| Category | Items |
|---|---|
| Tools | get_table_schema, write_objects, get_objects, list_elements, list_dynamics, browse_object_model |
| Tables | DisplaysList, DisplaysSymbols, DisplaysLayouts |
Critical: Use DisplaysList for creating and editing displays. DisplaysDraw is the Designer visual editor UI — NOT a writable table.
Before building any display, use these tools to discover what's available:
list_elements() -- all element types by category + library folders
list_dynamics() -- ALL dynamic types by category
list_elements('Symbol/HMI') -- browse symbol library
list_elements('Canvas') -- Canvas display structure schema
list_elements('Dashboard') -- Dashboard display structure schema
list_elements('Wizard') -- Wizard symbol schema
Every display you create falls into one of these categories. Know which one you're building before you start.
Content pages fill the main Content region of the layout. These are what the operator sees and interacts with. Two panel types:
Default content size: 1366 x 728. Set PanelType explicitly — omitting it silently defaults to Canvas.
Decision guide: If the display is primarily data monitoring (gauges, trends, grids), prefer Dashboard. Use Canvas only when process flow, spatial layout, or custom shapes/lines are needed.
Headers and sidebars are persistent displays that stay visible while content pages change.
1366 x 40. Contains the solution title/logo on the left and navigation buttons on the right. Uses Canvas layout with IndustrialIcons and basic labels/buttons.These are assigned to layout regions (Header, Menu) in DisplaysLayouts. Read the Startup layout to discover current region assignments and sizes.
CRITICAL: When creating Header by default, use "OnResize": "StretchFill",
Smaller displays that overlay the main content for confirmations, data entry, or detail views.
DisplayMode: "Dialog") — modal popup with OK/Cancel buttons. Blocks interaction with the page behind it. Use DialogOnOK() CodeBehind method for the OK action. Typical size: 400 x 300 to 600 x 400.DisplayMode: "Popup") — floating non-modal window. Operator can still interact with the page behind it. Typical size: 300 x 200 to 500 x 350.DisplayMode: "PopupWindow") — separate floating window (Not available for Web, do NOT use, unless specifically requested by the user)Open dialogs/popups via ActionDynamic with navigate to on a button click, with action OpenDisplay
The correct format for write_objects on DisplaysList uses top-level properties directly. The identifier field is Name.
{
"Name": "MyDisplay",
"PanelType": "Canvas",
"DisplayMode": "Page",
"OnResize": "StretchFill",
"Size": "1366 x 728",
"Elements": [...]
}
NEVER use JsonFormat wrapper, ObjectName, or nest properties inside sub-objects. Everything goes at the top level.
Displays are document objects — full replacement on write. When modifying an existing display, ALWAYS read first, merge your changes, then write back the complete document. Omitted content is deleted.
get_objects('DisplaysList', names=['MainPage'], detail='full')
MainPage is predefined in new solutions. Write main content directly into it — no need to create a new display for the landing screen.
{"Fill": "#FF3498DB", "FillTheme": ""} — otherwise the theme engine silently overrides your colorlist_elements('ThemeColors') for available named theme brushesWhen placing symbols on any display:
@Tag. bindings in SymbolLabels. NEVER use @Label. when placing symbols — @Label. is only for symbol internal definitions (in DisplaysSymbols).SymbolName: "Wizard/PUMP".Dashboards use a grid defined by DashboardDisplay (Columns/Rows) and a Cells array. Elements auto-fill their cells — no pixel positioning.
"1m" (1 minute) for demos and first prototypes so charts populate with data immediately. Use longer durations ("5m", "30m", "1h") only when the user specifies a monitoring window.ColSpan/RowSpan for elements that need multiple cells.Cell.HeaderLink adds a header label above the cell content.list_elements('Dashboard')
{
"table_type": "DisplaysList",
"data": [{
"Name": "MainPage",
"PanelType": "Dashboard",
"DashboardDisplay": {
"Columns": ["*", "*", "*"],
"Rows": ["*", "*"]
},
"Cells": [
{
"Row": 0, "Col": 0,
"Cell": { "HeaderLink": "Tank Level" },
"Content": {
"Type": "CircularGauge",
"LinkedValue": "@Tag.Plant/Tank1/Level.Value",
"Minimum": 0, "Maximum": 100
}
},
{
"Row": 0, "Col": 1,
"Cell": { "HeaderLink": "Temperature" },
"Content": {
"Type": "CircularGauge",
"LinkedValue": "@Tag.Plant/Tank1/Temperature.Value",
"Minimum": 0, "Maximum": 100
}
},
{
"Row": 0, "Col": 2,
"Cell": { "HeaderLink": "Trend" },
"Content": {
"Type": "TrendChart",
"Duration": "1m",
"Pens": {
"Type": "TrendPenList",
"Children": [
{ "Type": "TrendPen", "LinkedValue": "@Tag.Plant/Tank1/Level", "PenLabel": "Level", "Stroke": "#FF2196F3", "Auto": true }
]
}
}
},
{
"Row": 1, "Col": 0, "ColSpan": 3,
"Cell": { "HeaderLink": "Alarms" },
"Content": {
"Type": "AlarmViewer"
}
}
]
}]
}
Canvas displays use absolute positioning. Think of it like building a React UI — you're arranging elements on a coordinate plane.
CRITICAL: Do not spend time searching for library symbols at the start. Build the display in two passes:
Pass 1 — Layout and structure:
FillTheme: "PanelBackground")Pass 2 — Optional library symbols (if needed):
list_elements('Symbol/HMI/...')200×150 and your allocated area is 100 wide, use Width: 100, Height: 75Unless you are intentionally layering elements (e.g., a status indicator on top of a tank), do NOT overlap elements. Tanks, motors, valves — each gets its own space. Overlapping equipment symbols creates a confusing, unreadable display.
| Element | Minimum Size | Recommended |
|---|---|---|
| Wizard Symbol | 70×70 | 80×80 to 100×100 |
| Library Symbol | 60×60 | 80×80 |
| Section Title | FontSize 14 | FontSize 15–16 |
| Value Text | FontSize 13 | FontSize 14 |
| Gauge (Circular) | 150×150 | 180×180 |
| TrendChart | 300×180 | 500×200 |
| AlarmViewer | 400×150 | 600×200 |
| Button | 100×35 | 130×40 |
NEVER put value and unit in separate TextBlocks side by side. Use ONE TextBlock with composite LinkedValue:
{
"Type": "TextBlock",
"LinkedValue": "Flow: {@Tag.Plant/Intake/FIT_1001} GPM",
"Left": 30, "Top": 150, "Width": 170, "Height": 24,
"FontSize": 14
}
Patterns: "Flow: {@Tag.X} GPM" (label + value + unit), "{@Tag.X} NTU" (value + unit), "pH: {@Tag.X}" (label + value).
Symbol.Left = Area.Left + (Area.Width - Symbol.Width) / 2
Symbol.Top = Area.Top + offset
When building process overview displays with multiple stages (intake → treatment → distribution), use this zone-based approach to ensure the entire canvas is utilized and content is well-organized.
NEVER start by placing individual elements. Instead:
Title bar: full width, 50px tall
Available width = 1366 - 20 (margins) = 1346
Zone width = (1346 - (N-1) × 15) / N
Zone height = 320
Zone startY = 60
Zone[i].Left = 20 + i × (zoneWidth + 15)
Zone[i].Top = 60
Zone[i].Width = zoneWidth
Zone[i].Height = 320
Bottom panel: Left=20, Top=400, Width=1326, Height=remaining
Every zone MUST have a background Rectangle for visual grouping, placed FIRST in the Elements array:
{
"Type": "Rectangle",
"Left": 20, "Top": 60, "Width": 208, "Height": 320,
"FillTheme": "PanelBackground",
"Stroke": "#FF909090", "StrokeThickness": 1
}
Each process zone follows this vertical pattern from its origin:
{
"Type": "TextBlock",
"Text": "→",
"Left": 228, "Top": 190, "Width": 15, "Height": 30,
"FontSize": 20
}
Below the process zones, fill remaining space with ONE of:
Bottom panel sections should also use PanelBackground theme background rectangles.
Dynamics attach runtime behaviors to ANY element. They are placed inside the element's Dynamics array. Each dynamic is a separate JSON object within that array. Call list_dynamics() to see all types.
list_dynamics() -- all dynamics by category
list_dynamics('FillColorDynamic') -- full schema for a specific dynamic
{
"Type": "Rectangle",
"Left": 100, "Top": 200, "Width": 60, "Height": 60,
"Dynamics": [
{
"Type": "FillColorDynamic",
"LinkedValue": "@Tag.Plant/Pump1/Running",
"ChangeColorItems": [
{ "Type": "ChangeColorItem", "ChangeLimit": 0, "LimitColor": "#FF808080" },
{ "Type": "ChangeColorItem", "ChangeLimit": 1, "LimitColor": "#FF00FF00" }
]
}
]
}
For analog values, use graduated thresholds (e.g., 0→blue, 60→yellow, 80→orange, 95→red).
{
"Type": "Rectangle",
"Left": 50, "Top": 100, "Width": 30, "Height": 30,
"Dynamics": [
{
"Type": "VisibilityDynamic",
"LinkedValue": "@Tag.Plant/Tank1/AlarmActive"
}
]
}
Element visible when non-zero/true, hidden when zero/false.
{
"Type": "Ellipse",
"Left": 200, "Top": 150, "Width": 80, "Height": 80,
"Dynamics": [
{
"Type": "RotationDynamic",
"LinkedValue": "@Tag.Plant/Fan1/Speed",
"MinAngle": 0, "MaxAngle": 360,
"MinValue": 0, "MaxValue": 100
}
]
}
Elements can have multiple dynamics simultaneously in the Dynamics array. Always verify schemas: list_dynamics('DynamicTypeName').
Dynamics array — NEVER as a direct property on the element.ChangeColorItems uses a flat array of ChangeColorItem objects — do NOT wrap in {"Type": "ColorChangeList", "Children": [...]}.Dynamics array.ActionDynamic defines actions triggered by user interactions. Like all dynamics, it goes inside the element's Dynamicsarray. Navigation is typically in Header displays only.
Standard format — ActionDynamic inside the Dynamics array with explicit mouse event:
{
"Type": "Button",
"Text": "Go to Details",
"Left": 50, "Top": 400, "Width": 150, "Height": 40,
"Dynamics": [
{
"Type": "ActionDynamic",
"MouseLeftButtonDown": {
"Type": "DynamicActionInfo",
"ActionType": "OpenDisplay",
"ObjectLink": "DetailPage"
}
}
]
}
Common Action Examples:
// Toggle a digital tag
"Dynamics": [
{
"Type": "ActionDynamic",
"MouseLeftButtonDown": {
"Type": "DynamicActionInfo",
"ActionType": "ToggleValue",
"ObjectLink": "@Tag.Placeholder.DigitalTag"
}
}
]
// Navigate to a display
"Dynamics": [
{
"Type": "ActionDynamic",
"MouseLeftButtonDown": {
"Type": "DynamicActionInfo",
"ActionType": "OpenDisplay",
"ObjectLink": "MainPage"
}
}
]
// Set a specific value
"Dynamics": [
{
"Type": "ActionDynamic",
"MouseLeftButtonDown": {
"Type": "DynamicActionInfo",
"ActionType": "SetValue",
"ObjectLink": "@Tag.Plant/Flow/FIT_1001",
"ObjectValueLink": 10
}
}
]
// Run a CodeBehind script
"Dynamics": [
{
"Type": "ActionDynamic",
"MouseLeftButtonDown": {
"Type": "DynamicActionInfo",
"ActionType": "RunScript",
"ActionScript": "MouseLeftButtonDown1"
}
}
]
Multi-event — same element, different actions for different mouse events:
{
"Type": "Button",
"Left": 31.6, "Top": 133.84,
"LabelLink": "Button",
"Width": 100, "Height": 32,
"Dynamics": [
{
"Type": "ActionDynamic",
"MouseLeftButtonDown": {
"Type": "DynamicActionInfo",
"ActionType": "RunScript",
"ActionScript": "MouseLeftButtonDown1"
}
}
]
}
When a solution has multiple content pages, navigation buttons belong in the Header display — not on content pages.
IMPORTANT: First create all the Content Pages you need, the Header on the end. So you know the pages you need to put in the navigation.
get_objects('DisplaysLayouts', names=['Startup'], detail='full')get_objects('DisplaysList', names=['Header'], detail='full')CRITICAL: Do NOT put page navigation buttons on content pages. Content pages only get in-page action buttons (start/stop, acknowledge, open popup). Page-to-page navigation is always in the Header.
CodeBehind is CLIENT-SIDE C# or VB.NET code embedded in each display.
Lifecycle methods: DisplayOpening(), DisplayIsOpen(), DisplayClosing(), DialogOnOK()
Contents field format: {Language}\r\n{Code} — first line is CSharp or VBdotNet.
{
"Name": "Page1",
"Engine": "Portable",
"PanelType": "Canvas",
"DisplayMode": "Page",
"OnResize": "StretchFill",
"Width": 1366,
"Height": 728,
"Elements": [
{
"Type": "Button",
"Left": 31.6,
"Top": 133.84,
"LabelLink": "Button",
"Width": 100,
"Height": 32,
"Dynamics": [
{
"Type": "ActionDynamic",
"MouseLeftButtonDown": {
"Type": "DynamicActionInfo",
"ActionType": "RunScript",
"ActionScript": "MouseLeftButtonDown1"
}
}
]
}
],
"IsOpenInterval": "100",
"Contents": "CSharp\r\npublic async Task DisplayOpening()\r\n{\r\n\t// Add your code here\r\n\t\r\n}\r\npublic async Task DisplayIsOpen()\r\n{\r\n\t// Add your code here\r\n\t\r\n}\r\npublic async Task DisplayClosing()\r\n{\r\n\t// Add your code here\r\n\t\r\n}\r\npublic async Task<bool> DialogOnOK()\r\n{\r\n\t// Add your code here\r\n\t\r\n\treturn true;\r\n}\r\n\r\npublic async Task MouseLeftButtonDown1(object sender, System.Windows.Input.InputEventArgs e)\r\n{\r\n\t// Add your code here\r\n\t\r\n}"
}
See the Scripts and Expressions skill for full CodeBehind guidance.
Layout regions: Header, Footer, Menu, Submenu, Content. The Startup layout defines which display loads into each region.
get_table_schema('DisplaysLayouts')
get_objects('DisplaysLayouts', names=['Startup'], detail='full')
For plant-wide navigation with dynamic content:
Client.Context updates → content displays react@Tag.Area1/Line1/StateAsset(Client.Context.AssetPath + "State1")This allows a single display template to show data for whichever asset the operator selects.
Before writing any Canvas display, verify:
FillTheme: "PanelBackground"Name (not ObjectName)PanelType is set at top levelDynamics array (never as direct properties)| Mistake | How to Avoid |
|---|---|
Using DisplaysDraw as table_type | DisplaysDraw is the visual editor UI, not a writable table. Use DisplaysList |
| Omitting PanelType | Always set PanelType explicitly — defaults silently to Canvas |
Using ObjectName instead of Name | The identifier field is Name |
Using JsonFormat wrapper | NEVER use JsonFormat. All properties go at top level |
Using @Label. in display elements | @Label. is only for DisplaysSymbols internals. Use @Tag. when placing symbols |
| Library symbols too large for allocated area | Set Width/Height to scale down proportionally to fit the area. Maintain aspect ratio |
| Overlapping equipment symbols | Each piece of equipment gets its own space unless intentionally layered |
| Setting colors without clearing theme | Set value AND clear theme: {Fill: '#FF3498DB', FillTheme: ''} |
| Sending partial display content | Always read-modify-write for existing displays (document objects) |
| Canvas positioning in Dashboard | Canvas uses Left/Top + Elements; Dashboard uses Row/Col + Cells |
| Guessing dynamic property names | Always call list_dynamics('DynamicTypeName') for exact schema |
| CodeBehind in wrong Contents format | Contents must start with CSharp\r\n or VBdotNet\r\n before code |
| All Canvas content in top-left corner | Divide canvas into areas FIRST, then place elements within them |
| Symbols at 40×40 or 50×50 | Minimum 80×80 on process overview displays |
| Value and Unit as separate TextBlocks | Single TextBlock: "{@Tag.X} GPM" |
| No background rectangles on areas | Always add Rectangle with FillTheme: "PanelBackground" |
| Navigation buttons on content pages | Page navigation goes in the Header display |
| FontSize 10–11 for values | Minimum 13, prefer 14 for operator readability |
| Dynamics as direct element properties | NEVER use flat format (e.g. "ActionDynamic": {...} on the element). Always use the "Dynamics": [...] array |
| ChangeColorItems wrapped in ColorChangeList | Use a flat array: "ChangeColorItems": [...] — do NOT wrap in {"Type": "ColorChangeList", "Children": [...]} |
| Display Action | Tool Call |
|---|---|
| Get display schema | get_table_schema('DisplaysList') |
| Get Canvas structure | list_elements('Canvas') |
| Get Dashboard structure | list_elements('Dashboard') |
| Browse all dynamics | list_dynamics() |
| Get specific dynamic schema | list_dynamics('FillColorDynamic') |
| Browse symbols | list_elements('Symbol/HMI') or list_elements('Library') |
| Browse Wizard symbols | list_elements('Wizard') |
| Browse theme colors | list_elements('ThemeColors') |
| Read existing display | get_objects('DisplaysList', names=['PageName'], detail='full') |
| Write display | write_objects('DisplaysList', data=[...]) |
| Read layout | get_objects('DisplaysLayouts', names=['Startup'], detail='full') |
| Read Header | get_objects('DisplaysList', names=['Header'], detail='full') |