Which hops does the import message cross?
Start with the data path. A button click in the ImportFile popup, a message handler on a component in the parent view, a project-library stored procedure call, and a close instruction back to the browser page are all involved. Every one of those scripts executes on the Ignition gateway inside the Perspective session. The browser only renders the result.
| Hop | Script / call | Runs on | Where its output lands |
|---|---|---|---|
| 1 | Parent view button: system.perspective.openPopup("ImportFile", "GlobalComponents/Modals/ImportFile", params) with Type = 'states'
|
Gateway (session) | Popup instance with id ImportFile on the current page |
| 2 | Popup Import button onClick: maps Type to importStates, calls system.perspective.sendMessage(messageType, payload)
|
Gateway (session) | Default scope is page. Every handler on that page listening for importStates at page scope receives it. |
| 3 |
onMessageReceived handler importStates
|
Gateway (session) | Logger output goes to the gateway log. system.perspective.print goes to the browser console, or to the Designer output console in preview. |
| 4 | mes.config.sp.addEditLineState(params) |
Gateway to database | Row inserted. Return value comes back into the handler. |
| 5 | system.perspective.closePopup("ImportFile") |
Gateway, then pushed to page | Popup removed from the page |
A row in the database proves hop 4 ran. It does not prove the handler reached hop 5. The packet stops between 4 and 5.
Where does the handler stop before closePopup?
The handler stops on this line, which runs inside the for loop right after the sproc call:
if str(res) is not "Success":
raise ValueError('Sproc call failed for row {}. Result: {}'.format(row, str(res)))
is and is not test object identity, not string equality. The string returned by the sproc wrapper, or produced by str(res), is a different object from the literal "Success". The identity test therefore evaluates true even when the procedure succeeded. The sequence is deterministic:
- The first CSV row that does not already exist is inserted. This is the sproc execution you can see in the database.
-
rescomes back as a success string, andstr(res) is not "Success"evaluates true. -
ValueErroris raised with a message ending inResult: Success. - Control jumps to
except, which tries to openErrorModaland logs a warning. -
sendMessage('refreshTree'),sendMessage('refreshStatesTable')andclosePopup("ImportFile")never execute. Every remaining CSV row is skipped.
This fault leaves two signatures. First, exactly one new row is inserted per import regardless of CSV length. Second, if ErrorModal opens, it reports a failure whose result text is Success. There is also a quick confirmation test. Import a CSV whose rows all already exist. Every row hits continue, no comparison runs, and the popup closes. If the popup closes on the all-duplicates file and stays open on a file with new rows, the comparison is the cause.
The fix is !=. Normalize the return value too, because a stored procedure wrapper can return a Java string, a unicode string, or a value with trailing whitespace:
if str(res).strip() != 'Success':
Which other faults leave the popup open?
Several other faults leave the popup open with the sproc still firing. Rule each one in or out with a specific check:
| Symptom | Cause | Check |
|---|---|---|
One row inserted, popup open, error text says Result: Success
|
is not identity comparison raises after the first insert |
Count rows inserted versus rows in the CSV. Run the all-duplicates test. |
Popup open, no ErrorModal, gateway log shows an error attributed to the component script rather than your logger name |
A Java exception, such as a JDBC error from the sproc, is not caught by except Exception in Jython and escapes the handler |
Search the gateway log for the view path and onMessageReceived. Add an except java.lang.Throwable branch. |
Rows inserted but none of your logger.info lines appear |
Handler that executed is not the one you edited: unsaved project, or a second view on the page with an importStates handler |
Save the project. Search the project for importStates. Log self.view and self inside the handler. |
| Refresh appears to happen even though the handler aborted | Tree or table refresh is driven by a binding poll or another event, not by refreshTree / refreshStatesTable
|
Put a logger line in each refresh handler. |
ErrorModal never appears after an exception |
Wrong view path or popup id collision, so the error path fails silently | Open GlobalComponents/Modals/ErrorModal from a test button. |
| Next import re-sends the previous file's rows |
system.dataset.clearDataset() result discarded in the popup |
See the popup-side fixes below. |
Why don't the logger lines show up?
Perspective loggers do not write to a browser or Designer console. The scripts run on the gateway, so system.util.getLogger('insertStates_message') writes to the gateway log. Read it in the gateway web interface log viewer, under Status > Diagnostics > Logs, and filter on the logger name. system.perspective.print is the only call here whose output reaches the browser console or Designer output.
If the gateway log contains none of the handler's logger lines, including the unconditional running the message handler, while rows still appear in the database, the edited code is not the code that ran. Save the project, confirm the session is running against the saved revision (close and reopen the session), and search for every importStates handler on the page. Only after the log shows running the message handler is the handler you are editing the one that ran.
Fix the logging calls at the same time. logger.warn('There was an error: ' + str(e)) reduces the exception to its message and discards the stack trace. Pass the exception as a separate argument, as in logger.warn('There was an error', e), for Java throwables. For Python exceptions, append traceback.format_exc() so the log shows the failing line.
Where should the popup close: handler or button?
You can place the closePopup call in three places. They differ in whether the close depends on the insert actually succeeding:
| Approach | Close gated on sproc success | Failure visibility | Coupling |
|---|---|---|---|
| A. Handler closes the popup after the loop completes | Yes | Popup stays open. ErrorModal shows the reason. |
Handler needs the popup id ImportFile
|
B. Popup button closes itself right after sendMessage
|
No. It closes before the handler finishes. | User loses the upload context on failure. | Low |
| C. Handler sends a result message back and the popup closes itself | Yes | Popup can show row-level status | Two handlers and an extra message type |
Use approach A. It is the existing design, it already gates the close on success, and it only fails because of the comparison bug. The page-scoped popup id resolves correctly from a component handler on the same page, so closePopup("ImportFile") needs no sessionId or pageId arguments. Move to approach C if operators need per-row feedback inside the popup.
What does the corrected handler look like?
The corrected handler keeps the insert loop and closes only after every row succeeds. It catches Java and Python exceptions separately and logs at each hop so the gateway log shows exactly where execution stopped.
def onMessageReceived(self, payload):
import java.lang
import traceback
logger = system.util.getLogger('insertStates_message')
logger.info('importStates received on ' + str(self.view))
def showError(msg):
system.perspective.openPopup('ErrorModal', 'GlobalComponents/Modals/ErrorModal',
{'ModalName': 'ErrorModal', 'ErrorMessage': msg})
try:
pyData = system.dataset.toPyDataSet(payload.get('csvDS'))
existingData = system.dataset.toPyDataSet(self.view.custom.Data)
lineId = self.view.params.ID
inserted = 0
for row in pyData:
if any(r['ReasonCode'] == row['ReasonCode'] and r['LineID'] == lineId for r in existingData):
logger.warn('ReasonCode {} already exists on LineID {}'.format(row['ReasonCode'], lineId))
continue
params = {
'AltCode': row['AltCode'],
'OperatorSelectable': row['OperatorSelectable'],
'SubReasonOf': row['SubReasonOf'],
'StateID': 0,
'LineID': lineId,
'Description': row['Description'],
'DowntimeCategoryCode': row['DowntimeCategoryCode'],
'DisplayColor': row['DisplayColor'],
'PlannedDowntime': row['PlannedDowntime'],
'ReasonCode': row['ReasonCode'],
'RecordDowntime': row['RecordDowntime'],
'IconPath': row['IconPath'],
'StateTypeID': row['StateTypeID']
}
res = mes.config.sp.addEditLineState(params)
if str(res).strip() != 'Success':
raise ValueError('Sproc failed for ReasonCode {}: {}'.format(row['ReasonCode'], res))
inserted += 1
logger.info('Inserted {} rows, closing ImportFile'.format(inserted))
system.perspective.sendMessage('refreshTree')
system.perspective.sendMessage('refreshStatesTable')
system.perspective.closePopup('ImportFile')
except java.lang.Throwable as t:
logger.error('importStates Java exception', t)
showError(str(t))
except Exception as e:
logger.warn('importStates failed: ' + traceback.format_exc())
showError(str(e))
Three decisions are embedded in this code:
-
LineID source. The original duplicate check compared
row["LineID"]from the CSV, but the insert wroteself.view.params.ID. If those differ, the check passes and the insert duplicates. The version above uses the view'sIDfor both. If the CSVLineIDis authoritative, use it for both instead. Never mix the two sources. -
Per-row toast. The per-row
gpa.frontEnd.localUI.throwSuccesscalls are removed from the loop. Any exception inside that helper would also abort the loop beforeclosePopup. Report one summary after the loop if needed. - Partial imports. Rows inserted before a failure stay committed. If a partial import is unacceptable, move the loop into a single transaction in the project library.
What needs fixing on the popup side?
The popup's onClick script has three defects:
-
Dataset clear.
system.dataset.clearDataset()returns a new, empty dataset and does not modify its argument. The original line discards the result, socsvDictis never cleared. Assign it back:fu.custom.csvDict = system.dataset.clearDataset(fu.custom.csvDict), wherefu = self.parent.parent.getChild('FileUpload'). -
Type mapping.
type_mapping[str(self.view.params.Type)]raisesKeyErroron an unmapped type, so theif messageType:guard never runs. Usetype_mapping.get(str(self.view.params.Type))and log when it returnsNone. -
Logging. Replace
logger.warn('There was an error: ' + str(e))with a call that preserves the stack trace, as in the handler.
Clearing the upload properties right after sendMessage is safe. The payload already holds the CSV string and dataset reference, and Ignition datasets are immutable, so the handler keeps its copy.
How do you verify the fix?
- Save the project, then close and reopen the Perspective session so it loads the saved revision.
- Open the gateway log viewer and filter on
insertStates_messageandimportFile. - Import a CSV with three new rows. Confirm
importStates received on ...appears once. If it appears twice, a second handler exists on the page. - Confirm the database gained three rows, not one, all with the intended
LineID. - Confirm the log shows
Inserted 3 rows, closing ImportFile, the tree and states table refresh, and the popup closes. - Re-import the same CSV. Expect three already-exists warnings, zero inserts, and the popup closing.
- Force a failure, for example by passing an invalid
StateTypeID. Confirm the popup stays open,ErrorModalshows the procedure's actual result, and the gateway log carries a full stack trace. - Reopen
ImportFileand confirm theFileUploadcustom propertiesconfigCSVandcsvDictare empty before selecting a new file.
FAQ
What happens if I compare strings with is not in Ignition scripting?
is not tests object identity, so a returned 'Success' string is usually a different object from the literal and the test evaluates true. Any code gated on that test, such as a raise, runs even on success. Use != and normalize with str(res).strip().
What happens if a Java exception is thrown inside a Perspective message handler?
In Jython, except Exception does not catch Java throwables such as JDBC errors, so the handler exits and every later line, including closePopup, is skipped. Add an except java.lang.Throwable as t branch and log with logger.error('msg', t) to capture the stack trace in the gateway log.
What happens if closePopup is called from a different view than the popup?
A component script on the same page closes the popup by its id alone, for example system.perspective.closePopup('ImportFile'), because the session and page come from the script's context. From a gateway-scoped script with no page context, pass the sessionId and pageId explicitly.