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:
| Characteristic | Design-Time NetLogic | Runtime NetLogic |
| Base Class | BaseDesignTimeNetLogic | BaseNetLogic |
| Execution Environment | Runs 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 Methods | Does not use Start() or Stop(). | Uses Start() (initialization) and Stop() (cleanup). |
| Trigger Mechanism | Triggered 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 Output | Permanently 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
.L5Xrungs 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:
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")orProject.Current.Get("UI/Screens")).InformationModel.MakeObject<T>("NodeName"): Instantiates a new object or UI widget in memory—such as aFolder,Panel,ColumnLayout,RowLayout,Label,Button, orLED.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).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" foldervar 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:
- Navigate to the
Modelfolder, create aPumpStationDatasubfolder, and generate structured tag sets forPump_1throughPump_5(RunStatus,SpeedRPM, andFaultActive). - Navigate to the
UIfolder, create a new screen Type calledPumpDiagnosticsScreen, 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 correspondingModelvariables.
Step 1: Create the Design-Time NetLogic Node
- In FactoryTalk Optix Studio, right-click the NetLogic folder in the Project view.
- Select New -> Design-Time NetLogic.
- Name the node
PumpStationBuilder. - (Optional) Right-click
PumpStationBuilder, select New -> Variable -> Int32, name itNumberOfPumps, and set its value to5.
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 directivesusing 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;#endregionpublic 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
- Save the file in Visual Studio Code and return to FactoryTalk Optix Studio.
- Wait 2–3 seconds for the NetLogic compiler notification in the Output pane to confirm Build succeeded.
- Click on
PumpStationBuilderunder the NetLogic folder. - In the right-hand Properties panel (or by right-clicking the
PumpStationBuildernode), click Execute next toGeneratePumpModelAndUI.
Immediately check your Project tree:
- Expand
Model -> PumpStationData: You will seePump_1throughPump_5folders, each populated withRunStatus,SpeedRPM, andFaultActivevariables. - Expand
UI/Screens(orUI) and double-clickPumpDiagnosticsPanel: 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
NumberOfPumpschild variable onPumpStationBuilderto8, 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:
- CSV-to-Alarm Generator: Read an exported PLC tag CSV file (
System.IO.File.ReadAllLines()) and automatically instantiate hundreds ofDigitalAlarmorExclusiveLevelAlarmControllernodes with pre-populated message strings and severity levels. - Standard Application Scaffolder: On day one of a new project, run a single script that builds your company’s standard
800x600or1920x1080MainWindowlayout—complete with top header banner, corporate colorStyleSheet,NavigationPanel, and empty placeholder screens (Overview,Alarms,Trends,Settings). - Tag & Naming Convention Auditor: Write an audit script that loops through every node in
UIandModel, 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.
Tutorial List
- OptixEdge First Steps
- Running Node-RED with Docker
- Softing EdgeConnector Docker Container
- Energy Management with Pluto Analytics
- Add an Apache Webserver in OptixEdge
- Two Isolated Containers Deployed in OptixEdge
- Chirpstack LoRaWAN Network Server in OptixEdge
- Private Chirpstack Network Server in OptixEdge
- Parsing Chirpstack Sensor Data with OptixEdge
- Mastering Designtime NetLogic in FT Optix
