Reading time: 9 minutes and 53 seconds.

FAQ – Action mods

Find answers to common questions related to action mods.

What are capabilities?

The action mods API is designed with security in mind to allow a robust access control transparent for the end user. Using the default settings of the manifest file, the API will not reach outside the analysis. For example, a mod that needs to query the library, or save an analysis to a new location, must request this via the capability property of the manifest. There are two extended capabilites available, “LibraryRead” and “LibraryWrite”. When specified, the mod gets full access to the API, including library read and write capabilites, such as Search and SaveAs.

The user will be informed about the use of extended capabilities when asked to trust a mod. It is not possible to opt out on specific capabilites, meaning that a mod developer will not need to design their scripts for the possibility that certain parts of the API is unavailable due to the user not allowing access to these capabilities.

Why does my script run inside a transaction?

Using the default settings for a script in an action mod, the script will be executed in a transaction. If the script executed successfully, all modifications will be captured in a single undo/redo step, and any unhandled errors will cause the entire transaction to be rolled back.

There are however some API methods that, for various reasons, are not allowed to run inside a transaction. These methods are tagged with a remark stating that the script needs to be marked with wrapInTransaction set to false, like Save and ExecuteInvisibleTransaction.

If such a script makes multiple modifications to the document, these modifications can be wrapped in a single undo/redo step using ExecuteTransaction:

document.Transactions.ExecuteTransaction(new System.Action(() => {
    document.ActivePageReference!.Title = "New page title";
    document.ActiveVisualReference!.Title = "New visual title";
}))

Why is something ‘possibly null’?

There are many methods and properties in the API that are marked as “can return null”, like ActivePageReference returning the union type null | Page. This means that there are situations when the value can be null, and the mod developer must handle this. Failing to do so can cause runtime errors for users under certain circumstances that the mod developer did not forsee. For example, an analysis that contains no pages will return null as ActivePageRerence.

The template project created by the Mods SDK is created with the TypeScript strict flag enabled. The strict flag enables a wide range of type checking behaviors. This results in stronger guarantees of program correctness. One of the most noticiable effects is that this enables strictNullChecks which will enforce null checks on the nullable API methods and properties.

Depending on the situation, use TypeScript non-null or JavaScript optional chaining, rather than turning off the strict flag.

// Yields a compiler error, and potenitally a runtime error, as ActivePageReference can be null.
const tableName = document.ActivePageReference.Title;

// Use non-null type guard. Use only when the value never can be null, as this may still cause a runtime error if the assertion is incorrect.
const tableNameNeverNull = document.ActivePageReference!.Title;

// Preferred way. Use optional chaining to defer the handling of no active page.
const canBeUndefined = document.ActivePageReference?.Title;

Why can’t I use arrays as arguments to Spotfire APIs?

C# arrays are always strongly typed whereas regular JavaScript arrays are untyped, and for API with arrays as argument this can easily create ambiguity. When using such APIs, a strongly typed array can be created using TypedArray.create.

This snippet of code uses GetBits to fetch all currently filtered rows in the active data table in an effective way.

const indexSet = document.ActiveFilteringSelectionReference!.GetSelection(document.ActiveDataTableReference!).AsIndexSet()!;
const bits = TypedArray.create(System.UInt32, new Uint32Array((indexSet.Capacity + 31) / 32));
indexSet.GetBits(bits);

for (const bit of bits) {
    console.log(bit);
}

How do I index an element of a managed object?

Managed objects sometimes expose “indexers” which are accessed using special syntax in C#. To access these in a script, you need to use the Item property of the managed object which exposes one or both of PropertyGet and PropertySet.

For example, if you have a value of type System.Collections.Generic.Dictionary<string, string> called dict you can get and set its keyed values via:

const value = dict.Item.get("key");
dict.Item.set("key", "newValue");

Most collection APIs also implement the “iterator protocol”. This means that you can loop through the collection’s items via for...of loops or create arrays from the collection via Array.from.

💡 See snippet Set up a page layout and rotate the visualizations on consecutive runs for an example where the visualizations on a page are converted to an array and then indexed via position.

How do I cast a value?

Sometimes you might have to cast a managed object from one type to another to access a different API surface. This can be done via the Cast and TryCast methods, which are defined for all managed objects. The only difference between the methods is that Cast throws if the cast is invalid while TryCast returns null.

The main scenarios where casting is relevant are:

  • Casting to a more specific type. Some APIs return a super type that you need to cast to a more specific type to access some APIs. An example is the VisualizationData.Filterings API which returns a collection of DataSelections, which are either a DataFilteringSelection or a DataMarkingSelection.

    for (const selection of visualizationData.Filterings) {
        const marking = selection.TryCast(Spotfire.Dxp.Data.DataMarkingSelection);
        if (marking) {
            // We can access the color property here.
            const color = marking.Color;
        }
    }
    

    💡 See snippet Remove unreferenced filtering schemes which uses casting to build an array of filtering shemes.

  • Casting to an explicitly implemented interface. This scenario should be rare but it is nonetheless supported by the action mod type system.

Casting is only relevent for managed objects, i.e. objects returned by APIs found in Spotfire or System. A somewhat similar concept occurs in TypeScript called “narrowing”. Unlike casting however, narrowing does not produce any effect, it is simply a verification step. This is relevant for APIs which return untyped objects that you need to manually narrow yourself, e.g. DataProperty.Value and DataValue.ValidValue.

💡 See snippet Set up a page layout and rotate the visualizations on consecutive runs" which uses typeof to narrow the value of a document property to number.

How do I use APIs with output parameters?

C# has a concept of out parameter modifiers which signals that the parameter will be assigned when the method is called. This concept is often used for methods which may fail, where instead of returning a possibly null value or throwing the method instead returns a false value and sets the out parameter to null. Such methods often have their name prefixed with “Try”, e.g. TryGetPage. To call such a method from TypeScript you must initialize an OutParam with the appropriate target type and pass that object’s out field to the method you want to call.

For instance, if you want to call the “TryGetPage” method it would look something like this:

const page = OutParam.create(Spotfire.Dxp.Application.Page);
if (document.Pages.TryGetPage(new System.Guid("<page GUID>"), page.out))
{
    page.Title = "New Title";
}

💡 See snippet Replace a data table with an SBDF file from the library for a more detailed example.

How do I use APIs which expect callback arguments?

To call an API which expects a callback as an argument you need to construct a managed callback object. This is necessary since .NET expects callbacks to be strongly typed. Some examples of managed callbacks are:

To construct a managed callback, you call its constructor providing the necessary type references (if any are required) and the JavaScript function. For example DataColumnCollection.FindAll expects a callback of type System.Predicate<DataColumn>, to create this predicate you write:

const predicate = new System.Predicate(Spotfire.Dxp.Data.DataColumn, dataColumn => ...);

💡 See snippet Find all columns that match a predicate for a more detailed example.

Why is my favorite API missing?

The action mods API is a subset of the Spotfire .NET API, alongside some types from the .NET standard library that are required for the Spotfire API to work. Some APIs have been omitted from the action mods API for different reasons:

  • For security reasons, file I/O is excluded.
  • For security reasons, reflection APIs are excluded.
  • For SSRF reasons, HTTP APIs are excluded.
  • There is no way to create and install new types in the action mods API, thus extension APIs are excluded.
  • Action mods run on the application thread and need to support cancellation, thus threading APIs are excluded.
  • Creating event handlers via the event manager would make the lifetime of a script unpredictable and is therefore not possible.
  • Scripting APIs (creating and running IronPython/JavaScript etc. scripts) have a different security model that could make it possible to escape the sandboxed environment that the action mod is executed in, thus scripting APIs are excluded.
  • Action mod scripts are executed in a pure JavaScript environment, therefore browser APIs are excluded. For example, setTimeout and localStorage cannot be used, even if the API does not directly require a visible browser window.

If you find that a specific API is missing, please visit the Spotfire Ideas Portal and add, or vote for an existing, idea.

How do I port my IronPython script to an action mod?

Spotfire has long supported scripting similar to action mods via IronPython scripts. However, action mods have numerous advantages which make them more suitable for scripting than IronPython:

  • Action mods run in a sandboxed environment, which means they only have access to APIs that are secure. For example, a script in an action mod cannot read files on disk or make HTTP requests.
  • Scripts in action mods can easily be maintained and updated across all analyses if the mod is stored in the library.
  • Action mods can be developed externally in a modern development environment with support for type checking, build tools, and version control.
  • When developing using the installed client, action mods can be run with a step-by-step debugger for easy debugging.
  • Scripts in action mods can be used on action triggers in visualizations, and be configured to prompt for parameters in runtime.
  • For easy access, action mods can be pinned to the Actions flyout, either by users or by an administrator.

However, not all IronPython scripts can be ported to an action mod, mainly due to missing APIs which have been removed due to ergonomic or security reasons. If there is a use-case which cannot be supported by an action mod script, then please open an issue on the GitHub page or post an idea in the Spotfire Ideas Portal.

To port an IronPython script to TypeScript you should be aware of the following differences:

  • Document and Application are passed as parameters to the entry point function of the script as opposed to being available in the global scope.
  • Type parameters are passed as the initial parameters to a function, as opposed to using the “bracket style” of providing type parameters.
  • Indexing is done through the Item.get and Item.set methods of an object (see “How do I index an element of a managed object?” for more info).

Below is an example of a one-to-one conversion of an IronPython script to an action mod script.

Note:

  • the extra lines are due to declaring and registering the entry point function as well as the convention of closing block scopes using curly brackets on new lines.
  • the usage of the “Non-null Assertion Operator” (the ! operator) on the line declaring the barchart variable. Each usage of the ! operator is an assertion that you know something will not be null, and should in general be avoided, since it can lead to errors when the script is executed in an unexpected context. In IronPython scripts it is very hard to know when these errors can occur, but in action mod scripts it is clear. For more info see “Why is something ‘possibly null’?”.
  • the generic function As takes the type parameter BarChart as its first argument.
  • the code which actually performs the changes on the document must be encapsulated in a function passed to RegisterEntryPoint.

💡 See snippet Set the colors of a bar chart for a parameterized version of this script including error handling.

IronPython

from Spotfire.Dxp.Application.Visuals import BarChart, CategoryKey
from System.Drawing import Color

# Get a reference to a visual on the page specified
def getVisual(page, visualTitle):
    for vis in page.Visuals:
        if vis.Title == visualTitle:
            return vis
    return None

# Get the bar chart
barchart = getVisual(Document.ActivePageReference, "My Bar Chart").As[BarChart]()

# Clear the color axis coloring
barchart.ColorAxis.Coloring.Clear()
barchart.ColorAxis.Expression = "[fruit]"

red = Color.FromArgb(255, 0, 0)
yellow = Color.FromArgb(255, 255, 0)
barchart.ColorAxis.Coloring.SetColorForCategory(CategoryKey("Apple"), red)
barchart.ColorAxis.Coloring.SetColorForCategory(CategoryKey("Banana"), yellow)

TypeScript

const { BarChart, CategoryKey } = Spotfire.Dxp.Application.Visuals;
const { Color } = System.Drawing;

/** Get a reference to a visual on the page specified */
function getVisual(page: Spotfire.Dxp.Application.Page, visualTitle: string) {
    for (const vis of page.Visuals) {
        if (vis.Title === visualTitle) {
            return vis;
        }
    }
    return null;
}

export function barChartColoring({ document, application }: BarChartColoringParameters) {
    // Get the bar chart
    const barchart = getVisual(document.ActivePageReference!, "My Bar Chart")!.As(BarChart)!;

    // Clear the color axis coloring
    barchart.ColorAxis.Coloring.Clear();
    barchart.ColorAxis.Expression = "[fruit]";

    const red = Color.FromArgb(255, 0, 0);
    const yellow = Color.FromArgb(255, 255, 0);
    barchart.ColorAxis.Coloring.SetColorForCategory(new CategoryKey("Apple"), red);
    barchart.ColorAxis.Coloring.SetColorForCategory(new CategoryKey("Banana"), yellow);
}

RegisterEntryPoint(barChartColoring);
Last modified June 5, 2024