The symptom is always the same: a single Button lives inside a Vision template, a Template Repeater stamps out two or three copies, and the requirement is a radio-group behavior. Click instance 1, it turns green. Click instance 2, it turns green and instance 1 returns to its idle color. The scripts that get written first produce a button that colors itself on click and never un-colors, because they attack the final element instead of the signal that should drive it. Look at the signal chain first: what is the selection state, where does it live, and how does each button read it.
Why does event.source.parent.getComponent("Button 2") do nothing in a repeater?
The first attempt is a per-button actionPerformed script that paints itself and then reaches for its neighbors by name:
event.source.buttonBG = 255,255,255
event.source.parent.getComponent("Button 2").buttonBG = 0,0,0
event.source.parent.getComponent("Button 3").buttonBG = 0,0,0
That works when three separately named buttons sit on one window. It fails in a repeater for two reasons. First, the template contains exactly one Button, so there is no "Button 2" anywhere in the template's container; getComponent returns None and the line throws. Second, event.source.parent is the root container of that one template instance, not the repeater. Each stamped instance is its own isolated container; a script inside instance 1 has no sibling named instance 2 in scope. The observed result matches this exactly: the clicked button changes color, the previously clicked button keeps its color forever.
Why does a Toggle Button with a local color script leave the old button lit?
The second attempt swaps in a Toggle Button and colors it from its own selected state:
if event.source.selected:
event.source.buttonBG = 255,255,255
else:
event.source.buttonBG = 0,0,0
This is a purely local loop. Each instance measures only itself and acts only on itself. Nothing in the chain carries "instance 2 was just selected" back to instance 1, so mutual exclusion never happens. It also hides a classic scripting trap: a stray - in place of = on the assignment line turns the else-branch into a subtraction that silently does nothing and leaves the button in whatever color it had. Tuning the colors does not fix a chain that has no shared signal.
Why is looping over templatePath or walking the repeater tree the wrong long-term fix?
The third attempt loops over event.source.parent.getComponents(), checks each component's templatePath, and resets every matching template to the idle color before painting the clicked one:
for comp in event.source.parent.getComponents():
try:
if "your template name" in comp.templatePath:
event.source.parent.getComponent(comp.name).buttonBG = 0,0,0
except:
pass
event.source.buttonBG = 255,255,255
This is valid when template instances are dropped directly onto a window, because the window's root container is then parent and it does hold every instance. Inside a repeater, parent is again the instance's own container, so the loop finds nothing and the bare except: pass hides that fact.
The fourth attempt climbs out of the instance with a chain of .parent.parent... until it reaches the Template Repeater, then descends into its internals. Discovery is done with a recursive printer:
def printAllObjectComponents(object, indexPrefix='', indent=''):
components = object.getComponents()
for index, component in enumerate(components):
print "%s %s%s %s" % (indent, indexPrefix, index, component)
printAllObjectComponents(component, indexPrefix + str(index) + '.', indent + '\t')
On the tested build the instances were found at templateRepeater.getComponent(1).getComponent(0).getComponent(0).getComponents(), and a propertyChange script keyed on event.propertyName == 'refresh' could then iterate them and set component properties. It works, but it depends on undocumented index positions inside the repeater that Inductive Automation may reorder in any release, and every stamped color is lost the next time the repeater rebuilds its instances. Treat this as a diagnostic tool, not production logic.
What is the real cause: where does the selection state live?
Every failed attempt pushes state into the final element. The button's background is an output; the selection is an input. The fix is to move the selection into one shared signal that every instance can read, and let each instance derive its own color from that signal with a binding. In a Vision client, the natural home for per-user UI state is a client tag: it is scoped to the client session, it is not shared between operators, it survives repeater refreshes, and expression bindings re-evaluate the instant it changes.
The working configuration measures one thing (which instance was clicked), stores it in one place ([client]btnColor), and lets three expression bindings compare their own identity against it. Mutual exclusion falls out automatically because only one value can be in the tag at a time.
How do you configure the client tag, the binding, and the click script?
- In the Vision Client Tags section of the Designer, create a memory tag named
btnColor. It must hold the selection key, so give it the String data type when the key is the button text. Leave the initial value empty so no instance is highlighted at startup. - Open the template that contains the Button. Select the Button and bind its Background Color property with an expression. The tested expression, for a template named
Trend_btnwith a component namedButton, is:
The pathif ({[client]btnColor} = {Trend_btn.Button.text}, color(71,255,71), color(138,255,255))Trend_btn.Button.textis the template root name followed by the component name; adjust both to match your template.color(71,255,71)is the active green,color(138,255,255)the idle cyan; swap in your own values. - On the same Button, add an
actionPerformedevent script that writes the instance's own key into the client tag. UseactionPerformedrather thanmouseClickedso keyboard activation also updates the selection:
Both arguments are lists. Writing bare strings, as in the shorthandsystem.tag.writeBlocking(["[client]btnColor"], [event.source.text])system.tag.writeBlocking("[client]btnColor", event.source.text), is the pattern that was reported working; wrap them in lists to match the documented signature. - Remove every direct assignment to
buttonBGfrom all scripts on the Button. A scripted write and an expression binding on the same property fight each other; whichever evaluates last wins, and the binding re-evaluates on every tag change, so the scripted color appears to "stick" or "flicker" depending on timing. - Save the template. The Template Repeater picks up the change on its next refresh; no changes to the repeater itself are required.
Which selection key should the expression compare against?
The tested key is event.source.text. That is fine as long as every repeated instance has distinct button text. If two instances can carry the same caption, both will match the tag and both will highlight. In that case add a template parameter such as an index or an item name, have the repeater's dataset populate it, write that parameter into the tag on click, and compare against the parameter in the expression instead of the text. The mechanism is identical; only the key changes.
For the case where the group is small, fixed, and exactly one option must always be active, skip custom logic entirely and use the Vision Multi-State Button component. It implements the radio-group behavior natively: one control value, N states, each state with its own colors, and the component handles the exclusivity. It is the right choice when the buttons are peers on one window rather than rows produced by a repeater.
What does each stage of the chain look like when it is wrong?
| Signal | Source | Wrong-value symptom |
|---|---|---|
Selection key (text or template parameter) |
Template instance | Two instances share the same key: both turn active on one click |
[client]btnColor |
actionPerformed write |
Never written: every instance stays idle; written with the wrong key: nothing matches, no highlight |
| Background Color expression | Binding on the Button | Path wrong (template root or component name): binding error in Designer, color reverts to default |
Scripted buttonBG
|
Leftover assignment in a script | Color sticks after a different instance is clicked, or flickers between two colors |
| Repeater refresh | Dataset or template-parameter change | Instances rebuilt; scripted colors vanish, bound colors survive because the tag still holds the selection |
How do you verify the fix before handing it to operations?
- Open the Tag Browser in the Designer, expand Client Tags, and watch
btnColorwhile you click instances in Preview mode. The value must change to the clicked instance's key on every click. If it does not, the script is not firing or is writing the wrong path. - Confirm exactly one instance is green after each click and the previous one has returned to cyan. If two go green, the key is not unique.
- Change the repeater's dataset or a template parameter to force a rebuild. The highlighted instance must stay highlighted, because the color is derived from the tag rather than stamped on the component.
- Launch two separate Vision clients and click different instances in each. Each client must show its own selection independently; client tags are per session, which is the reason this state does not belong in a gateway tag.
- Check the Designer's expression binding editor for the Button's Background Color: it must show no error and must reference the template's root container name, not the window name.
FAQ
What happens if I put the selection in a gateway memory tag instead of a client tag?
Every connected Vision client sees the same highlighted button, and one operator's click changes another operator's screen. Use [client] scope for UI selection state; reserve gateway tags for process values that must be shared.
What happens if I keep a scripted buttonBG write alongside the expression binding?
The binding and the script overwrite each other on every evaluation, so the button either sticks on the scripted color or flickers between the two. Delete all direct buttonBG assignments and let the expression own the property.
What happens if two repeated buttons have the same text?
Both match {[client]btnColor} and both highlight on one click. Add a template parameter fed by the repeater dataset, write that parameter in actionPerformed, and compare against it in the expression instead of text.
When should I stop and contact Inductive Automation support?
Stop when the client tag updates correctly in the Tag Browser but the bound Background Color does not re-evaluate, or when the Template Repeater refuses to refresh templates after a save on your specific Ignition version. At that point the chain is proven and the problem is platform behavior; open a ticket with Inductive Automation support through their official support portal, attaching the template export, the expression, and the gateway and client logs from the failed refresh.