Part 4 -Let the Usertask determine the UI layout

Frans van Ek
4 min readOct 12, 2022

--

This series describes the implementation of a User Interface driven by an Elsa workflow engine. In this part, we will investigate a bit more how the engine can be the driving force of the UI. The idea is that the workflow can dictate which data entries the UI should provide.

Photo by Kelly Sikkema on Unsplash

In the Previous part — Adding functionality to return data from a user task it was a simple list of workflows on which the data could be entered. In this part it will be extended a bit more.

We will introduce a new page that displays the user task details. The user task screen can be fully customized to the needs of the user task.

This implementation is not that different from the previous version. The only difference is that there is a specific page for handling the user task.
Based on the signal name, the component is selected. It will get the data and provide it back to the engine.

If the signal is unknown, the fallback scenario is just to send a completed signal without any data.

This signal and UI coupling show one of the problems with the current implementation. There is a tight coupling between the configuration of the engine and the UI. The engine dictates the signal and assumes the UI will provide the data as expected. The UI considers for some cases, it can complete the task without sending any data.

This coupling is not something new. In most cases, there will be a tight coupling between UI and the backend. If you know this, you can set up a DevOps environment where they are both deployed.

In the implementation of the user task activity, there were more properties than we haven’t used until now.

UIDefinition

This property allows the configuration of the user task holds a UI definition. It is a suggestion by the workflow for the UI. Whether or not the UI adheres to it is not the workflow concern.

In the current implementation, the UI-Definition is used by the dynamicUserTaskComponent. This component leverages the functionality of the dynamic Blazor form.
Dynamic Blazor Form is an experimental component that creates a form based on a JSON structure. The input in the form results in a JSON object.

(At a later point, I will write a series of documents on this component as well)

A sample of a UI definition can be seen in the next code snippet.

This Json containg the proposed UI structure is converted to a Form. The JSONPath — XPath for JSON (goessner.net) notation determines the structure of the result of each element.

DynamicUserTask

The working of the component is quite simple. It gets the layout, generates a form, and on the binding, the component updates the resulting model whenever the form’s input fields change their value.

The Dynamic Blazor form allows you to define which implementation of the UI components you want to use inside the form. You can leverage the default HTML components or use, for instance, the Radzen component library. Mixing and matching, even creating your types, is possible.

The components you want to use need to be registered in the startup.cs.

This means that it is possible to have new components in the UI-Definition and let het UI map to specific components for those types.

When creating new user tasks with known UI-Definition names, updating the Web application is not necessary. The application can now create the forms and send the result back to the engine.

TIMING Issues

In the implementation of the workflow detail page, UI components activate the task completed event. The component sends the signal with the data to the engine. After that, it will raise the TaskFinished event. The detail page will act on this by loading the next usertask.

But what if

  • the engine has not completed the task yet.
    - The detail page will show the same usertask again.
  • The workflow has ended
    - nothing to show.
  • The execution of the next step is a long running background activity
    —the user has to wait but what to show in this case?

There are some solutions to mitigate these issues. Unfortunately, most of them resort to having more communication between the engine and the client. For example, knowing the workflow’s last execution time. So, when sending the signal, the client knows when the workflow was updated because the execution time will differ. But it also means that the client needs to hold state.

Dispatch or execute?

Sometimes the issue is not that severe because of the intent of the workflow. For example, when all activities are user tasks, the signals can be sent as ‘Execute.’ This type of request means that the request will trigger the execution of the workflow, and only when the workflow stops running is the result handed back. This execution method is a synchronous call. This scenario works well in the case of a wizard-like process where the data is stored in the workflow instance itself, and there are no dependencies on possible slow-running activities.

If the workflow holds long-running steps, then dispatch is the best option, but that requires a sort of synchronization. Alternatively, an ugly method for polling or a task delay for retrieving the next step. Under the assumption that within a certain period, the workflow will be executed.

Luckily there is a mechanism to handle the communication between the client and the engine gracefully. A SignalR implementation deals with it.

In the next part, the signal R implementation is covered.

Next: Part 5 — Use notifications to update the User Interface

Used version: Elsa 2.10.467

--

--

Frans van Ek

Software developer architect with a passion for sustainability., just trying to improve the world a bit.