Summary
Implementing a VariableChange handler on FTOptix digital I/O requires attaching to the correct node type and keeping the subscription alive. The posted code attaches the handler to a Tag object that is never updated because the underlying IUAVariable does not raise VariableChange events unless its DataChange flag is enabled and the node is a variable, not a tag wrapper.
Root Cause
- The handler is registered on a
Tagwrapper (tIO.VariableChange += OnChange;).
Tagonly forwards change notifications when the parentIUAVariablesignals a data change. In the current loop theTaginstance is discarded after the iteration, so the runtime does not retain a reference to it. VariableChangeEventArgsis never populated because the callOnChange(tIO, null);passesnull. The runtime expects an actual event argument generated by the engine.- The module alias (
aModule) points to a folder, not directly to the variables that generate change events. Subscribing to the folder’s children without enablingDataChangeon each variable results in no events.
Why This Happens in Real Systems
- Transient wrapper objects: When you cast a node to a wrapper class (e.g.,
Tag) and do not store the wrapper, the GC can collect it, breaking the event pipeline. - Missing
DataChangeconfiguration: In FTOptix, a variable must have theDataChangeattribute set totrue(or be aVariablenode withEventNotifierset) for change notifications to be emitted. - Incorrect node path: Using an alias that resolves to a folder rather than the actual variable leads to a silent subscription failure.
Real-World Impact
- No runtime feedback: Operators see stale I/O states, potentially causing safety incidents.
- Debugging overhead: Engineers waste time chasing “missing events” that are silently dropped.
- Resource leakage: Re‑creating handlers on every start without proper disposal can leak memory and degrade performance.
Example or Code (if necessary and relevant)
public override void Start()
{
Log.Info("clsLiveIO.cs Start()...");
var moduleNode = Owner.GetAlias(mcstrAliasModule) as IUANode;
if (moduleNode == null)
throw new Exception($"Alias '{mcstrAliasModule}' not found");
// Find all variable children under the module
var variables = moduleNode.GetChildren()
.OfType()
.Where(v => v.DataChange) // ensure change notifications are enabled
.ToArray();
foreach (var variable in variables)
{
// Keep a strong reference to the wrapper so it is not GC'd
var tag = variable as Tag;
if (tag == null)
{
Log.Warning($"Node {variable.BrowseName} is not a Tag, skipping");
continue;
}
tag.VariableChange += OnChange;
Log.Info($"Subscribed VariableChange for {variable.BrowseName}");
}
Log.Info("clsLiveIO.cs Start() completed...");
}
How Senior Engineers Fix It
- Store the wrapper (
Tag) in a class‑level collection to prevent garbage collection. - Validate that each
IUAVariablehasDataChange = truein the information model; enable it programmatically if needed:variable.DataChange = true;. - Subscribe to the exact variable nodes rather than a folder alias; use
GetChildren()with a filter or explicit paths ("aModule/Pt00/Data"). - Avoid calling the handler manually with
nullargs; let the engine invoke it, or create a properVariableChangeEventArgswhen testing. - Unsubscribe in
Stop()to avoid dangling handlers:foreach (var tag in _registeredTags) tag.VariableChange -= OnChange; _registeredTags.Clear();
Why Juniors Miss It
- Assume wrapper objects are permanent: Newcomers often think casting to
Tagcreates a permanent subscription, overlooking the need for a strong reference. - Ignore
DataChangeflag: The default model may have it disabled, and juniors tend to focus on code rather than model configuration. - Use aliases liberally: They treat an alias as a direct pointer to a variable, not realizing it can resolve to a folder hierarchy.
- Test with manual calls: Passing
nullto the handler masks the fact that the engine never fires the event, giving a false sense of correctness.