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_RowPkis 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, notsaveStatus; - writing into
savinghas no effect; METHOD GET CODEreads the in-memory version, not the file on disk, so it will happily confirm code you have just changed but not reloaded;- no
ALERTserver-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
| Property | Role |
|---|---|
errCode | Integer error code |
message | Displayed message |
seriousError | True = visible exception; False = silent validation failure |
extraDescription | Optional 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
| Goal | File |
|---|---|
| Acting on one record | <TABLE>Entity.4dm |
| Acting on the table or a set | <TABLE>.4dm / <TABLE>Selection.4dm |
| Orchestration with no natural record | a 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 …
| Zone | Written by | Notes |
|---|---|---|
| 1 — between the markers | BSPK_UTIL_UPDATE_DATACLASS | Re-injects store, delete, updateFromObject… on every pass. A function you redefine is renamed xxxBspk |
2 — up to YOUR CODE AFTER THIS | Launcher_FC, from Resources/Misc/DataClasses/<Class>.4dm | Copied into the host at install |
| 3 — after the marker | You | Preserved 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
- Events and the POST: the dispatch, and what
$vo_POSTcontains - The 4D language:
Num(Null), typed variables, shared objects

