Problem Overview
An Inductive Automation Ignition (formerly FactoryPMI) Vision label component is bound to a switch expression that should map an integer tank value to a human-readable week tag (e.g., 5 -> BBT: 5W). The label correctly displays the static prefix BBT: , but the dynamic suffix returns the fallback token N/A even though the underlying value is verifiably 5 in the source dataset.
Typical failing expression as written on the label's text property binding:
"BBT: " +
switch(
{cntBottling.dsTankData}["BBTW2"],
0,
1,
2,
3,
4,
5,
6,
7,
8,
"N/A",
"1W",
"2W",
"3W",
"4W",
"5W",
"6W",
"7W",
"8W",
"N/A")
Symptom: BBT: N/A instead of BBT: 5W. The PLC tag, database query, and dataset row all show the value 5; only the expression evaluates to the fallback branch.
switch expression returns the final fallback value regardless of the candidate value, the value being compared is not equal to any literal in the case list. In numeric ladders this almost always indicates a type mismatch between the expression argument and the case literals.
Root Cause: Type Mismatch in Dataset Columns
The switch expression in Ignition's expression language performs strict equality comparison between the argument value and each positional case literal. When the argument resolves to a Java Double (e.g., 5.0) but the case literals are integers (5), the comparison fails for every case and the expression falls through to the last fallback value.
Even when the designer dataset viewer reports the column as "integer", the value entering the binding pipeline can be a Double if any of the following are true:
- The database table column is defined as
FLOAT,DOUBLE,REAL,NUMERIC, orDECIMAL. - The SQL query driving the dataset returns a numeric type that JDBC promotes to
Double. - A historian tag is bound to the dataset and the underlying PLC tag is a
REALorLREAL. - An upstream expression has implicitly widened the value (e.g., division, multiplication, or
toFloat()).
Why the Designer Dataset Viewer Can Lie
The Vision dataset viewer inspects the Java object currently cached in the dataset. If the cache is populated from a Float/Double source column, the viewer may display "Integer" when the value has no fractional component (e.g., 5.0 is indistinguishable from 5 on screen), but the actual Class of the object is still java.lang.Double. Always verify by adding a temporary toString() binding to inspect the raw value, or use a debug expression that returns the type.
Diagnostic Procedure
- Open the Window containing the label and open the binding on the
textproperty. - Confirm the candidate value by adding a side expression:
toString({cntBottling.dsTankData}["BBTW2"]). Bind this to a second label. If the value is shown as5with no decimal point but the switch still fails, the underlying object is aDouble. - Open the dataset in the dataset viewer (table-with-magnifying-glass icon in the Vision component palette).
- Click a cell in the
BBTW2column. Read the column type displayed at the bottom of the viewer. - Compare the dataset column type to the database table column type. If the DB column is
FLOAT/DOUBLE/REALbut the dataset showsInteger, the JDBC driver is silently promoting the value. - Check that Designer Database Traffic is enabled (Project Browser -> right-click the Database Connection -> Enabled). Without this, the designer never receives the cached value and the expression evaluates against
null, returning the fallback branch. - Confirm there is exactly one row in
dsTankData. With zero rows,{dsTankData}["BBTW2"]evaluates tonulland every numeric comparison fails.
Solution 1: Cast the Argument with toInt()
The fastest fix is to coerce the argument to the same numeric type as the case literals before the comparison runs:
"BBT: " +
switch(
toInt({cntBottling.dsTankData}["BBTW2"]),
0,
1,
2,
3,
4,
5,
6,
7,
8,
"N/A",
"1W",
"2W",
"3W",
"4W",
"5W",
"6W",
"7W",
"8W",
"N/A")
toInt() truncates toward zero. 5.0 -> 5, 5.9 -> 5, -5.9 -> -5. If you need banker's-style rounding instead, wrap with round() first: toInt(round({dsTankData}["BBTW2"])).
Solution 2: Match Literals to the Argument Type
If the argument is genuinely Double and you want to preserve fractional information downstream, change every case literal to a float-typed numeric by appending .0, or multiply the argument by 1.0 in a pre-binding. Example with float literals:
switch(
{cntBottling.dsTankData}["BBTW2"],
0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,
"N/A",
"1W", "2W", "3W", "4W", "5W", "6W", "7W", "8W",
"N/A")
This is fragile because floating-point equality is never guaranteed; prefer Solution 1 whenever the input is conceptually an integer.
Solution 3: Correct the Dataset Column Type at the Source
The cleanest long-term fix is to keep the data type consistent end-to-end:
- Change the DB table column from
FLOAT/DOUBLEtoINT/BIGINTif no fractional storage is required. - If the PLC tag is
REALorLREAL, either change it toINTor add a scaling tag and store the integer in the DB. - Re-create the SQL query binding so the dataset column re-derives from the corrected column.
- Force a refresh of the dataset cache (right-click -> Refresh) and re-validate the dataset viewer.
Switch Expression Syntax Reference
The Ignition switch expression uses positional arguments: switch(value, case0, case1, ..., caseN, result0, result1, ..., resultN, defaultResult). The number of case literals must equal the number of result values, with the trailing argument as the default. The expression language is documented in Inductive University's Expression Binding – Switch video series at inductiveuniversity.com and the 8.0 revision.
For comparison, the C# switch expression uses pattern-matching with => arms and does not suffer the positional ambiguity of Ignition's switch. Java 14+ switch expressions use the same arrow syntax. When porting logic between PLC HMI scripts and JVM-based languages, expect this syntactic and semantic drift.
| Position | Argument | Required Type Match | Notes |
|---|---|---|---|
| 0 | Value being tested | numeric or string | Type determines comparison rules |
| 1..N | Case literals | Same type as value | Strict equality, no coercion |
| N+1..2N | Result literals | Same type as final result | Mixing types triggers a coercion warning |
| 2N+1 | Default result | Same type as results | Always returned on no match |
Type Coercion Rules in the Ignition Expression Language
The expression engine will silently coerce in some directions and refuse to coerce in others:
| From | To | Behavior |
|---|---|---|
| Double | Int (case literal) | Not coerced — strict equality fails |
| Int | Double (case literal) | Coerced on comparison; matches 5 against 5.0
|
| String "5" | Int 5 | Not coerced — falls to default |
| Int 5 | String "5" | Not coerced — falls to default |
| Float | Double | Coerced automatically by JVM widening |
| null | Any | Never matches; always returns default |
If you suspect a type problem but cannot inspect the dataset live, use typeOf({dsTankData}["BBTW2"]) in a side label. The expression returns the Java class name as a string, which removes all ambiguity.
Common Pitfalls in Vision Label Bindings
-
Designer DB traffic disabled. The designer shows stale or empty data; runtime clients see fresh data. The expression in the designer falls back to
N/A, masking the fact that production is correct. -
Multi-row datasets. A non-aggregated
SELECT *query returns many rows; the binding picks row 0 in the designer but may bind to a different row on a polling client. -
Indirect tag indirection. A UDT member referenced through
{[~]Tank/BBTW2}may resolve to aFloatwhile the expression expects anInt. - Property binding inheritance. A label inside a template with parameter binding can wrap the value in another transformation (scaling, deadband) before the local expression evaluates.
-
Bidirectional binds. A two-way
valuebinding on a numeric input will return aStringwhen the input mask is enabled.
Verification
- With the corrected binding deployed, open the Vision Client and confirm the label reads
BBT: 5Wfor the tank in question. - In the dataset viewer, hover the
BBTW2cell and verify the column type isInteger. - Force a value change at the PLC (write
3to the tag) and confirm the label updates toBBT: 3Wwithin one polling cycle. - Set the PLC value to
0and confirm the label updates toBBT: 1W(since the original switch maps case0to result 1W; verify this matches the project's intent). - Set the PLC value to a number outside the 0–8 range (e.g.,
99) and confirm the label showsBBT: N/A. - Re-open the designer, confirm the binding evaluates correctly there too. If only the client shows the right value, designer DB traffic is disabled — enable it on the database connection.
Troubleshooting Matrix
| Symptom | Likely Cause | Verification | Fix |
|---|---|---|---|
| Always returns final fallback | Argument is null or empty dataset | Check row count, Designer DB traffic | Fix query, enable traffic |
| Always returns final fallback | Type mismatch (Double vs Int) |
typeOf() side label |
toInt() wrapper |
| Matches only some values | Locale decimal separator in dataset | Inspect raw query output | Cast to numeric at query level |
| Returns result of wrong case | Off-by-one in positional switch | Count literals vs results | Rebuild expression with correct count |
| Designer correct, client wrong | Tag history vs live tag mismatch | Check binding source path | Point at the live tag, not the history binding |
| Client correct, designer wrong | Designer DB traffic disabled | Project Browser -> DB Connection -> Enabled | Enable traffic |
| Intermittent fallback | Null during polling gap | Check tag quality and scan class | Wrap with if(hasValue(...), ... , default)
|
Best Practices for Expression Bindings on Labels
- Always normalize numeric types at the boundary (dataset, tag, or expression). Use
toInt()/toFloat()/toDouble()at the top of any expression that performs equality. - Avoid positional
switchfor more than ~10 cases. Refactor to a lookup dataset joined on the value, or to a UDT parameter mapping. - Keep the default result last and visibly different from any real result so debugging is trivial.
- Wrap unstable expressions in
if(hasValue(x), switch(...), "N/A")so polling gaps don't surface as bogus domain values. - Document the data type contract on the dataset itself — set column type explicitly in the SQL query's
CAST(... AS ...)rather than relying on JDBC inference. - For multi-developer projects, centralize common label mappings (week codes, status codes) as expression functions in the project library rather than copying inline
switches into every window.
FAQ
Why does my Ignition switch expression always return the default value even when the value looks correct?
The argument and the case literals are different Java types. A dataset value of 5.0 (Double) will not strictly equal the integer literal 5. Wrap the argument with toInt() or change the literals to 5.0 to make the types match.
How do I confirm the actual Java type of a value inside an Ignition expression?
Add a temporary label binding that calls typeOf({dsTankData}["BBTW2"]). The expression returns the class name (for example, java.lang.Double) so you can compare it directly against the case literal types in your switch.
The expression works in the Vision Client but not in the Designer. What's wrong?
Designer Database Traffic is disabled on the database connection. Enable it via the Project Browser: right-click the database connection, then toggle Enabled. Without designer traffic, the cached dataset is empty and the expression evaluates against null, hitting the default branch.
Can I use a string argument with a numeric switch in Ignition?
No. The Ignition expression switch does not coerce between strings and numbers. If the dataset column is a String, either change the column type in the source database, parse it inside the expression with toInt({dsTankData}["BBTW2"]), or rebuild the switch with string cases such as "5" -> "5W".
Does toInt() round or truncate in Ignition?
toInt() truncates toward zero. Use round() first if you need symmetric rounding, for example toInt(round({dsTankData}["BBTW2"])). For banker's rounding, use toInt(round(x, 0)) with the explicit precision argument.