Home / FactoryTalk Optix / Advanced Topics

Mastering Design-Time NetLogic in FactoryTalk Optix: Automating Project Engineering with C#

In a previous module, we explored how Runtime NetLogic executes live C# code on an OptixPanel or industrial edge PC to handle timers, background calculations, and IT/OT data bridging. However, some of the most time-consuming bottlenecks in HMI development don’t happen at runtime—they happen at your engineering workstation while building the application.

Whether you are creating 50 identical alarm tags, building a standardized navigation skeleton for a new machine line, or generating dozens of status rows on a diagnostic screen, repetitive point-and-click engineering eats up billable hours and introduces human error.

Design-Time NetLogic solves this by letting you run C# scripts inside FactoryTalk Optix Studio during development. Because every folder, variable, alarm, and UI widget in Optix is a node in an OPC UA Information Model, a Design-Time script can programmatically generate, modify, or audit hundreds of project nodes with a single click.

1. Design-Time vs. Runtime NetLogic: Key Architectural Differences

While both script types use C# and interact with the same InformationModel API, their lifecycle and execution triggers are completely different:

CharacteristicDesign-Time NetLogicRuntime NetLogic
Base ClassBaseDesignTimeNetLogicBaseNetLogic
Execution EnvironmentRuns strictly inside FactoryTalk Optix Studio during engineering. It does not execute on the deployed HMI terminal.Runs inside the Optix Runtime / Emulator when the application is live.
Lifecycle MethodsDoes not use Start() or Stop().Uses Start() (initialization) and Stop() (cleanup).
Trigger MechanismTriggered manually by the engineer right-clicking the script node in Studio (or clicking a Method button in the Properties pane) configured with [ExportMethod].Triggered by runtime UI events (OnMouseClick), PLC tag transitions, or PeriodicTask timers.
Primary OutputPermanently creates, modifies, or deletes nodes in your Optix Studio project tree (saved into your project .yaml files).Manipulates in-memory runtime values, external APIs, or transient session screens.

Think of Design-Time NetLogic like Studio 5000 L5X Scripting or ACM: If you have ever used Excel VBA to bulk-generate Studio 5000 .L5X rungs or Rockwell’s Application Code Manager (ACM) to instantiate modular code libraries, Design-Time NetLogic gives you that exact capability natively inside your HMI project without exporting or re-importing files.

2. Core API Building Blocks: Project.Current and InformationModel

To build or modify nodes in your project tree, Design-Time NetLogic relies on four foundational methods across the FTOptix.HMIProject and UAManagedCore namespaces:

  1. Project.Current.Get("Path/To/Node"): Locates an existing folder or object in your project tree relative to the root project folder (for example, Project.Current.Get<Folder>("Model") or Project.Current.Get("UI/Screens")).
  2. InformationModel.MakeObject<T>("NodeName"): Instantiates a new object or UI widget in memory—such as a Folder, Panel, ColumnLayout, RowLayout, Label, Button, or LED.
  3. InformationModel.MakeVariable("VarName", OpcUa.DataTypes.DataType): Instantiates a new variable node of a specific OPC UA data type (e.g., OpcUa.DataTypes.Int32, OpcUa.DataTypes.Boolean, OpcUa.DataTypes.Float, OpcUa.DataTypes.String).
  4. parentNode.Add(childNode): Attaches your newly created variable or UI element into the project tree under the target parent node.

Essential Namespaces to Include

One of the most common compiler errors when writing Design-Time scripts from scratch (such as CS0246: BaseDesignTimeNetLogic could not be found or CS0103: InformationModel does not exist) happens when a required namespace is missing from the #region Using directives block. Always ensure these core namespaces are present at the top of your script:

C#

using System;
using UAManagedCore;
using OpcUa = UAManagedCore.OpcUa;
using FTOptix.HMIProject;
using FTOptix.NetLogic;
using FTOptix.UI;
using FTOptix.NativeUI;
using FTOptix.CoreBase;
using FTOptix.Core;

3. Making Scripts Idempotent (Safe to Run Multiple Times)

When an engineer clicks Execute on a Design-Time method twice in a row, what happens? If your script blindly calls modelFolder.Add(newVar) without checking whether newVar already exists, Optix will either throw an exception or clutter your tree with duplicate renamed nodes (Pump1, Pump1_1, Pump1_2).

A well-engineered Design-Time script is idempotent—meaning you can run it once or ten times and it will cleanly update or rebuild the target structure without creating duplicates. Always check if a node exists before creating it:

C#

// Check if the "PumpStation" folder already exists inside "Model"
var modelFolder = Project.Current.Get<Folder>("Model");
var existingFolder = modelFolder.Get<Folder>("PumpStation");
if (existingFolder != null)
{
// Option A: Delete it to perform a clean rebuild
existingFolder.Delete();
}
// Now create a fresh "PumpStation" folder
var pumpFolder = InformationModel.MakeObject<Folder>("PumpStation");
modelFolder.Add(pumpFolder);

4. Hands-On Tutorial: Auto-Generating a Pump Station Model & Diagnostic Screen

Let’s build a practical Design-Time NetLogic generator. Imagine you are commissioning a cooling water skid with 5 pumps. With a single click in Optix Studio, we want our C# script to:

  1. Navigate to the Model folder, create a PumpStationData subfolder, and generate structured tag sets for Pump_1 through Pump_5 (RunStatus, SpeedRPM, and FaultActive).
  2. Navigate to the UI folder, create a new screen Type called PumpDiagnosticsScreen, build a clean header banner and vertical layout container, and automatically generate 5 formatted UI status rows—each pre-wired with Dynamic Links to its corresponding Model variables.

Step 1: Create the Design-Time NetLogic Node

  1. In FactoryTalk Optix Studio, right-click the NetLogic folder in the Project view.
  2. Select New -> Design-Time NetLogic.
  3. Name the node PumpStationBuilder.
  4. (Optional) Right-click PumpStationBuilder, select New -> Variable -> Int32, name it NumberOfPumps, and set its value to 5.

Step 2: Add the C# Generator Code (PumpStationBuilder.cs)

Double-click PumpStationBuilder to launch your code editor and replace the template with the following script:

C#

#region Using directives
using System;
using UAManagedCore;
using OpcUa = UAManagedCore.OpcUa;
using FTOptix.HMIProject;
using FTOptix.NetLogic;
using FTOptix.UI;
using FTOptix.NativeUI;
using FTOptix.CoreBase;
using FTOptix.Core;
#endregion
public class PumpStationBuilder : BaseDesignTimeNetLogic
{
[ExportMethod]
public void GeneratePumpModelAndUI()
{
// Read the desired pump count from a child variable (or default to 5)
var countVar = LogicObject.GetVariable("NumberOfPumps");
int pumpCount = countVar != null ? (int)countVar.Value : 5;
Log.Info("PumpStationBuilder", $"Starting automated build for {pumpCount} pumps...");
// 1. Generate the Information Model variables inside the "Model" folder
Folder dataFolder = BuildModelVariables(pumpCount);
// 2. Generate the Diagnostic Screen UI and bind widgets to the new variables
BuildDiagnosticScreenUI(pumpCount, dataFolder);
Log.Info("PumpStationBuilder", "Automated build completed successfully!");
}
private Folder BuildModelVariables(int count)
{
var modelRoot = Project.Current.Get<Folder>("Model");
if (modelRoot == null)
{
throw new Exception("Could not locate the root 'Model' folder in the project.");
}
// Clean up existing folder if re-running the generator
var existingFolder = modelRoot.Get<Folder>("PumpStationData");
existingFolder?.Delete();
// Create a fresh container folder for our pump variables
var stationFolder = InformationModel.MakeObject<Folder>("PumpStationData");
modelRoot.Add(stationFolder);
for (int i = 1; i <= count; i++)
{
var pumpSubfolder = InformationModel.MakeObject<Folder>($"Pump_{i}");
// Create RunStatus (Boolean)
var runVar = InformationModel.MakeVariable("RunStatus", OpcUa.DataTypes.Boolean);
runVar.Value = false;
pumpSubfolder.Add(runVar);
// Create SpeedRPM (Float)
var speedVar = InformationModel.MakeVariable("SpeedRPM", OpcUa.DataTypes.Float);
speedVar.Value = 1750.0f;
pumpSubfolder.Add(speedVar);
// Create FaultActive (Boolean)
var faultVar = InformationModel.MakeVariable("FaultActive", OpcUa.DataTypes.Boolean);
faultVar.Value = false;
pumpSubfolder.Add(faultVar);
stationFolder.Add(pumpSubfolder);
Log.Info("PumpStationBuilder", $"Created Model tags for Pump_{i}");
}
return stationFolder;
}
private void BuildDiagnosticScreenUI(int count, Folder stationDataFolder)
{
// Locate the UI/Screens folder (or fallback to root UI folder)
var uiTargetFolder = Project.Current.Get<Folder>("UI/Screens") ?? Project.Current.Get<Folder>("UI");
if (uiTargetFolder == null)
{
throw new Exception("Could not locate the 'UI' folder in the project.");
}
// Remove previous generated screen if it exists
var existingScreen = uiTargetFolder.Get("PumpDiagnosticsPanel");
existingScreen?.Delete();
// Create a new Panel instance to hold our generated layout
var mainPanel = InformationModel.MakeObject<Panel>("PumpDiagnosticsPanel");
mainPanel.Width = 800;
mainPanel.Height = 600;
mainPanel.HorizontalAlignment = HorizontalAlignment.Stretch;
mainPanel.VerticalAlignment = VerticalAlignment.Stretch;
// Create a master vertical ColumnLayout inside the Panel
var masterColumn = InformationModel.MakeObject<ColumnLayout>("MasterLayout");
masterColumn.HorizontalAlignment = HorizontalAlignment.Stretch;
masterColumn.VerticalAlignment = VerticalAlignment.Stretch;
masterColumn.RowMargin = 10;
// --- A. Build the Top Header Banner ---
var headerBanner = InformationModel.MakeObject<Rectangle>("HeaderBanner");
headerBanner.Height = 60;
headerBanner.HorizontalAlignment = HorizontalAlignment.Stretch;
headerBanner.FillColor = new Color(255, 20, 58, 92); // Industrial dark blue
var headerTitle = InformationModel.MakeObject<Label>("HeaderTitle");
headerTitle.Text = "PUMP STATION DIAGNOSTICS OVERVIEW";
headerTitle.TextColor = new Color(255, 255, 255, 255);
headerTitle.FontWeight = FontWeight.Bold;
headerTitle.FontSize = 20;
headerTitle.HorizontalAlignment = HorizontalAlignment.Center;
headerTitle.VerticalAlignment = VerticalAlignment.Center;
headerBanner.Add(headerTitle);
masterColumn.Add(headerBanner);
// --- B. Build a Status Row for Each Pump ---
for (int i = 1; i <= count; i++)
{
var pumpVarFolder = stationDataFolder.Get<Folder>($"Pump_{i}");
var runVar = pumpVarFolder.GetVariable("RunStatus");
var speedVar = pumpVarFolder.GetVariable("SpeedRPM");
// Create a horizontal RowLayout for this pump
var row = InformationModel.MakeObject<RowLayout>($"PumpRow_{i}");
row.Height = 50;
row.HorizontalAlignment = HorizontalAlignment.Stretch;
row.ColumnMargin = 20;
row.LeftMargin = 24;
row.RightMargin = 24;
// 1. Pump Name Label
var nameLabel = InformationModel.MakeObject<Label>($"Lbl_PumpName_{i}");
nameLabel.Text = $"Cooling Pump #{i}";
nameLabel.Width = 180;
nameLabel.FontWeight = FontWeight.Bold;
nameLabel.VerticalAlignment = VerticalAlignment.Center;
row.Add(nameLabel);
// 2. Run Status LED (Dynamically linked to RunStatus variable)
var statusLed = InformationModel.MakeObject<LED>($"Led_Run_{i}");
statusLed.Width = 32;
statusLed.Height = 32;
statusLed.VerticalAlignment = VerticalAlignment.Center;
statusLed.GetVariable("Active").SetDynamicLink(runVar, DynamicLinkMode.Read);
row.Add(statusLed);
// 3. Speed RPM Value Label (Dynamically linked to SpeedRPM variable)
var speedLabel = InformationModel.MakeObject<Label>($"Lbl_Speed_{i}");
speedLabel.Width = 140;
speedLabel.VerticalAlignment = VerticalAlignment.Center;
speedLabel.GetVariable("Text").SetDynamicLink(speedVar, DynamicLinkMode.Read);
row.Add(speedLabel);
// 4. Manual Toggle Button (Dynamically toggles RunStatus variable)
var toggleBtn = InformationModel.MakeObject<Switch>($"Sw_Run_{i}");
toggleBtn.Width = 80;
toggleBtn.VerticalAlignment = VerticalAlignment.Center;
toggleBtn.GetVariable("Checked").SetDynamicLink(runVar, DynamicLinkMode.ReadWrite);
row.Add(toggleBtn);
masterColumn.Add(row);
}
mainPanel.Add(masterColumn);
uiTargetFolder.Add(mainPanel);
Log.Info("PumpStationBuilder", "Generated 'PumpDiagnosticsPanel' under UI.");
}
}

Step 3: Execute the Generator in Optix Studio

  1. Save the file in Visual Studio Code and return to FactoryTalk Optix Studio.
  2. Wait 2–3 seconds for the NetLogic compiler notification in the Output pane to confirm Build succeeded.
  3. Click on PumpStationBuilder under the NetLogic folder.
  4. In the right-hand Properties panel (or by right-clicking the PumpStationBuilder node), click Execute next to GeneratePumpModelAndUI.

Immediately check your Project tree:

  • Expand Model -> PumpStationData: You will see Pump_1 through Pump_5 folders, each populated with RunStatus, SpeedRPM, and FaultActive variables.
  • Expand UI/Screens (or UI) and double-click PumpDiagnosticsPanel: You will see a complete 800×600 screen with a dark-blue header banner and 5 aligned rows of labels, LEDs, live speed readouts, and interactive toggle switches—all pre-wired with Dynamic Links.
  • Want 8 pumps instead of 5? Change the NumberOfPumps child variable on PumpStationBuilder to 8, click Execute again, and watch the model and UI rebuild themselves in less than a second.

5. How Programmatic Dynamic Links Work (SetDynamicLink)

Look closely at how the script wired the LED and Switch widgets to our newly created variables without human intervention:

C#

// Read-only binding (Model Variable -> LED Active Property)
statusLed.GetVariable("Active").SetDynamicLink(runVar, DynamicLinkMode.Read);
// Bidirectional binding (Model Variable <-> Switch Checked Property)
toggleBtn.GetVariable("Checked").SetDynamicLink(runVar, DynamicLinkMode.ReadWrite);

In FactoryTalk Optix, every property on a UI widget (such as Text, Visible, Width, Active, or Checked) is itself backed by an underlying IUAVariable. Calling .GetVariable("PropertyName").SetDynamicLink(targetVariable, DynamicLinkMode) in C# performs the exact same action as clicking the little chain-link icon in the Studio Properties panel.

6. High-Value Industrial Use Cases for Design-Time NetLogic

Once you understand how to manipulate the InformationModel at design time, you can build reusable engineering tools that live in a custom Optix Template Library and drop them into every project you commission:

  1. CSV-to-Alarm Generator: Read an exported PLC tag CSV file (System.IO.File.ReadAllLines()) and automatically instantiate hundreds of DigitalAlarm or ExclusiveLevelAlarmController nodes with pre-populated message strings and severity levels.
  2. Standard Application Scaffolder: On day one of a new project, run a single script that builds your company’s standard 800x600 or 1920x1080 MainWindow layout—complete with top header banner, corporate color StyleSheet, NavigationPanel, and empty placeholder screens (Overview, Alarms, Trends, Settings).
  3. Tag & Naming Convention Auditor: Write an audit script that loops through every node in UI and Model, checking for unlinked Dynamic Links, hardcoded strings lacking localization keys, or widgets that violate your plant’s naming conventions, printing a clickable report directly to the Optix Studio Log pane.