4.2 Event Handler Registration & Navigation to Custom Pages via the Client API
Key Takeaways
- OnLoad fires once at load, OnChange fires per attribute value change, and OnSave can inspect the save mode and cancel the save with preventDefault().
- A handler must have its library attached to the form, be referenced by a namespaced function name, and have the execution-context checkbox selected, or it silently fails or misbehaves.
- Namespacing function names (e.g., Contoso.Forms.Account.onLoad) avoids collisions when multiple libraries load on the same form.
- Xrm.Navigation.navigateTo() opens custom pages inline or as a dialog and can pass parameters, including the current record's ID.
- Structured dialog helpers (openAlertDialog, openConfirmDialog, openErrorDialog) should replace native browser alert()/confirm() calls.
Client API code only runs when something triggers it. This section covers how form events are registered so your JavaScript actually executes at the right moment, and how the Client API navigates users to custom pages — the canvas-app-style surfaces that can be embedded inside a model-driven app.
Which Form Events Fire, and When
| Event | Fires when | Typical use |
|---|---|---|
OnLoad | Once, after the form finishes loading but before it renders to the user | Set defaults, conditionally hide fields, apply initial notifications |
OnChange | An attribute's value changes, either by the user or by setValue() followed by fireOnChange() | Cascading logic — e.g., filtering a lookup based on another field |
OnSave | Save is initiated, whether by the ribbon, autosave, or formContext.data.save() | Final validation; can block the save entirely |
TabStateChange | A tab is expanded or collapsed | Lazy-load content, adjust layout |
GridLoad / OnRecordSelect | A subgrid loads or a row is selected | Subgrid-specific customization |
Multiple handlers can be registered on the same event, and they execute in the order listed in the form's Event Handlers configuration — but relying on implicit ordering across unrelated libraries is fragile, and PL-400 scenario questions often hinge on recognizing that a handler didn't run at all rather than ran in the wrong order.
Registering a Handler
Registration happens in the form designer's Event Handlers pane, not in code. Three pieces must line up:
- Library — the JavaScript web resource must first be added to the form's library list; a function cannot be referenced from a library that isn't attached to the form.
- Function — specify a fully namespaced function, such as
Contoso.Forms.Account.onLoad, rather than a bare global function name, to avoid collisions when multiple ISV or customization libraries load on the same form. - Pass execution context as first parameter — this checkbox must be selected, or the handler receives no arguments at all, making
executionContext(and thereforeformContext) undefined inside the function. This single unchecked box is one of the most common causes of "my script doesn't work" bugs in real projects and on the exam.
var Contoso = Contoso || {};
Contoso.Forms = Contoso.Forms || {};
Contoso.Forms.Account = Contoso.Forms.Account || {};
Contoso.Forms.Account.onLoad = function (executionContext) {
var formContext = executionContext.getFormContext();
// initialization logic
};
OnSave: Reading Save Mode and Cancelling a Save
The OnSave handler receives the same executionContext, but you additionally call executionContext.getEventArgs() to get a SaveEventArgs object. getSaveMode() returns a numeric code identifying why the save fired — a plain Save, Save and Close, Save and New, Deactivate, or an autosave — which lets validation logic behave differently for interactive saves versus background autosaves. Calling eventArgs.preventDefault() cancels the save entirely, which is the standard pattern for client-side validation that must block persistence, such as rejecting an out-of-range value before it reaches the server.
Navigating to Custom Pages
Custom pages are canvas-app-style pages that live inside a model-driven app rather than as a standalone canvas app. They can be reached through the app's site map, opened from a command, or launched programmatically with Xrm.Navigation.navigateTo().
var pageInput = {
pageType: "custom",
name: "cr123_customreviewpage_1a2b3",
entityName: "account",
recordId: formContext.data.entity.getId()
};
var navigationOptions = {
target: 2, // open as a dialog
position: 1, // centered
width: { value: 60, unit: "%" },
height: { value: 80, unit: "%" }
};
Xrm.Navigation.navigateTo(pageInput, navigationOptions).then(
function success() { /* dialog closed */ },
function error(e) { console.error(e); }
);
pageInput identifies the custom page and can pass parameters (including the calling record's ID) into it; navigationOptions.target chooses whether the page opens inline in place of the calling content or as a dialog, with position and width/height controlling dialog placement. navigateTo() returns a promise that resolves when the dialog closes, which is useful for refreshing the calling form after the custom page finishes its work.
Xrm.Navigation also exposes openForm() (open a different record's form), openAlertDialog(), openConfirmDialog(), openErrorDialog(), and openUrl(). These structured dialog helpers should be used instead of native browser alert()/confirm(), which are inconsistent with the app's UI and can be blocked by the browser. Knowing which navigation method matches a given requirement — opening an existing record's form versus a custom page versus a plain URL — is a frequent scenario pattern on the exam.
Disabling Versus Removing a Handler
The Event Handlers pane also has an Enabled checkbox per registered handler, independent of the handler's presence in the list. Unchecking it stops the handler from firing without deleting the registration, which matters when troubleshooting: a disabled handler is invisible at runtime but still shows up if a colleague later inspects the form's customizations, which can be confusing if the checkbox state isn't documented. Handlers can also be removed entirely with removeOnChange() for attribute-level OnChange registrations made dynamically from code — for example, a script that conditionally wires up a temporary OnChange handler during a wizard-like flow and tears it down again once the flow completes, rather than leaving a stale listener attached for the rest of the session.
Static Parameters and Passing Data into a Handler
Beyond the execution context, the Event Handlers pane lets a developer supply comma-separated static parameters that get appended after executionContext when the function is invoked, which is useful for reusing one generic library function across several fields or forms with different configuration per registration (for example, a single Contoso.Forms.Shared.validateRange function reused on both a credit-limit field and a discount-percentage field, each registered with different min/max static parameters). This pattern avoids duplicating near-identical functions across web resources purely to hardcode different thresholds, and PL-400 scenario questions occasionally test whether a candidate recognizes reusable, parameterized handlers as the more maintainable design over one-off copies.
A form library's OnLoad handler is registered in the form designer's Event Handlers pane, but formContext inside the function is undefined at runtime. What is the most likely cause?
Which Xrm.Navigation call should a developer use to open a custom page as a centered dialog and pass the current record's ID into it?