Perspective closePopup Fails When 'is not' Aborts the Handler

Daniel Price9 min read
HMI / SCADAOther ManufacturerTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

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:

  1. The first CSV row that does not already exist is inserted. This is the sproc execution you can see in the database.
  2. res comes back as a success string, and str(res) is not "Success" evaluates true.
  3. ValueError is raised with a message ending in Result: Success.
  4. Control jumps to except, which tries to open ErrorModal and logs a warning.
  5. sendMessage('refreshTree'), sendMessage('refreshStatesTable') and closePopup("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 wrote self.view.params.ID. If those differ, the check passes and the insert duplicates. The version above uses the view's ID for both. If the CSV LineID is authoritative, use it for both instead. Never mix the two sources.
  • Per-row toast. The per-row gpa.frontEnd.localUI.throwSuccess calls are removed from the loop. Any exception inside that helper would also abort the loop before closePopup. 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:

  1. Dataset clear. system.dataset.clearDataset() returns a new, empty dataset and does not modify its argument. The original line discards the result, so csvDict is never cleared. Assign it back: fu.custom.csvDict = system.dataset.clearDataset(fu.custom.csvDict), where fu = self.parent.parent.getChild('FileUpload').
  2. Type mapping. type_mapping[str(self.view.params.Type)] raises KeyError on an unmapped type, so the if messageType: guard never runs. Use type_mapping.get(str(self.view.params.Type)) and log when it returns None.
  3. 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?

  1. Save the project, then close and reopen the Perspective session so it loads the saved revision.
  2. Open the gateway log viewer and filter on insertStates_message and importFile.
  3. Import a CSV with three new rows. Confirm importStates received on ... appears once. If it appears twice, a second handler exists on the page.
  4. Confirm the database gained three rows, not one, all with the intended LineID.
  5. Confirm the log shows Inserted 3 rows, closing ImportFile, the tree and states table refresh, and the popup closes.
  6. Re-import the same CSV. Expect three already-exists warnings, zero inserts, and the popup closing.
  7. Force a failure, for example by passing an invalid StateTypeID. Confirm the popup stays open, ErrorModal shows the procedure's actual result, and the gateway log carries a full stack trace.
  8. Reopen ImportFile and confirm the FileUpload custom properties configCSV and csvDict are 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.

Back to blog