App code execution flow
As a developer it is important that you fully understand how the VIKTOR platform triggers code when a user interacts with an application within the browser, in order to write effective code and prevent unexpected bugs. The app code execution flow is referred to as a job, and VIKTOR's execution flow might be different from the approach you are familiar with, especially if you are used to writing standalone (Python) scripts.
Triggering actions
A job is triggered if (and only if) one of the following actions take place:
-
Modifying input
Probably the most occurring trigger of the app's code is by changing the value of an input field. The entry point in the code is the currently active view-method.
noteIf the current view is 'slow' (i.e.
duration_guess>3
), the method is not triggered automatically. -
Refreshing a view
Similar to modifying the input, the currently active view-method is triggered if the user refreshes the view. This takes place when:
- entering the editor
- switching to another view tab
- clicking the view update button (in case of a 'slow' view)
-
Clicking an action button
The third way of triggering the code is by clicking an action button (e.g.
DownloadButton
). The entry point in the code is the method linked to the button that is clicked. -
Clicking a next step button
In case of a stepwise editor, the 'on-next' function is triggered when a user navigates to the next step.
-
Pre-process upload file
A less typical case occurs when defining an entity type as file. During the uploading of the file, the method decorated with the
ParamsFromFile
is triggered.
In all cases, a method on the corresponding controller class is called, which ends with returning the required result to the platform. A job is completed after this return and code execution is terminated, waiting for a next trigger (which might already be present in the queue). The next trigger follows the same procedure (and if it is the same action it actually runs the same piece of code again!).
Stateless code
A consequence of above-mentioned repetitive call flow is that the app's Python code is stateless (though within a
call-cycle the code can still be stateful). This means that data is not stored within the code (e.g. on a global
variable) for using it in the next call cycle. Instead, the data is recalculated each time it is requested. The only
data that is stored (and thus can be used for the next call cycle) are the parameters (params
) on each of the
entities within the app.
VIKTOR's stateless design is clarified in the following example. Imagine a cube defined by its length, width, and height,
that can be set by the user. We want to show results based on these inputs to the user, by creating a TableView
:
import viktor as vkt
class Parametrization(vkt.Parametrization):
length = vkt.NumberField("Length", default=1)
width = vkt.NumberField("Width", default=1)
height = vkt.NumberField("Height", default=1)
class Controller(vkt.Controller):
parametrization = Parametrization
@vkt.TableView("Results")
def results(self, params, **kwargs):
volume = params.length * params.width * params.height
surface = 2 * (params.length * params.width + params.length * params.height + params.width * params.height)
data = [
[volume, "m³"],
[surface, "m²"],
]
row_headers = ["Volume", "Surface"]
column_headers = ["Value", "Unit"]
return vkt.TableResult(data, row_headers=row_headers, column_headers=column_headers)
Note that even though we calculate the surface area and volume of the cube, we never store this data. Each time the result is requested (e.g. when updating one of the dimensions in the editor), the above code is rerun.
Similarly:
- A report is generated and returned as download each time the user clicks the
DownloadButton
(the report is never stored). - A 3D-model is created and visualized in a
GeometryView
each time the user refreshes the corresponding view tab or modifies the input within the editor (the 3D-model is never stored). - A plot of the results is drawn and visualized in an
ImageView
each time the user refreshes the corresponding view tab or modifies the input within the editor (the figure is never stored). - etc.
Single source
The examples of output above (report, 3D-model, plot) can be seen as representations of one and the same model (defined solely by its parametrization). Besides it is not possible to store these alternative representations, it is also not desired. What would happen if we would store the 3D-model of the cube and subsequently modify its length? Exactly, the visualization would be outdated, possibly without the user even noticing.
The concept of defining a 'model' by its minimal set of independent parameters is called single source modelling.
Long-running results
Sometimes, recalculating the results each time the information is requested is too time-consuming (e.g. if doing some long-running analysis to calculate results) and therefore not an option. We call such type of output long-running results. Below are solutions to two of the most common cases.
Slow views
For example, a user would not expect (nor desire) a plot of the results of a long calculation (e.g. a structural analysis with third-party software) to be
cleared if he or she (accidentally) changes the value in a NumberField
.
Here slow views come in handy. A slow view is a View
that is refreshed by clicking its 'update' button. It is
triggered by setting a high duration_guess
. The user is notified by means of a
warning if the result has become outdated, but the figure would remain visible.
Memoize
Another common example is the implementation of a DownloadButton
(e.g. generating a result report) and a View
(e.g.
for showing the results in a plot) calling the same analysis. If a user wants to visualize the results within the view
and download the report to its local hard disk, running the analysis twice would be undesirable.
Preferably, the result is stored internally, so that it will return instantly on the second (or third) call. The
memoize
function can be used to cache a calculation and achieve exactly this.
Persistent storage
VIKTOR offers storage which can be used to store and retrieve files within an app workspace. The storage is persistent, meaning that the data will remain available with no time limit. This can be very helpful in cases where (intermediate) results need to be shared between jobs. For example, a long-running task is performed in job A, of which the results can be accessed in job B without the need to rerun the task. Read our guide for more detail!
Local files persistence between jobs
When writing data to a file in the filesystem of your development environment, this file will be available between different jobs because it is actually saved on disk. However, the production environment works in a different way. An application is running in production in multiple processes, to support executing multiple jobs by multiple users working in parallel. Each of those processes has its own disk space. This means that the files stored in the disk of a specific process will be available to all jobs executed by that process, but not to other jobs executed by other processes. You as developer cannot control these processes or in which one a job will run, which could result in unpredictable behavior. Therefore we advise against working with local files! You can instead use the Persistant Storage. Read our guide for more detail!