Perspective Table CSV Export: Fix saveFile and Scripts

Brian Holt9 min read
B&R AutomationOther TopicTechnical Reference
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

A Perspective button that is supposed to hand the operator a CSV of the table usually fails for one of three reasons: the script calls a Vision-only file function, the script references event.source which does not exist in a Perspective event, or the component path from the button to the table is wrong. A fourth failure appears later, after someone adds cell colouring: the table's props.data is no longer a plain dataset and system.dataset.toCSV chokes on it. Work the checks below in order. Each one names what to read and where to go next.

Check 1: Read the Scope of Every system.* Function You Call

The first script most people copy from the Vision-era CSV example looks like this:

component = event.source.parent.getChild("StopsData").getChild("Table")
csv = system.dataset.toCSV(component.props.data)
system.file.saveFile("myExport.csv", "csv", "Comma Separated Values")
if filePath:
    system.file.writeFile(filePath, csv)

It fails on two lines before it ever writes anything. system.file.saveFile opens a Swing file chooser on a Vision client's JVM. A Perspective session has no client JVM; the session runs on the Gateway and the operator only has a browser. The manual page for every scripting function lists its scope at the top, and system.file.saveFile is listed as Vision client only. system.file.writeFile in a Perspective script writes to the Gateway server's filesystem, not to the operator's PC, so even a working filePath would land the file on the wrong machine.

Reading to take: open the manual page for each system.* call in your script and confirm the scope line includes Perspective. If any function is Vision-only, replace it before touching anything else. For file delivery to the browser, the replacement is system.perspective.download, which pushes bytes or a string to the session and lets the browser handle it as a normal web download. Reference pages that are still useful for the dataset side: Exporting and Importing a CSV and system.dataset.exportCSV; note that exportCSV is itself a Vision function and only toCSV carries over.

Outcome: script still errors after replacing the file call → go to Check 2. Script runs but no file appears → go to Check 4.

Check 2: Replace event.source with self

The error object has no attribute source means the script is still using Vision's object model. In Vision, an action script receives event and the component that fired it is event.source. In a Perspective component event script, the firing component is self; the event object carries only event-specific data and has no source attribute. Every event.source in a copied Vision snippet must become self.

Reading to take: the exact error text in the Gateway logs or the Designer's script console output. has no attribute source is this fault and nothing else.

Outcome: error changes to getChild returning nothing or a NoneType attribute error → go to Check 3. Error clears → go to Check 4.

Check 3: Walk the Component Tree from the Button, Not from the View

getChild("Name") only resolves direct children of the container you call it on. The path has to be built from where the button actually sits. Two common layouts:

Layout Button location Table location Working path
Button and table share the same container Root container Root > StopsData > Table self.parent.getChild("StopsData").getChild("Table")
Header/body split Root > HeaderContainer > Button Root > BodyContainer > Table self.parent.parent.getChild("BodyContainer").getChild("Table")

In the header/body case, self.parent is HeaderContainer. HeaderContainer has no child named BodyContainer, so the lookup returns nothing and the next .getChild or .props throws. Go up one more level with a second .parent to reach the root, then descend into BodyContainer. Component names in the path must match the Designer's Project Browser exactly, including case.

The minimum working script for the shared-container layout:

component = self.parent.getChild("StopsData").getChild("Table").props.data
csv = system.dataset.toCSV(component)
system.perspective.download("myExport.csv", csv)

Outcome: file downloads → go to Check 5 to decide whether the browser behaviour is acceptable. toCSV raises a type error → go to Check 6.

Check 4: Confirm Where the Browser Put the File

system.perspective.download does not open a save dialog by itself. It sends the file to the browser, and the browser applies its own download policy. On most browsers the default is a silent save to the user's Downloads folder, which looks like "nothing happened" if you are watching the screen for a prompt.

Reading to take: the browser's download list (the downloads icon or history) immediately after clicking the button. If the file is there, the script works and the remaining question is delivery policy, not code.

Three ways to get a save-location prompt, none of them scriptable from Ignition:

  • Set the browser's Options/Preferences/Settings to "ask where to save each file". This is a per-browser, per-user setting and is exactly how downloads from any other website are handled.
  • Run the client in Perspective Workstation instead of an OS browser. Workstation prompts the user for a save location on every download without any browser configuration.
  • Leave the default. For most plant users a file in Downloads is the accepted behaviour and requires no changes to the client PC.

Do not try to reach the operator's filesystem from the Gateway or via browser automation tooling. Perspective is designed so the client machine is off-limits to the project; the operator has a view into the process through a browser and nothing more. Attempts to force a folder run into OS permission and admin-rights problems that the download model exists to avoid.

Outcome: file is in Downloads and that is acceptable → go to Check 6 only if you plan to colour cells. Otherwise the export is done; proceed to the procedure at the end.

Check 5: Give the File a Useful Name

Once the download works, a fixed name like myExport.csv gets overwritten or suffixed by the browser on every click. Build the name from a timestamp so each export is distinct and sortable:

today = system.date.now()
fileSuffix = system.date.format(today, "yyyyMMddHHmm")
component = self.parent.getChild("StopsData").getChild("Table").props.data
csv = system.dataset.toCSV(component)
system.perspective.download("Stops" + fileSuffix + ".csv", csv)

The format yyyyMMddHHmm resolves to the minute. Extend the prefix with the current dropdown selection (machine, line, shift) by reading that component's value the same way you read the table, and concatenate it before the suffix. Keep the name free of path separators and spaces; the browser decides the folder, not the string.

Check 6: Keep an Unspoiled Copy of the Data Before You Colour Cells

Cell colouring in a Perspective table is done by transforming the data so the cell in the target column becomes a dictionary containing a value and a style block, rather than a bare value. The moment you do this on props.data directly, the table's data is no longer a flat dataset. system.dataset.toCSV expects a dataset, so an export script pointed at the same props.data fails or produces garbage once colouring is added.

Symptom Cause Fix
object has no attribute source Vision event.source used in a Perspective script Use self
NoneType error on getChild chain Path does not match the component tree Add or remove .parent hops; match names exactly
Script errors on system.file.saveFile Function is Vision client scope only Use system.perspective.download
Button runs, nothing visible Browser saved silently to Downloads Check browser download list; set browser to ask, or use Workstation
toCSV type error after adding colours props.data is now a styled JSON structure, not a dataset Export from a raw custom property instead

The pattern that avoids the conflict: put the query or tag binding on a custom property of the view (a dataset or JSON document), bind the table's props.data to that custom property with a transform that injects the style dictionaries, and point the export script at the custom property. The table shows colours; the export reads clean data.

# Export from the raw view custom property, not from the styled table
raw = self.view.custom.stopsData
csv = system.dataset.toCSV(raw)
system.perspective.download("Stops" + system.date.format(system.date.now(), "yyyyMMddHHmm") + ".csv", csv)

The custom property name stopsData is an example; use whatever you named it. Where to put the colouring code: on the table's props.data binding, as a script transform that iterates rows and replaces the target column's value with {"value": v, "style": {...}} based on your condition. Not in a component event, not on the button.

On the bottom horizontal scrollbar: it appears when the sum of column widths exceeds the table's rendered width. Reduce or fix column widths in the table's column configuration in the Property Editor, or let columns size to the container, and the scrollbar goes away without CSS. Treat CSS overrides on the table's internal scroll region as a last resort; they are theme-dependent and break on upgrades.

Procedure: Build the Export Button

  1. Add a custom property to the view (for example stopsData) and move the table's query or tag binding onto it.
  2. Bind the table's props.data to view.custom.stopsData. Add a script transform only if you need cell styling.
  3. Add a Button component. Open its onActionPerformed event and select Script.
  4. Enter the export script reading from self.view.custom.stopsData, building the filename with system.date.format(..., "yyyyMMddHHmm"), and calling system.perspective.download.
  5. Save the project and launch the view in a Perspective session, not the Designer preview alone.

Verify

  1. Click the button. Open the browser's download list and confirm a file named Stops<yyyyMMddHHmm>.csv appears.
  2. Open the file. Confirm the first row is the column headers and the row count matches the table.
  3. Click again within the same minute and confirm the browser suffixes or overwrites as expected; click after a minute boundary and confirm a new name.
  4. If cell colours are configured, confirm the CSV contains plain values in the coloured column, not dictionary text.
  5. If a save prompt is required, confirm it appears with the browser set to ask, or in Perspective Workstation.

Stop here if the download works in one browser but not another, or if the corporate browser policy blocks downloads or forces a folder you cannot change: that is an IT group policy issue, not a project issue. Stop and contact Inductive Automation support through the official support portal if system.perspective.download raises an exception with a correctly typed string argument in a supported browser, or if the table's transform and export behave differently between the Designer and a live session on the same Gateway version.

FAQ

How do I export a Perspective table to CSV when system.file.saveFile gives an error?

system.file.saveFile is Vision client scope only. In Perspective use csv = system.dataset.toCSV(self.parent.getChild("StopsData").getChild("Table").props.data) followed by system.perspective.download("myExport.csv", csv); the browser saves it to its downloads location.

How do I fix "object has no attribute source" in a Perspective button script?

Replace event.source with self. Perspective event scripts expose the firing component as self, and if the button and table are in different containers add another .parent hop, for example self.parent.parent.getChild("BodyContainer").getChild("Table").

How do I make Perspective ask where to save the downloaded CSV?

You cannot do it from the script. Set the browser's download preference to "ask where to save each file", or run the client in Perspective Workstation, which prompts for a save location automatically.

Back to blog