Writing 4D functions

A block reaches your code in four different ways, with four different signatures. Using the wrong one is the most common cause of "it does nothing and says nothing".

1. An event handler

Function togglePublish($vo_POST : Object)->$vo_WebResponse : cs.bspkComponent.WebFormController
    $vo_WebResponse:=cs.bspkComponent.WebFormController.new()
    …
    $vo_WebResponse.sendAlert("success"; BSPK_Translate("Toast_done"))

The return type is what makes the function visible to the panel, not its name. Put it in a dataclass, an entity class, or a project class that extends WebFormController: the dispatcher accepts ds and cs alike (see Events and the POST). Name it in camelCase.

2. Filling a select or a checkbox group

Function getMyValuesWF($vo_POST : Object)->$vo_Return : Object

A name ending in WF, and exactly that signature. No exception, no tolerance: a mismatch gives an empty field with no error. Run BSPK_REFRESH_STORAGE after adding one.

3. A hook before save

It is configured on the form block, as vt_HookBeforeSave. If you did not write the arguments, the framework appends them itself:

$vt_Expression+="(vo_POST;This)"

So the signature is:

Function hookBeforeSaveContact($vo_POST : Object; $vo_WFC : cs.bspkComponent.WebFormController)

To block the save, set $vo_WFC.vb_Continue:=False and send a message. Three traps, all silent:

  • Never reassign $vo_WFC: assign to its properties. Replacing the object loses the caller's reference, and your refusal never arrives.
  • The parameter is vo_Param, singular, where other mechanisms use plurals.
  • vt_RowPk is absent in this context; do not reach for it.

Note the order: the hook runs after the security check, so format constraints (vt_PregMatch, required, input type) have already been enforced. A format test here is never reached for a value the schema already rejected.

4. Formulas evaluated during render

conditionalDisplay, a column's vt_FieldFormula, vt_FirstRequest, a repeat's function: these are expressions, run through EXECUTE FORMULA with the page context. They are not called with parameters; they read Entity and the other process variables.

For a listbox column returning HTML (a multi-state icon, for instance), the function goes in the Entity class, not the dataclass, and returns raw HTML.

Computed attributes need their own functions

A computed attribute that is queried needs a Function query, and one that is sorted on needs a Function orderBy. Without them, 4D falls back to a sequential scan, or cannot sort at all. This is the usual explanation for a listbox that is slow only when filtered.

ORDA events

store() and delete() are aliases of save() and drop(); the logic belongs in the entity events. Four things to know:

  • the status is $event.status.success, not saveStatus;
  • writing into saving has no effect;
  • METHOD GET CODE reads the in-memory version, not the file on disk, so it will happily confirm code you have just changed but not reloaded;
  • no ALERT server-side, ever: the dialog blocks the process, with nobody there to dismiss it.

Entity events (validateSave, validateDrop…) exist from 4D 21 onwards. On 20 R10, the only override point is BSPK's own delete().

Refusing a save from validateSave: the error object must be FLAT

A data-model event fires on every write path: .save(), .store(), import, sync, the Claude API and the desktop ATL. It is therefore the only place a guard holds outside the web. A check placed in a dataclass function called by BWEB screens protects those screens only.

Function event validateSave($event : Object) : Object
    If (<refusal condition>)
        return {errCode: 1; message: "…"; seriousError: True}
    End if
PropertyRole
errCodeInteger error code
messageDisplayed message
seriousErrorTrue = visible exception; False = silent validation failure
extraDescriptionOptional free-form object

Returning Null, or nothing, lets the save proceed.

Trap: any other shape is ignored silently. {errors: [{message: "…"}]}, a plausible form close to what other 4D APIs return, blocks nothing: the event runs, the refusal is computed, the message is built, and the record is saved anyway. The guard looks installed and protects nothing.

Validate a guard by provoking the refusal, then check two things: the message appears, and the value is unchanged in the database, not just on screen.

Where to put the code

GoalFile
Acting on one record<TABLE>Entity.4dm
Acting on the table or a set<TABLE>.4dm / <TABLE>Selection.4dm
Orchestration with no natural recorda class that extends WebFormController

For host tables, the live entity class is the host's (Project/Sources/Classes). The copies under the component's Resources/Misc/DataClasses are installation templates: editing them changes nothing at runtime.

A dataclass file has three zones, written by three different actors

Class extends BSPKEntity

/*** START BSPKENTITY ***/
   … zone 1: the generic base …
/*** END BSPKENTITY ***/

   … zone 2: BWeb-specific …

/********* YOUR CODE AFTER THIS ********/

   … zone 3: your code …
ZoneWritten byNotes
1 — between the markersBSPK_UTIL_UPDATE_DATACLASSRe-injects store, delete, updateFromObject… on every pass. A function you redefine is renamed xxxBspk
2 — up to YOUR CODE AFTER THISLauncher_FC, from Resources/Misc/DataClasses/<Class>.4dmCopied into the host at install
3 — after the markerYouPreserved when zone 2 is re-copied

Confusing them either loses code at install time or accumulates copies of it. A class that carries only the base has nothing to do in Resources/Misc/DataClasses: it is generated on its own.

See also