title: "Display Construction — Canvas, Dashboard, Dynamics, Symbols & CodeBehind" tags: [display, canvas, dashboard, symbol, navigation, hmi, ui, layout, dynamics, codebehind] description: "Build displays from scratch: Canvas and Dashboard layouts, symbol placement with SymbolLabel binding, visual dynamics (color, visibility, rotation), ActionDynamic click handling, CodeBehind for display logic, and asset-driven navigation" version: "2.0" author: "Tatsoft"


What This Skill Does

Build complete FrameworX displays: choose between Canvas and Dashboard layouts, place controls and symbols with proper tag binding, wire visual dynamics (color, visibility, rotation based on tag values), configure ActionDynamic for navigation, write CodeBehind for display lifecycle events, and set up asset-driven navigation patterns.

When to Use This Skill

Use when:

Do NOT use when:

Prerequisites

MCP Tools and Tables

CategoryItems
Toolsget_table_schema, write_objects, get_objects, list_elements, list_dynamics, browse_runtime_properties
TablesDisplaysList, DisplaysSymbols, DisplaysLayouts

Critical: Use DisplaysList for creating and editing displays. DisplaysDraw is the Designer visual editor UI — NOT a writable table.

Discovery Entry Points

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 (Color, Visibility, Action, Animation, etc.)
list_elements('Symbol/HMI')     -- browse symbol library
list_elements('Canvas')          -- Canvas display structure schema
list_elements('Dashboard')       -- Dashboard display structure schema

list_dynamics() with no parameter is the door to the entire dynamics system — it lists every available dynamic type organized by category with minimal examples.

Implementation Steps

Step 1: Choose Panel Type

Fetch the display schema and decide Canvas vs Dashboard:

get_table_schema('DisplaysList')
list_elements('Canvas') or list_elements('Dashboard')

Canvas — absolute positioning (Left/Top/Width/Height). Use for:

Dashboard — responsive grid (Columns/Rows/Cells). Use for:

PanelType property is REQUIRED — omitting it silently defaults to Canvas, which causes confusing results if you intended a Dashboard.

Step 2: Build a Canvas Display

Canvas uses absolute positioning with Elements array. Each element needs Left, Top, Width, Height.

2a: Query Element Schemas

list_elements('TextBlock,CircularGauge,Rectangle')
list_elements('Shapes')              -- all drawing primitives
list_elements('Interaction')         -- buttons, inputs, toggles
list_elements('Charts')              -- TrendChart, BarChart, PieChart
list_elements('Gauges')              -- CircularGauge, LinearGauge, BulletGauge

Batch lookups with comma-separated names:

list_elements('TrendChart,CircularGauge,TextBlock')

2b: Canvas Construction Example

A concrete Canvas display with shapes, text, a symbol, and a dynamic:

{
  "table_type": "DisplaysList",
  "data": [{
    "ObjectName": "ProcessView",
    "PanelType": "Canvas",
    "Size": "1366 x 728",
    "OnResize": "StretchFill",
    "Elements": [
      {
        "Type": "TextBlock",
        "Text": "Process Overview",
        "Left": 50, "Top": 20, "Width": 300, "Height": 30,
        "FontSize": 22
      },
      {
        "Type": "Rectangle",
        "Left": 50, "Top": 80, "Width": 200, "Height": 150,
        "Dynamics": [
          {
            "Type": "FillColorDynamic",
            "LinkedValue": "@Tag.Plant/Tank1/ValveOpen",
            "ChangeColorItems": {
              "Type": "ColorChangeList",
              "Children": [
                { "Type": "ChangeColorItem", "ChangeLimit": 0, "LimitColor": "#FF808080" },
                { "Type": "ChangeColorItem", "ChangeLimit": 1, "LimitColor": "#FF4CAF50" }
              ]
            }
          }
        ]
      },
      {
        "Type": "TextBlock",
        "LinkedValue": "{@Tag.Plant/Tank1/Level.Value}",
        "Left": 100, "Top": 160, "Width": 100, "Height": 25
      },
      {
        "Type": "Symbol",
        "SymbolName": "Wizard/PUMP",
        "Left": 300, "Top": 120, "Width": 80, "Height": 80,
        "SymbolLabels": [
          { "Type": "SymbolLabel", "Key": "State", "LabelName": "State", "LabelValue": "@Tag.Plant/Pump1/Running.Value", "FieldType": "Expression" }
        ]
      },
      {
        "Type": "CircularGauge",
        "LinkedValue": "@Tag.Plant/Tank1/Pressure.Value",
        "Left": 450, "Top": 80, "Width": 180, "Height": 160,
        "Minimum": 0, "Maximum": 100
      }
    ]
  }]
}

Key Canvas rules:

Step 3: Build a Dashboard Display

Dashboard uses a grid with DashboardDisplay (columns/rows definition) and Cells array.

list_elements('Dashboard')
{
  "table_type": "DisplaysList",
  "data": [{
    "ObjectName": "MainPage",
    "PanelType": "Dashboard",
    "DashboardDisplay": {
      "Columns": ["*", "*", "*"],
      "Rows": ["*", "*"]
    },
    "Cells": [
      {
        "Row": 0, "Col": 0,
        "Cell": { "HeaderLink": "Tank 1 Level" },
        "Content": {
          "Type": "CircularGauge",
          "LinkedValue": "@Tag.Plant/Tank1/Level.Value",
          "Minimum": 0, "Maximum": 100
        }
      },
      {
        "Row": 0, "Col": 1,
        "Cell": { "HeaderLink": "Tank 1 Temp" },
        "Content": {
          "Type": "CircularGauge",
          "LinkedValue": "@Tag.Plant/Tank1/Temperature.Value",
          "Minimum": 0, "Maximum": 100
        }
      },
      {
        "Row": 0, "Col": 2,
        "Cell": { "HeaderLink": "Trend" },
        "Content": {
          "Type": "TrendChart",
          "Duration": "5m",
          "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": "Equipment Status" },
        "Content": {
          "Type": "Symbol",
          "SymbolName": "Wizard/TANK",
          "SymbolLabels": [
            { "Type": "SymbolLabel", "Key": "Value", "LabelName": "Value", "LabelValue": "@Tag.Plant/Tank1/Level.Value", "FieldType": "Expression" }
          ]
        }
      }
    ]
  }]
}

Key Dashboard rules:

Step 4: Place Symbols

Browse available symbols:

list_elements('Symbol')              -- all symbol categories
list_elements('Symbol/HMI')         -- HMI symbol library
list_elements('Symbol/HMI/Equipment') -- equipment symbols
list_elements('Library')             -- all library folders
list_elements('Wizard')              -- Wizard symbol schema

Symbol Placement with SymbolLabel Binding

Symbols are reusable components with parameterized inputs. When you place a symbol, you use SymbolLabels to connect the symbol's internal @Label. parameters to actual @Tag. values. Here's how the mapping works:

Concrete example — placing a Pump symbol:

{
  "Type": "Symbol",
  "SymbolName": "Wizard/PUMP",
  "Left": 200, "Top": 300, "Width": 80, "Height": 80,
  "SymbolLabels": [
    { "Type": "SymbolLabel", "Key": "State", "LabelName": "State", "LabelValue": "@Tag.Plant/Pump1/Running.Value", "FieldType": "Expression" },
    { "Type": "SymbolLabel", "Key": "Speed", "LabelName": "Speed", "LabelValue": "@Tag.Plant/Pump1/Speed.Value", "FieldType": "Expression" }
  ]
}

The Key matches the symbol's internal @Label. name. The LabelValue provides the actual tag binding using @Tag. prefix. The symbol internally resolves @Label.State → the value of @Tag.Plant/Pump1/Running.Value.

CRITICAL: Use @Tag. bindings in SymbolLabels. NEVER use @Label. when placing symbols in displays — @Label. is only for symbol internal definitions (in DisplaysSymbols).

WizardSymbols — always available, no import needed: TANK, VALVE, PUMP, MOTOR, BLOWER. Place with SymbolName: "Wizard/PUMP". These provide easy customization through SymbolLabels for common industrial equipment.

Library symbols (~1,600 available) auto-import when referenced by SymbolName. No manual import step needed — just reference the path (e.g., "HMI/Equipment/CentrifugalPump").

Sizing: Symbols are vector-based and scale to any proportional size. Use 40x40 for compact, 80x80 for medium, 120x120 for large — the library default is just a starting point.

Step 5: Add Visual Dynamics

Dynamics attach runtime behaviors to ANY element. They decouple the visual effect from the data binding, so any property of any control can become live.

Discovery: Call list_dynamics() with no parameter to see all available dynamic types, or narrow by category:

list_dynamics()                      -- all dynamics by category
list_dynamics('Color')               -- color-changing dynamics
list_dynamics('Visibility')          -- show/hide dynamics
list_dynamics('Animation')           -- rotation, position, scaling
list_dynamics('Action')              -- click/navigation dynamics
list_dynamics('FillColorDynamic')    -- full schema for a specific dynamic

5a: FillColorDynamic — Color Changes Based on Tag Value

The most common visual dynamic. Changes an element's fill color based on a tag value.

Pattern: A Dynamics array on the element, containing one or more dynamic objects. Each dynamic has a LinkedValue (data source) and behavior definition.

{
  "Type": "Rectangle",
  "Left": 100, "Top": 200, "Width": 60, "Height": 60,
  "Dynamics": [
    {
      "Type": "FillColorDynamic",
      "LinkedValue": "@Tag.Plant/Pump1/Running",
      "ChangeColorItems": {
        "Type": "ColorChangeList",
        "Children": [
          { "Type": "ChangeColorItem", "ChangeLimit": 0, "LimitColor": "#FF808080" },
          { "Type": "ChangeColorItem", "ChangeLimit": 1, "LimitColor": "#FF00FF00" }
        ]
      }
    }
  ]
}

This makes the rectangle gray when Running = 0 (stopped) and green when Running = 1 (running). The ChangeLimit values define thresholds — the color applies when the tag value reaches that limit.

For analog values (temperature, pressure), use graduated thresholds:

{
  "Type": "FillColorDynamic",
  "LinkedValue": "@Tag.Plant/Tank1/Temperature",
  "ChangeColorItems": {
    "Type": "ColorChangeList",
    "Children": [
      { "Type": "ChangeColorItem", "ChangeLimit": 0,  "LimitColor": "#FF2196F3" },
      { "Type": "ChangeColorItem", "ChangeLimit": 60, "LimitColor": "#FFFFEB3B" },
      { "Type": "ChangeColorItem", "ChangeLimit": 80, "LimitColor": "#FFFF9800" },
      { "Type": "ChangeColorItem", "ChangeLimit": 95, "LimitColor": "#FFF44336" }
    ]
  }
}

5b: VisibilityDynamic — Show/Hide Elements

{
  "Type": "TextBlock",
  "Text": "ALARM",
  "Left": 100, "Top": 50, "Width": 80, "Height": 25,
  "Dynamics": [
    {
      "Type": "VisibilityDynamic",
      "LinkedValue": "@Tag.Plant/Tank1/AlarmActive"
    }
  ]
}

Element is visible when the linked value is non-zero/true, hidden when zero/false.

5c: RotationDynamic — Rotate Elements

{
  "Type": "Symbol",
  "SymbolName": "Wizard/BLOWER",
  "Left": 200, "Top": 100, "Width": 80, "Height": 80,
  "Dynamics": [
    {
      "Type": "RotationDynamic",
      "LinkedValue": "@Tag.Plant/Fan1/Speed",
      "MinAngle": 0, "MaxAngle": 360,
      "MinValue": 0, "MaxValue": 100
    }
  ]
}

5d: Multiple Dynamics on One Element

Elements can have multiple dynamics simultaneously:

{
  "Type": "Rectangle",
  "Left": 100, "Top": 200, "Width": 60, "Height": 60,
  "Dynamics": [
    {
      "Type": "FillColorDynamic",
      "LinkedValue": "@Tag.Plant/Pump1/Running",
      "ChangeColorItems": {
        "Type": "ColorChangeList",
        "Children": [
          { "Type": "ChangeColorItem", "ChangeLimit": 0, "LimitColor": "#FF808080" },
          { "Type": "ChangeColorItem", "ChangeLimit": 1, "LimitColor": "#FF4CAF50" }
        ]
      }
    },
    {
      "Type": "VisibilityDynamic",
      "LinkedValue": "@Tag.Plant/Pump1/Enabled"
    }
  ]
}

Always verify dynamic schemas: Call list_dynamics('FillColorDynamic') (or whichever type) for the exact property names and structure. The examples above are patterns — the schema is the source of truth.

Step 6: Add Click Actions (ActionDynamic)

For button/navigation behavior:

list_dynamics('ActionDynamic')

ActionDynamic supports two formats:

Simple format — single action, auto-mapped to MouseLeftButtonDown:

{
  "Type": "Button",
  "Text": "Go to Details",
  "Left": 50, "Top": 400, "Width": 150, "Height": 40,
  "ActionDynamic": {
    "Type": "NavigateToDisplay",
    "DisplayName": "DetailPage"
  }
}

Nested format — multi-event, explicit mouse event mapping:

{
  "ActionDynamic": [
    {
      "Event": "MouseLeftButtonDown",
      "Actions": [
        { "Type": "NavigateToDisplay", "DisplayName": "DetailPage" }
      ]
    }
  ]
}

Common pattern: navigation button = ActionDynamic + ShineDynamic for visual click feedback.

For all available dynamics:

list_dynamics()     -- lists all dynamic types

Step 7: Write the Display

Displays are document objects — full replacement on write.

Creating a new display: Write the full object as shown in Steps 2–3.

Modifying an existing display — read first, modify, write back:

get_objects('DisplaysList', names=['MainPage'], detail='full')

Then modify the content and write the full object.

MainPage is predefined in new solutions. Write main content directly into it — no need to create a new display for the landing screen.

Step 8: CodeBehind (Client-Side Display Logic)

CodeBehind is CLIENT-SIDE C# or VB.NET code embedded in each display for lifecycle events and operator interaction.

Lifecycle methods:

CodeBehind is stored in the Contents field of the display, formatted as {Language}\r\n{Code}.

Minimal example — a button click that writes a tag value:

{
  "table_type": "DisplaysList",
  "data": [{
    "ObjectName": "ControlPage",
    "PanelType": "Canvas",
    "Size": "1366 x 728",
    "Contents": "CSharp\r\nvoid DisplayOpening()\r\n{\r\n    // Initialize display state\r\n}\r\n\r\nvoid ButtonStart_Click(object sender, EventArgs e)\r\n{\r\n    @Tag.Plant/Pump1/Command.Value = 1;\r\n}\r\n\r\nvoid ButtonStop_Click(object sender, EventArgs e)\r\n{\r\n    @Tag.Plant/Pump1/Command.Value = 0;\r\n}",
    "Elements": [
      {
        "Type": "Button",
        "Text": "Start Pump",
        "Left": 50, "Top": 100, "Width": 120, "Height": 40,
        "ClickEvent": "ButtonStart_Click"
      },
      {
        "Type": "Button",
        "Text": "Stop Pump",
        "Left": 200, "Top": 100, "Width": 120, "Height": 40,
        "ClickEvent": "ButtonStop_Click"
      },
      {
        "Type": "TextBlock",
        "LinkedValue": "{@Tag.Plant/Pump1/Running.Value}",
        "Left": 50, "Top": 160, "Width": 200, "Height": 25
      }
    ]
  }]
}

Key CodeBehind rules:

See the Scripts and Expressions skill for full CodeBehind and server-side scripting guidance.

Step 9: Configure Layouts

Layout regions: Header, Footer, Menu, Submenu, Content.

get_table_schema('DisplaysLayouts')
get_objects('DisplaysLayouts', names=['Startup'], detail='full')

The Startup layout defines which display loads into the Content region (typically MainPage). Only modify if you need:

Layouts are document objects — read first, modify, write back.

Dependencies: DisplaysSymbols → DisplaysList → DisplaysLayouts

Step 10: Asset Navigation Pattern (Advanced)

Common pattern for plant-wide navigation with dynamic content:

Architecture:

Static binding (fixed tag path):

@Tag.Area1/Line1/State

Dynamic binding (resolves based on selected asset):

Asset(Client.Context.AssetPath + "State1")

This allows a single display template to show data for whichever asset the operator selects in the navigation tree.

Setting Up Asset Navigation:

  1. Create tags organized by consistent asset paths (e.g., Plant/Line1/Motor1, Plant/Line2/Motor1 with same member names)
  2. Modify the Startup layout to include an AssetTree region
  3. Use Asset() syntax in display bindings for dynamic resolution
  4. Use browse_runtime_properties('Client') at design time to explore the Client namespace and Context properties

Step 11: Theme and Color Management

When setting custom colors on elements, be aware that theme overrides can silently replace your custom values at runtime.

Best practice: OMIT color properties to use themed defaults. Only specify colors when intentionally overriding for process-specific meaning (alarm red, water blue, etc.).

Pattern — set color AND clear theme (when you must override):

{
  "Fill": "#FF3498DB",
  "FillTheme": ""
}

If you only set Fill without clearing FillTheme, the theme engine may override your color. Always clear the corresponding theme property when setting custom colors.

Use list_elements('ThemeColors') for available named theme brushes (StateOK, StateAlarm, Water, AlarmHighPriority, etc.).

Common Pitfalls

MistakeWhy It HappensHow to Avoid
Using DisplaysDraw as table_typeConfused with DisplaysListDisplaysDraw is the visual editor UI, not a writable table
Omitting PanelTypeDefaults silently to CanvasAlways set PanelType explicitly
Using @Label. in display elementsConfused with symbol definitions@Label. is only for DisplaysSymbols internals. Use @Tag. when placing symbols
Not discovering dynamics firstDoesn't know what's availableCall list_dynamics() with no parameter to see all dynamic types
Screenshots for self-validationHabit from other toolsTrust write_objects success. User sees live updates
Setting colors without clearing themeTheme overrides custom colorsSet value AND clear theme: {Fill: '#FF3498DB', FillTheme: ''} — or just omit to use themed defaults
Sending partial display contentForget it's a document objectAlways read-modify-write for existing displays
Canvas positioning in DashboardMixing layout paradigmsCanvas uses Left/Top + Elements; Dashboard uses Row/Col + Cells
Referencing symbols with wrong pathIncomplete library pathBrowse with list_elements('Symbol/HMI') to get exact paths
Guessing dynamic property namesDifferent dynamics have different schemasAlways call list_dynamics('DynamicTypeName') for exact schema
CodeBehind in wrong Contents formatMissing language prefixContents must start with CSharp\r\n or VBdotNet\r\n before code

Quick Reference

Display ActionTool Call
Get display schemaget_table_schema('DisplaysList')
Get Canvas structurelist_elements('Canvas')
Get Dashboard structurelist_elements('Dashboard')
Get element schemalist_elements('ElementName')
Browse all dynamicslist_dynamics()
Get specific dynamic schemalist_dynamics('FillColorDynamic')
Browse symbolslist_elements('Symbol/HMI') or list_elements('Library')
Browse Wizard symbolslist_elements('Wizard')
Read existing displayget_objects('DisplaysList', names=['PageName'], detail='full')
Write displaywrite_objects('DisplaysList', data=[...])
Navigate Designer to displaydesigner_action('navigate', 'Display.PageName')
Browse runtime propertiesbrowse_runtime_properties('Client')

What's Next