5.1The decision ladder
Business rule, Power Fx command, or script
"The field is hidden until the type is chosen" can be met three ways on this platform, and the three are not interchangeable. What you are choosing between is where the behaviour is stored, who can read it after you have gone, and where it runs.
The ladder below runs from the most declared to the most general, and it reads downwards: your requirement stops at the first rung that reaches it.
| Rung | Where it lives | What it reaches | Who can read it |
|---|---|---|---|
| Business rule | A solution component on one table | Column values, visibility, lock state, requirement level, an error message | A maker, in the designer |
| Power Fx command | A command component library in one app | Command action and command visibility | A maker, in the command designer |
| Form script | A JavaScript web resource, registered on a form | The documented Client API surface | A developer, in source control |
A business rule's actions are Set Field Value, Set Default Value, Show Error Message, Lock/Unlock, Set Visibility, Set Business Required and Recommendation; it applies to main and quick create forms, and the four actions that apply to model-driven apps alone are ignored when the rule runs server-side.citedsource: Power Apps docs — Create business rules and recommendations — what a business rule can do
So the first rung is wider than it looks, and it stops abruptly. It covers much of what you would otherwise write a form script for — showing, hiding, locking, defaulting, blocking a save — and it reaches no data beyond the columns on the form and no service outside Dataverse. Chapter 4 sets out the rule's own boundary in more detail (§4.5).
Implementing the same behaviour on two rungs at once costs you more than picking the wrong rung. A hidden field with both a business rule and an onLoad handler acting on it behaves according to their order, and neither surface shows you the other.experientialbasis: handovers of implementations where the same visibility logic existed as both a rule and a script, and the two had drifted apart
Guidance
Write the requirement as a business rule first, and reach for script when the rule will not reach it. Whoever owns the app after your project ends can read a rule in the designer; a script needs a developer and the repository it lives in.
5.2Web resources, and how a script reaches a form
A file, a solution component, a registration
A form script is a web resource: a file stored as a row in Dataverse and carried in a solution like any other component. Getting your script onto a form is three separate acts — writing the file, importing it as a component, and registering a function on an event.
In solution source a script web resource is a pair of files: the script itself, and a sidecar naming its type and its id.
Evidence — the pair of files, the pack, the import, and the row it left
"use strict";
var ch45 = window.ch45 || {};
ch45.account = (function () {
function onLoad(executionContext) {
var formContext = executionContext.getFormContext();
var name = formContext.getAttribute("name");
if (name === null) { return; }
formContext.getControl("name").setDisabled(name.getValue() !== null);
}
return { onLoad: onLoad };
}());
getAttribute returns null for a column that is not on the form, which is a different condition from a column whose value is empty — see §5.3.The sidecar for a script web resource declares WebResourceType 3 for JScript and a WebResourceId.verifiedartifact: ch05-webresource-source · artifacts-ch05.md
The packer reports the components it processed and names each web resource it registered.verifiedartifact: ch05-webresource-pack · artifacts-ch05.md
Packing Solution... Processing Component: Entities - Account Processing Component: Roles Processing Component: Workflows Processing Component: FieldSecurityProfiles Processing Component: Templates Processing Component: EntityMaps Processing Component: EntityRelationships Processing Component: OrganizationSettings Processing Component: optionsets Processing Component: CustomControls Processing Component: WebResources - ch45_formHandlers.js Processing Component: AppModuleSiteMaps Processing Component: AppModules Processing Component: EntityDataProviders Processing Sharded Component Files Unmanaged Pack complete.The source and destination paths, and the trailing "Packed Solution.", are in artifacts-ch05.md, the paths with their absolute prefixes elided there too. Neither file carries pac's version banner.
Processing Component: WebResources is the line to look for, and the file's name sits under it. A run from a source folder one element short printed neither line, and the zip it wrote still carried the file.What the pack and the import do when each element is removed, one zip per case. A sidecar with no WebResourceId fails the pack, and no zip is written. A source folder with no <WebResources /> element in Other/Customizations.xml packs a zip that carries both files, registers no component, and warns in a line naming the root component it could not find a definition for. And a zip of that second shape, reduced to the web resource as its only root component, is rejected with Cannot add a Root Component ch45_formHandlers.js of type 61 because it is not in the target system by an environment that does not hold the web resource, and accepted by one that does — one zip, two targets, each target's prior state read with pac env fetch first.verifiedartifact: ch05-pack-rejections · artifacts-ch05.md
Importing the solution is one operation and publishing is a second the same command performs when asked; the run reported each in turn.verifiedartifact: ch05-webresource-import · artifacts-ch05.md
Connected as skydude@itrancyngergmail.onmicrosoft.com Connected to... skylib-test Solution Importing... Solution Imported successfully. Publishing All Customizations... Published All Customizations.
--publish-changes is what asked for the second, and the run's own output is the record of which of them the run performed.After import, the web resource is a row in Dataverse whose webresourceid is the id written in the source sidecar rather than one the platform generated.verifiedartifact: ch05-webresource-fetch · artifacts-ch05.md
Connected as skydude@itrancyngergmail.onmicrosoft.com Connected to... skylib-test name webresourcetype ismanaged webresourceid ch45_formHandlers.js Script (JScript) Unmanaged c4501ab1-4501-4501-9c45-c45c45c45c45
5.2.1The handler registration in FormXml
Registering a function on an event writes two things into the form's metadata: a library reference, and a handler. The library reference declares which web resource the form loads; the handler names your function inside it and the event it answers.
A handler written to take an execution context and registered without that attribute fails inside your function rather than at registration. What you see is a method called on an undefined value, which reads as a broken script rather than as a missing checkbox, and sends you to the wrong file.experientialbasis: form scripts inherited across implementations, where a handler taking an execution context had been re-registered by hand without the checkbox
Evidence — the registration in FormXml, and what the export of it carried
A registration is a Handler element carrying functionName, libraryName, a handlerUniqueId and passExecutionContext, sitting inside Handlers on an <event> marked application="false"; the platform's own handlers for the same event sit in InternalHandlers on a separate element marked application="true".verifiedartifact: ch05-handler-registration · artifacts-ch05.md
<events>
<event name="onload" application="true" active="true">
<InternalHandlers>
<Handler functionName="AppCommon.Account.Instance.ra_onload"
libraryName="AppCommon/Account/Account_main_system_library.js"
handlerUniqueId="7927DD68-5AC5-4E9A-B39A-44F62467656A"
enabled="true" />
</InternalHandlers>
</event>
<event name="onload" application="false" active="false">
<Handlers>
<Handler functionName="ch45.account.onLoad"
libraryName="ch45_formHandlers.js"
handlerUniqueId="{bb450002-4501-4501-9c45-c45c45c45c45}"
enabled="true" parameters="" passExecutionContext="true" />
</Handlers>
</event>
</events>
<formLibraries>
<Library name="ch45_formHandlers.js"
libraryUniqueId="{aa450001-4501-4501-9c45-c45c45c45c45}" />
</formLibraries>
The returned formxml is one line; it is broken here for reading. Attribute
names, values and order are unchanged.
passExecutionContext="true" is what the "Pass execution context as first parameter" checkbox writes. The attribute sits on the Handler element rather than on the Library element, so two functions drawn from the same web resource can differ in whether a context is passed to them.FormXml carries <events> at more than one scope: the form's own element follows </tabs>, and a control's element sits inside its <cell>. A registration written into the control's element imported without complaint and was stored against that control.verifiedartifact: ch05-events-scope · artifacts-ch05.md
An unmanaged solution's export of a stock table's form is a differential: elements the layer changed carry solutionaction="Modified" and elements it introduced carry solutionaction="Added". In this export the added formLibraries element was present and <events /> was empty, while the same form read back through pac env fetch carried the handler registration above.verifiedartifact: ch05-differential-export · artifacts-ch05.md
</tabs>
<events />
<header id="{d5a03552-1183-4347-a237-1f894ba449eb}" columns="111" celllabelposition="Top" labelwidth="115">
<rows>
<row>
<cell id="{d3e19d4e-b2ae-409f-9b4e-5976527c6c83}" ordinalvalue="10000">
<labels>
<label description="Annual Revenue" languagecode="1033" solutionaction="Modified" />
</labels>
</cell>
<formLibraries solutionaction="Added">
<Library name="ch45_formHandlers.js" libraryUniqueId="{aa450001-4501-4501-9c45-c45c45c45c45}" />
</formLibraries>
</form>
Two fragments of the same file, with the rows between them omitted.
Caution
A registration at the wrong scope is a slow defect to find, because everywhere you would look shows what a correct registration shows: the packer's output, the import log and the solution's own diff are unchanged by it.experientialbasis: registrations found at the wrong scope during handovers, where the deployment log and the solution diff had both been read and had both looked correct
Guidance
Confirm a handler registration in the target environment after a deployment, by reading the form's formxml back rather than by inspecting the solution that was shipped. The two answer different questions, and this chapter's own lab found them disagreeing.
5.3The formContext object: attributes and controls
Attributes, controls, and which method hangs off which
Your handler is given an execution context and asks it for a form context. It reaches the record and the interface through that object, and those are two separate collections on it.
formContext is a reference to the form, or to an item on it such as a quick view control or an editable-grid row, for the code currently executing; it is obtained from the passed execution context with getFormContext, which is what the "Pass execution context as first parameter" option enables, and it is valid during the event it was passed in and no longer.citedsource: Power Apps docs — Client API form context — getFormContext and the execution context
An attribute is a column's data on this record. A control is a rendering of a column on this form. Which of the two you are holding decides which method you can call: you read a value from an attribute, and you set a visibility on a control.
The attributes collection reaches the columns present on the form and no others, and a column may have more than one control on a form — so the controls collection for one column may hold more than one entry.citedsource: Power Apps docs — Client API form context — the attributes and controls collections
Two consequences follow, and both cause intermittent script failures. First, a handler you wrote against a main form breaks on a quick create form that omits the column, because getAttribute returns null and your next call in the chain lands on null. Second, hiding a column means hiding each of its controls, so a script that hides the first one leaves the second visible.
5.3.1Values, and the OnChange event
Reading a value is getValue on the attribute; writing one is setValue. Writing a value in script does not leave the form in the state a user's edit would: the dependent logic hanging off that column has not run.
formContext.getAttribute(arg).fireOnChange() causes the OnChange event to occur on the column, so that script associated with that event can execute.citedsource: Power Apps docs — fireOnChange (Client API reference)
That method exists because the event is what other logic listens to. setValue writes the column; the handlers registered on that column's OnChange event run when the event is raised, and fireOnChange is what raises it.
Caution
Call fireOnChange from inside an OnChange handler for a column that same handler writes to and you have a loop the platform will not stop. What you see is a form that hangs on one edit rather than an error, which is what makes it slow to attribute.experientialbasis: forms that hung on a single edit, where the handler writing the column also raised its OnChange
5.3.2Visibility, requirement level and notifications
Three further surfaces account for most of what you will ask a form script to do: hiding things, changing what the form insists on, and telling the user something is wrong.
setRequiredLevel takes one of none, required or recommended, and lowering a column's level in script does not lower it on the server: a column the server insists on still fails the save when it is empty.citedsource: Power Apps docs — setRequiredLevel (Client API reference) — the permitted values
formContext.getControl(arg).setNotification(message, uniqueId) puts an error on a control and blocks the form from saving; the optional uniqueId is the handle clearNotification later takes, and a notification set on a control inside a hidden section or tab still blocks the save while the user has no way to reach it.citedsource: Power Apps docs — setNotification (Client API reference) — blocking the save
Caution
A notification set without a uniqueId is a notification that a later handler has no handle on, and the cited claim above puts the save behind it.
Guidance
Give each notification an id derived from the column and the rule that raised it, so the handler that fixes the condition can clear the message that rule set and leave the others alone.
5.4The asynchronous OnLoad event
Promises, the per-promise bound, and what the form waits for
A handler that has to fetch something before the form is usable used to mean firing a request and letting the form render behind it. The asynchronous OnLoad event changes that: return a promise from your handler and the event becomes asynchronous, and the form waits.
The OnLoad event becomes asynchronous when a handler returns a promise, and the platform waits at most 10 seconds for each promise before treating it as timed out; the bound is applied per promise, so five promises returned give a total wait of 50 seconds. The event waits for one promise per handler, and the behaviour has to be turned on through the app's Async onload handler setting.citedsource: Power Apps docs — Form OnLoad event — Asynchronous OnLoad event handler support
The bound is per promise rather than on the form load as a whole, and handlers accumulate.
Controls on a form may not be ready when the form's OnLoad event occurs; a control's own OnLoad event is what a handler waits on instead.citedsource: Power Apps docs — Form OnLoad event — Asynchronous OnLoad event handler support
Guidance
Where several handlers on a form each need to wait, combine them into a single handler returning a single promise that wraps the rest. That is the documented recommendation, and it leaves your longest wait at a single timeout instead of the sum of them.
5.5The supported-API boundary
What the contract covers, and what an update does to the rest
The Client API reference is a contract, and §5.3 sets out the part of it your form handler works through. Code you write against it is expected to keep working across the platform's release waves; code you write against anything else is not.
Interaction with application pages is to be performed through the documented Client API methods, direct access to the Document Object Model of a model-driven app page is not supported, and the use of jQuery in form scripts and commands is not recommended. Modifications made outside the documented methods are not preserved during updates or upgrades, and reuse of the platform's own JavaScript may change or be overwritten at an upgrade.citedsource: Power Apps docs — Get started with customization using code — the DOM and unsupported customizations
A selector that finds a field's input element runs correctly against the rendering you wrote it for. It breaks when the platform renames a class or re-nests an element, and the platform does that without notice because nobody promised you otherwise.
Scripts that interact with DOM elements found in the web application do not work in tablet forms, because the same DOM elements are not available there.citedsource: Power Apps docs — Design considerations for main forms — Form presentation differences
DOM-dependent script tends to fail in bursts. It works for a long time and then several pieces fail together, because they were written against the same rendering assumption and one wave update invalidates it at once.experientialbasis: two implementations where DOM-dependent form script survived several waves and then broke in the same week on unrelated forms
5.6Command-bar script compared with Power Fx commanding
Two ways to put behaviour behind a button
A command bar button runs either a Power Fx formula or a JavaScript function, and its visibility comes from either a Power Fx formula or a classic rule. The two authoring surfaces sit side by side in the command designer and are not equivalent.
JavaScript is supported with both classic and modern commands, and Power Fx expresses both a command's action and its visibility while not being supported in classic commands. Calling more than one JavaScript library, or more than one function, from a single command is not supported.citedsource: Power Apps docs — Customize the command bar — Use JavaScript for actions
Power Fx commands do not run within the Dynamics 365 app for Outlook or within a model-driven app hosted in a Portal; Dataverse is the sole data source available to them, and not every Power Fx function is supported for a command.citedsource: Power Apps docs — Command bar customization limitations — where Power Fx commands do not run
Getting the form context for a JavaScript function behind a ribbon action is different from getting it in form scripting, and the parameters a command passes are declared on the command rather than supplied by the platform as they are to a registered form handler (§5.2).citedsource: Power Apps docs — Client API form context — the form context in ribbon actions
Guidance
Put a command's condition in Power Fx and its work in JavaScript when the condition asks about data and the work calls something outside Dataverse. A visibility formula is legible in the designer; a visibility rule buried in your library is not.
5.7Visibility in canvas Power Fx
The same requirement, expressed as a property
A custom page or an embedded canvas app expresses the same requirements as a form script, and expresses them differently. You get no handler and no event: a control's appearance is a property, and the property holds a formula the runtime re-evaluates as its inputs change.
Visible decides whether a control appears or is hidden, and DisplayMode takes Edit, View or Disabled — configuring whether the control accepts user input, displays data alone, or is disabled.citedsource: Power Apps docs — Core properties — Visible and DisplayMode
!IsBlank(TypeSelector.Selected)
The three surfaces therefore express one requirement three ways: a business rule sets visibility as a declared action (§5.1), a form script sets it by calling a method in an event (§5.3), and Power Fx states it as a property whose value is a formula.
Write across a model-driven app and its custom pages and you will write all three, and two of them are imperative while the third is not. Logic gets ported to a custom page as a translation of the script it replaces, one method call at a time, and you end up with a page carrying a variable and an OnVisible block where a property formula would have done.experientialbasis: custom pages added to existing model-driven apps, where visibility logic already existed as form script on the record the page summarised