A Perspective table that stops responding after repeated .refreshBinding("props.data") calls is usually a stalled session, and the table script is rarely the cause. The standard fix removes the manual refresh. Bind the tag read and the heavy formatting to a view custom property once. Then bind props.data to that property plus the checkbox values, so a filter change only filters rows that already exist. Before you change anything, open the browser's developer tools and find out whether the browser or the gateway is the side that stops.
How do you read the freeze symptoms?
Look at the pattern first. This installation shows the following:
- The formatting script (
system.tag.readBlocking()plus style and description processing for about 100 rows) completes in roughly 1–2 seconds. - The first few filter changes and Refresh clicks work.
- After several cycles, the Refresh button, cell clicks and cell edits all stop responding.
- Only a new browser tab with a fresh login recovers the session.
- A lockout that blocked Refresh until the script finished made no difference.
That last point matters. If the gateway script were simply being called too often, the lockout would have helped. Because every interaction in the session dies, not just the Refresh button, the fault is in something they all share: the session's event and property-sync path, or the browser's main thread.
Walk the signal chain from the tag to the pixel. The table below shows what each stage should carry and what a wrong value looks like.
| Signal | Source | Wrong-value symptom |
|---|---|---|
| Raw tag values |
system.tag.readBlocking() inside the binding transform |
Each refresh takes longer. Bad-quality values reach the formatting code as None and produce odd statuses. |
| Filter selections | Checkbox props.selected values passed into the script |
The filter works once, then lags one click behind, or rows reappear after refresh. |
| Busy/lock flag | A custom property (for example an isActive flag) set by the Refresh button script |
The flag is set after refreshBinding() is called, so a second click lands inside the open window. |
| Formatted row set | Transform output written to props.data
|
The browser console shows rendering errors. Cells show {} or blank values where a number was expected. |
| Property sync | Session connection between gateway and browser | The Network tab shows the session connection closing or reconnecting repeatedly. The UI stays visible but is dead. |
| Render/event dispatch | Browser main thread | The Performance profile shows long tasks during each refresh, and clicks queue without executing. |
Where in the chain does refreshBinding stall the session?
A Perspective binding and its script transform execute on the gateway. The result is written into the session's property tree and then pushed to the browser, which diffs it and re-renders the component.
Calling refreshBinding("props.data") forces the whole chain to run again:
- A fresh blocking tag read.
- The full formatting pass: the per-country irradiance averaging, the connector evaluation, and the status and colour mapping.
- A complete replacement of
props.data.
With per-cell style objects, 100 rows is not 100 values. It is 100 rows, times each column, times a style dictionary of a dozen keys (background colour, four radii, border styles, widths and colours, margin). Every refresh replaces all of it, even when the filter only removed rows.
Two failure paths follow from this:
- Browser side. Each full replacement makes the table rebuild its rows and styles. If a row contains a value the table cannot render, or the rebuild is slow, the main thread stays busy or throws. Clicks are captured but never dispatched, so every control looks dead at once.
- Gateway side. Component event scripts and binding executions for a session are handled on the gateway. A transform that blocks on I/O (a blocking tag read) or raises partway through can leave later events for that session waiting. The browser then keeps sending clicks that produce no response.
Both paths explain why a new tab recovers. A new tab starts a new session with an empty property tree and an empty event backlog.
Why did the busy-flag lockout not help?
The lockout only protects the gateway from re-entering the script. It does nothing for a browser that is still processing the previous payload, and nothing for a payload that breaks the table.
Order also matters. Set the flag to true before calling refreshBinding(), and clear it from the transform's completion rather than from the button script. If the button sets the flag after the call, or clears it immediately, the lock is open during exactly the window it was meant to cover.
Correct the ordering, but do not expect it to fix the freeze on its own. A lockout does not fix a render path. Removing the need for refreshBinding() does.
How do you restructure the view so filters never call refreshBinding?
Split the work so the expensive part runs on data change and the cheap part runs on filter change.
-
Add a custom property on the view, for example
view.custom.rawRows. -
Bind the tag read and full formatting to
rawRows. Use tag bindings, or a polled binding with a script transform that calls the existing libraryprocess()with no filter. It should return every row with its styles and a plainstatusfield (the status name such as"Status fault"or"Exclusion"). This is the only placesystem.tag.readBlocking()runs. Set its refresh rate to how often the plant data actually changes, not to how often operators click. -
Bind
props.datawith an expression structure binding to two keys:rowspointing at{view.custom.rawRows}, andfiltersholding each checkbox'sselectedvalue under the names the library already uses (powerOffCritical,noCommunication,powerOffConnector,excludedByOperator,noCondition). -
Add a script transform that only filters. Keep the
filterToStatusmapping in the same project library asprocess()so both paths use one definition. -
Delete the Refresh button's
refreshBinding()call. Delete the lock flag too, since nothing re-enters the heavy script on a click any more.
# Script transform on props.data (expression structure binding)
def transform(self, value, quality, timestamp):
rows = value['rows'] or []
filters = value['filters'] or {}
active = [k for k in filters if filters[k]]
if not active:
return rows
allowed = set()
for k in active:
allowed.update(AJ3.filterToStatus.get(k, []))
return [r for r in rows if r.get('status') in allowed]
This version treats a checked box as "show this category". If your checkboxes mean "hide this category", invert the membership test. A filter pass over 100 already-built rows is trivial compared with a blocking tag read plus full formatting.
How do you prove whether the browser or the gateway is blocking?
A frozen UI is a front-end symptom, so start in the browser. Measure before you touch the script.
- Open developer tools on the session tab before reproducing. Keep the Console, Network and Performance tabs ready.
- Record a Performance profile while you change filters and click Refresh until the freeze appears. Long tasks that start at each refresh and grow with every cycle point at render cost or a render loop.
-
Read the Console. A JavaScript error logged at the moment the UI stops means the table choked on a value in
props.data. Note which row was being rendered. - Watch the session connection in the Network tab. A connection that closes, reconnects, or stops carrying messages after a refresh points at payload size or a broken sync, not at the table.
- Check the gateway at the same moment. Look at the gateway logs for script exceptions from the transform, and at the gateway's Perspective session status pages for the stuck session. An exception on the Nth refresh, but not the first, usually means a specific tag value or quality broke the formatting code.
-
Time the transform. Log a timestamp at entry and exit through a
system.util.getLogger()logger, not withprint. If exit times stretch or stop appearing while the browser is healthy, the gateway side is blocking.
Decide from the result. A healthy gateway with a busy or erroring browser means fixing the payload shape. A gateway that stops logging transform exits means removing the blocking read from the click path, which the restructure above already does.
How do you verify the fix holds?
- Toggle every checkbox combination repeatedly, at least as many cycles as it previously took to freeze, then several times more. Cell clicks and edits must keep responding throughout.
- Record a second Performance profile. Filter changes should produce short tasks that do not grow cycle over cycle.
- Confirm in the gateway log that the heavy transform on
rawRowsfires only at its polling rate or on tag change, never on a checkbox click. - Force the ugly cases: a site with lack of communication, a connector with no
State, a bad-quality tag. Confirm no row carries an object where the table expects a number. - Leave the session open through several data refreshes of
rawRowswhile clicking filters. The session must stay live without a new tab or a re-login.
Which script habits keep reintroducing the freeze?
-
Dict defaults on scalar fields. Calls like
LV.get('State', {})return an empty dict when the key is missing. Then{} != 2evaluates true, and the code writes{"value": {}}into a cell that expects a number. Missing data becomes an object inprops.data, which is exactly the kind of value that breaks rendering on one refresh but not another. Default scalars toNoneand handleNoneexplicitly. -
Truthiness tests on missing keys.
not MV.get('Communication', {})treats "key missing" and "communication lost" the same. Decide which one you mean, because they map to different statuses. -
printinside per-row logic. The print for lost farm communication fires once per affected row, on every refresh. Use a logger with a level you can switch off. -
Dead branches. Leftover
if False:/if True:blocks hide which path actually runs. Remove them before profiling so the timing you measure reflects the real code. -
Duplicated style dictionaries. Building the same
styleInverterorstyleEmptydict into every cell inflates the payload. Where the table allows it, apply common styling at column or table level and carry only what varies (the status colour) per cell. -
Reading tags on user action. Any design that calls
system.tag.readBlocking()because an operator clicked something puts I/O latency on the UI path. Operator actions should filter, sort or select data that bindings already hold.
FAQ
Why does my Perspective table stop responding after several refreshBinding calls?
Each refreshBinding("props.data") re-runs the tag read and full formatting, then replaces the whole styled row set. Either the browser main thread stalls or errors on the new payload, or the session's gateway-side event handling blocks behind the transform. Check the browser Console and Performance tabs first, then the gateway logs.
Why does opening a new tab fix a frozen Perspective session?
A new tab starts a new session with a clean property tree and no queued events or broken render state. The problem returns once you repeat the same refresh cycles, which is why the fix is removing the manual refresh path, not reloading.
Why does a busy flag not stop the freeze when using refreshBinding?
The flag only prevents re-entry into the gateway script, and only if it is set before refreshBinding() is called. It cannot help a browser that is still rendering, or failing on, the previous payload. Filtering a cached custom property removes the need for the flag entirely.
When should I escalate a frozen Perspective session to Inductive Automation support?
Escalate when the restructured view (cached custom property plus filter-only transform) still freezes and the browser Console and gateway logs are clean. Also escalate if the session connection drops with no script error on either side. Open a case through the official support channel with the Performance profile, Console output, gateway logs from the same timestamp, and your Ignition version.