Events and the POST
An event binds a user action on a block to a 4D function. It is stored in the record's events field:
"events": {
"onClick": [
{ "vt_ClassName": "CONTACT", "vt_FunctionName": "togglePublish", "vo_Param": { } }
]
}
One event type holds a collection: several actions can fire in order.
What vt_ClassName may be
The dispatcher accepts either a dataclass or a project class:
If (ds[$vo_POST.vt_ClassName]#Null)
$vo_Controller:=ds[$vo_POST.vt_ClassName] // dataclass: used directly
Else
$vo_Controller:=cs[$vo_POST.vt_ClassName].new() // project class: INSTANTIATED
$vo_Controller.vo_Session:=$vo_Param.vo_Session // and handed the session
End if
So a controller class works, and it even receives vo_Session, which a dataclass does not get this way. If a name exists in both, ds wins.
Worth knowing: documentation written for V20 states as a critical rule that
vt_ClassNamemust be a dataclass and cannot be a project class. That is not true of V21. If a project class fails silently, the cause is visibility (below), not dispatch.
Being callable and being offered in the dev panel are two different things. BSPK_REFRESH_STORAGE builds the picker's list by scanning classes that declare Class extends WebFormController, DataClass, Entity or EntitySelection, keeping functions whose signature matches @(@)->$vo_WebResponse@. A plain project class with no extends can be dispatched but will never appear in the panel, which is how it ends up looking broken.
The WF suffix: only for value lists
There are two lists, built by BSPK_REFRESH_STORAGE with two different patterns:
| List | Pattern scanned | Used by | Naming rule |
|---|---|---|---|
AvailableController | @(@)->$vo_WebResponse@ | events (onClick, onChange…) | none, any name |
AvailableControllerForSelect | @WF($vo_POST : Object)->$vo_Return : Object@ | vt_ValuesFunction of fieldSelect / fieldCheckbox | must end in WF |
For an event handler, what matters is the return type, not the name. For a function feeding a select or a checkbox group, the name must end in WF and the signature must match to the character. Get it wrong and the field renders empty, with no error anywhere.
After adding such a function, reload the code, then run BSPK_REFRESH_STORAGE. Until you do, the panel does not know it exists.
The function name is camel-cased before the call
The dispatcher calls $vo_Controller[BSPK_String_To_CamelCase($vo_POST.vt_FunctionName)]. A function declared Function TogglePublish is therefore looked up as togglePublish and not found: the request returns with nothing done. Declare your functions in camelCase.
What the function receives
Function togglePublish($vo_POST : Object)->$vo_WebResponse : cs.bspkComponent.WebFormController
$vo_WebResponse:=cs.bspkComponent.WebFormController.new()
…
The dispatcher actually passes four arguments: $vo_POST, Entity, the session and an info object. You may declare more than one parameter if you need them.
$vo_POST is never Null inside BSPK_WEB_ON_CONNECTION; declaring it and testing it for Null at the top of a method tests the wrong thing.
Getting form values into $vo_POST
Nothing is sent automatically. You declare what you need in a /* parameters */ comment block, right under the function declaration:
Function createPayment($vo_POST : Object)->$vo_WebResponse : cs.bspkComponent.WebFormController
/* parameters
vt_BlocForm:select:getBlocsNameCollection:["wrapper","accordion","modal"]:mandatory:tomSelect:label:Form
vt_SendAllObjectsOfBlocName:hidden:vt_BlocForm
*/
Two common needs:
| Goal | Declaration | Arrives as |
|---|---|---|
| The rows selected in a listbox | vt_GetBlocInfo:select:getBlocsNameCollection:mandatory:tomSelect | $vo_POST.{blockName}_selectedRows |
| Every field of a container | a block selector + vt_SendAllObjectsOfBlocName:hidden:<its name> | $vo_POST.vt_FieldValue.{blockName} |
The indirection everyone gets wrong
vt_SendAllObjectsOfBlocName holds the name of the other parameter, as a string. Never a uuid, never a block name:
"vo_Param": {
"vt_BlocForm": "1DAC5696B4EB4A6A8C3ACB34E25A37C5", // the block's uuidKey
"vt_SendAllObjectsOfBlocName": "vt_BlocForm" // the PARAMETER NAME
}
Get it wrong and vt_FieldValue arrives empty ({}) while the function runs normally and reports success. The diagnosis is always the same: look at the request body of /bweb/call-action in the browser's network tab.
Two related traps, both silent:
- A field with no
vt_FieldNameis stripped by the security check, so it never reachesvt_FieldValue. - Values arrive as text, including for a single field's
onChange. Convert explicitly, and useBSPK_STRING_TO_REALrather thanNum()for decimals.
Answering: the WebFormController
The returned object is what acts on the page: sendAlert, reloadBlock (one block per call, no collection), redirectToRoute, opening or closing a modal. It is serialised into the response and replayed client-side.
sendAlert followed by a redirect destroys the toast: the page reloads before it can be read. When the message carries information, do not redirect, or use sendAlertAfterRedirect.
When nothing happens at all
In order of likelihood:
- The security check refused the POST. Look for the JSON error on
/bweb/call-action, not for a bug in your function: it was never called. - The function is not loaded (new function, no
BSPK_REFRESH_STORAGE). A call to a function that is not loaded hangs rather than failing. - The name is not camelCase.
- The class has no
extends, so the panel never offered it and the event is half-configured.

