Implementing gui_event

The gui_event function serves as a gateway for all events.

The event must be dispatched to other functions that actually provide the intended functionality for the events. In this section the calling syntax, dispatching techniques, and input and output arguments will be discussed, followed by a small example.

Options for triggering events when a component value is changed or loses focus are described in the Advanced features chapter.

Syntax

def gui_event(meta_data: dict, payload: dict) -> dict:
    ...
    return payload
function payload = guiEvent(metaData, payload)
    ...
end

Register events explicitly

The recommended way to ensure the correct function is executed, is to explicitly register the event handler functions to the events in the gui_event function. The event must then be dispatched to execute the registered function.

In the examples below the static method Form.eventHandler is used to register the event "SayHello" to the say_hello/sayHello function. The dispatch function ensures that the function registered to the event is executed. Note that the eventHandler input arguments can be repeated to register multiple events.

def gui_event(meta_data: dict, payload: dict) -> dict:
    Form.eventHandler(SayHello=say_hello)
    callback = utils.getEventFunction(meta_data, payload)
    return callback(meta_data, payload)

def say_hello(meta_data: dict, payload: dict) -> dict:
    utils.setSubmissionData(payload, "display", "Hello, world!")
    return payload
function payload = guiEvent(metaData, payload)
    Form.eventHandler("SayHello", @sayHello);
    payload = utils.dispatchEvent(metaData, payload);
end

function payload = sayHello(metaData, payload)
    payload = utils.setSubmissionData(payload, "display", "Hello, world!");
end

Note that you can configure the behaviour of the registered event handler functions by adding inputs to a function that wraps the actual function.

def gui_event(meta_data: dict, payload: dict) -> dict:
    Form.eventHandler(
        SayHello=say_something("Hello"),
        SayBye=say_something("Bye"),
    )
    callback = utils.getEventFunction(meta_data, payload)
    return callback(meta_data, payload)


def say_something(word: str) -> Callable:
    def inner(meta_data: dict, payload: dict) -> dict:
        utils.setSubmissionData(payload, "display", word + ", world!")
        return payload
    return inner
function payload = guiEvent(metaData, payload)
    Form.eventHandler( ...
        'SayHello', say_something("Hello"), ...
        'SayBye', say_something("Bye") ...
        );
    payload = utils.dispatchEvent(metaData, payload);
end

function func = say_something(word)
    % Wrapper function to multiply app input value with a configurable factor.
    func = @(metaData, payload) inner(metaData, payload, word);
end

function payload = inner(metaData, payload, word)
    payload = simian.gui.utils.setSubmissionData(payload, "display", word + ", world!");
end

Dispatching events

Using the information in the payload input it is possible to programmatically route events to the correct function with if-else blocks.

Alternatively, you can let Simian dispatch the events to the intended function for you. This can be achieved by using the dispatchEvent or getEventFunction utility functions:

caller = utils.getEventFunction(meta_data, payload)
payload = caller(meta_data, payload)
payload = utils.dispatchEvent(metaData, payload);

Simian will look for and execute the function that:

  • is explicitly registered to the event,
  • has the same full name (including packages and classes) as the event ("reflection"),
  • or throw an error

The dispatchEvent function calls a function equal to the name of the event that was triggered, whereas getEventFunction only returns the function object.

Note: In Python, breakpoints cannot be detected in functions executed via the dispatchEvent function. It is therefore recommended to use getEventFunction instead.

The functionality of the dispatching mechanism is further demonstrated in the example below.

Function arguments

meta_data:

Meta data describing the client session.

Dict/struct with fields:

  • session_id: unique ID for the session, can be used to differentiate between multiple instances of the application
  • namespace: package name of the application
  • application_data: The application data dictionary / struct. Contains the fields and values set by the Portal or local run call.
  • mode: local or deployed
  • client_data:
    • authenticated_user: for deployed apps, the portal provides the logged on user info
      • user_displayname: user name for printing (e.g. "John Smith")
      • username: unique identifier used by the authentication protocol

payload:

When entering the function, this contains the event data containing all values as provided by the client. It is a dict/struct with fields:

  • action: 'event'
  • download: information regarding the download of a file. Empty when no file is being downloaded.
  • event: name of the event that was triggered.
  • followUp (optional): name of a follow-up event that will be triggered after the current one has completed.
  • followUpMessage (optional): message to display on the spinner during the follow-up callback.
  • formMap: Map representing the form definition.
  • key: identifier of the component that triggered the event
  • submission: dict/struct with fields:
    • data: dict/struct with fields:
      • eventCounter: incrementing counter; do not change.
      • <key>: multiple fields containing the values of the components identified by the keys.
      • <nested form key>: See Nested forms for more information. dict/struct with fields:
        • data: dict/struct with fields:
          • <key>: multiple fields containing the values of the components identified by the keys.
  • updateForm: when true, the form definition is sent to the front end. The default value is false. Use this only when the form definition changes. Updating the form definition will be slower than only updating the submission data.
  • alerts (optional): array of dict/struct with fields:
    • type: message type, see Alerts
    • message: string

In the gui_event function, the submission field may be altered before sending payload back to the front-end in order to present results to the user. Additionally, the followUp field can be changed, which is described below.

The key is unique for each component within the context of its parent and/or nested form, but event can be shared between components. This can be useful when multiple components (partially) share functionality. See Example.

Follow-up Event

If the followUp field is added to the payload and set to the name of another event, that event will automatically be executed once the results of the current event are shown in the app. Both the original event and its follow-up share the same component key.

This mechanism is especially useful for workflows that consist of multiple steps, such as long-running calculations or data processing pipelines. It allows you to break the logic into clear stages while keeping the user informed of progress.

During a follow-up callback, you can also customize the spinner text by setting the followUpMessage field. For an even clearer user experience, this works well in combination with the StatusIndicator component.

Below is an example that demonstrates how to chain multiple events using follow-ups.

Example: Multi-stage execution with follow-up events

In this example, a single Run button triggers a three-phase process. Each phase simulates work by pausing for a short time and then schedules the next phase using followUp.

import time


def gui_init(meta_data: dict) -> dict:
    form = Form()
    payload = {"form": form}

    btnLoadData = component.Button("run", form)
    btnLoadData.label = "Run"
    btnLoadData.setEvent("Run")
    btnLoadData.properties = {"eventMessage": "Running phase 1..."}

    return payload


def gui_event(meta_data: dict, payload: dict) -> dict:
    if payload["event"] == "Run":
        time.sleep(1)
        payload["followUp"] = "RunNext"
        payload["followUpMessage"] = "Running phase 2..."
    elif payload["event"] == "RunNext":
        time.sleep(1)
        payload["followUp"] = "RunFinal"
        payload["followUpMessage"] = "Finalizing..."
    elif payload["event"] == "RunFinal":
        time.sleep(2)

    return payload
function payload = guiInit(metaData)
    form            = Form();
    payload.form    = form;

    btnLoadData         = component.Button("run", form);
    btnLoadData.label   = "Run";
    btnLoadData.setEvent("Run");
    btnLoadData.addCustomProperty("eventMessage", "Running phase 1...");
end

function payload = guiEvent(metaData, payload)
    switch payload.event
        case "Run"
            pause(1);
            payload.followUp = "RunNext";
            payload.followUpMessage = "Running phase 2..."

        case "RunNext"
            pause(1);
            payload.followUp = "RunFinal";
            payload.followUpMessage = "Finalizing..."

        case "RunFinal"
            pause(2);
    end
end

How this works:

  1. When the user clicks the Run button, the Run event is triggered.
  2. After completing the first phase, the handler sets payload.followUp to RunNext, causing that event to execute automatically.
  3. The same pattern is repeated to transition from RunNext to RunFinal.
  4. Each phase updates the spinner text via followUpMessage, giving the user clear feedback about the current stage.
  5. The process ends after the final event completes, with no further follow-ups defined.

This approach keeps complex logic readable and provides a smooth, guided experience for long-running operations.

Example

The following example shows a form with several buttons, each with their own event.

btn1 = component.Button("btn1", parent);
btn1.setEvent("ModelS.getResults");

btn2 = component.Button("btn2", parent);
btn2.setEvent("writeToDatabase");

btn3 = component.Button("btn3", parent);
btn3.setEvent("ModelX.getValue");

The following gui_event functions handle these events, the first one using conditional logic and the second one using the dispatchEvent or getEventFunction functions using event-function name reflection:

Note that the ModelS class contains a getResult method and the ModelX class a getValue method. For reflection, the event name must contain the class name!

def gui_event(meta_data, payload):
    if payload["event"] == "ModelS.getResults":
        ModelS.getResults(meta_data, payload)
    elif payload["event"] == "writeToDatabase":
        writeToDatabase(meta_data, payload)
    elif payload["event"] == "ModelX.getValue":
        ModelX.getValue(meta_data, payload)

def gui_event(meta_data, payload):
    # Use the automatic dispatch.
    caller = utils.getEventFunction(meta_data, payload)
    payload = caller(meta_data, payload)
function payload = guiEvent(metaData, payload)
    switch payload.event
        case "ModelS.getResults"
            payload = ModelS.getResults(metaData, payload);

        case "writeToDatabase"
            payload = writeToDatabase(metaData, payload);

        case "ModelX.getValue"
            payload = ModelX.getValue(metaData, payload);
    end
end

function payload = guiEvent(metaData, payload)
    % Use the automatic dispatch.
    payload = utils.dispatchEvent(metaData, payload);
end