xrmverse
Documentation menu

Model-driven apps

← xrmverse documentation

xrmverse opens the model-driven apps of an imported solution in its own app shell: an app picker, the app's site map as navigation, and the app's views as read-only grids. The shell is part of the same single-page app as the web admin (ui/), uses the same sign-in, and reads everything through the public Web API, so security, field security and the query engine apply as for any other client.

This is stages UI-1a (shell and views), UI-1b (forms) and UI-1c (web resources and form scripts) of the classic renderer (plan 3.5, spec §5.4). PCF controls, custom commands and dashboards come in later stages (see Stages still to come). Whatever a stage does not deliver yet opens a placeholder that names the stage, never a blank page, and a Client API member it does not implement throws an error that names the member and the stage.

What UI-1a delivers

  • Apps from solutions. Importing a solution that contains an AppModule fills appmodule (client type, form factor, navigation type, web resource, version, publisher and the other standard columns), appmodulecomponent (the app's tables, forms, views, charts and its site map) and appmoduleroles (the security roles the app is shared with). A re-import replaces the components and roles with those of the file. The export round trip of <AppModule> is unchanged.
  • App picker at /apps: a card per active app the user may open, sorted by name.
  • App shell at /main.aspx: a header with the xrmverse logo and name, the app's name, the user and Sign out; the site map on the left; the page in the middle.
  • Site map navigation: areas (an area switcher when there are several), groups and subareas, trimmed by the user's privileges.
  • Views: the public and personal views of a table, a view selector, the view's columns and widths, its order, a header-click sort, paging with first, previous and next, and quick find.
  • Server messages and tables the shell uses: RetrieveUserPrivileges (SDK, SOAP and the Web API bound function systemusers({id})/Microsoft.Dynamics.CRM.RetrieveUserPrivileges()), the userquery table (personal views), the userentityuisettings table (the last form used), and usersettings.uilanguageid and usersettings.paginglimit.

URLs

URL What it opens
/apps The app picker.
/main.aspx?appid={id} The app's home: the first subarea of the site map that the user may see and that opens inside the app.
/main.aspx?appid={id}&pagetype=entitylist&etn={logicalname} The table's list, in its default view.
…&pagetype=entitylist&etn={logicalname}&viewid={id}&viewType=1039 A public view (savedquery).
…&pagetype=entitylist&etn={logicalname}&viewid={id}&viewType=4230 A personal view (userquery).
…&pagetype=entityrecord&etn={logicalname}&id={id}[&formid={id}] A record, on its main form (see Forms). Without id the form opens in create mode.
…&pagetype=dashboard&id={id} A dashboard: placeholder "Dashboards open in stage UI-1f."
…&pagetype=webresource&webresourceName={name}[&data={data}] An HTML web resource, full size inside the app (see HTML web resources).
…&pagetype=custom&name={name} A custom page: placeholder "Custom pages are not planned."

main.aspx and its parameters are the documented URL interface of model-driven apps, so a bookmark or a link from an email keeps working after a migration. Parameter names are read in any case, GUIDs with or without braces ({…} or %7b…%7d). /main.aspx without appid goes to /apps. An app that does not exist, or that the user may not open, shows "This app does not exist or you do not have access to it." with a link to /apps. Any other pagetype shows Pages of type "{pageType}" are not available in this version. A subarea with an external URL opens in a new tab.

Who sees which app

  • A System Administrator sees every app.
  • Any other user sees an app when one of the app's roles (appmoduleroles) is among the user's roles, assigned directly or through a team the user belongs to.
  • An app shared with no role is shown to System Administrators only.
  • A user with no app is told "No apps are available to you. Ask an administrator to give you a security role of an app."

The shell reads appmodules, the caller's systemuserroles, teammembership and teamroles, the roles (to recognise System Administrator by its role template) and the appmoduleroles of the caller's roles, and joins them in the browser. Every user can read these: Basic User has Read on appmodule, appmodulecomponent, sitemap and savedquery, and the intersect tables are readable by every member. Opening an app is not a security boundary of its own: the data behind the app is protected by the user's roles, as in Dataverse.

Site map trimming

The site map comes from the app's sitemap row (sitemapxml); the user's privileges come from RetrieveUserPrivileges.

  • A subarea with an Entity is shown when the user holds prvRead{SchemaName} of that table at any depth.
  • Every <Privilege Entity="…" Privilege="Read,Write"/> child of a subarea must hold too (prvRead… and prvWrite…).
  • A subarea of a table the organization does not have is hidden.
  • A group, or an area, left empty is hidden.
  • Subareas without an Entity (a dashboard, a web resource, an external URL) are shown, and open their placeholder or URL. A URL opens only when it is http:, https: or relative to the site; any other scheme (javascript:, data:, …) is shown disabled, never as a link.

The selected subarea is marked (aria-current="page"); a record page marks its table's list.

Views

The list of a table (pagetype=entitylist) shows:

  • The view selector. The public views of the table (savedquery, querytype 0, active), sorted by name, then a separator and the user's personal views (userquery, querytype 0, active), sorted by name. A user without prvReadUserQuery sees the public views only.
  • The view that opens. The URL's viewid when it is one of the listed views; else the view marked default (isdefault); else the first public view by name; else the first personal view. Choosing another view puts its id and viewType in the URL and starts it over: page 1, its own order, no search.
  • The grid. The LayoutXml <cell name width> columns in their order, widths in pixels; hidden cells are not drawn. A cell named alias.attr reads the column of that link-entity. The header label is the column's display label in the user's language. Values are the server's formatted values; a lookup shows the related record's name, never its GUID. A column hidden by field-level security is empty.
  • Order. The view's FetchXML <order>. A header click sorts by that column ascending, a second click descending; the grid goes back to page 1. As in Dataverse, a lookup column sorts by the related record's primary name, and a choice (or Yes/No) column by its label, unless the view's FetchXML sets useraworderby="true".
  • Paging. The page size is the user's usersettings.paginglimit: 25, 50, 75, 100 or 250 (any other value, or none, gives 50). Paging uses the FetchXML paging cookie, with first, previous and next. The range reads "1 - 50 of 213" when the server returns the total, and "1 - 50" otherwise.
  • A row click (or Enter on the first cell's link) opens the record URL (see Forms).

The rows come from GET {entityset}?fetchXml=… with Prefer: odata.include-annotations="*".

Quick find

The search box ("Search this view") uses the table's quick find view (querytype 4: the default one, else the first by name). Enter runs it; clearing the box, or Enter on an empty box, goes back to the selected view.

  • The conditions of the filters marked isquickfindfields="1" get the search text. For like and not-like, % is appended unless the text ends with *, and a leading * becomes % (a "contains" search). begins-with and not-begin-with get the text as typed, so a * there is matched literally (Dataverse treats * as a wildcard in quick find; see Known limits). Numeric operators get the text when it is a number, and are dropped otherwise.
  • A like, not-like, begins-with, not-begin-with, ends-with or not-end-with condition on a lookup column matches the related record's primary name, as in Dataverse (for a Customer or Owner lookup, the name of whichever table the row points to). A row with an empty lookup matches neither like nor not-like.
  • The result shows the quick find view's columns, with its own sort and paging.
  • A table without a quick find view has no search box.

Languages

Labels follow the user's usersettings.uilanguageid:

  • Site map titles use the <Title LCID> of that language, else 1033, else the first title; a subarea without a title shows its Id.
  • Column headers use the display label in that language, else 1033.
  • The shell's own strings ship in English and Italian (1040); any other language shows English.

The language applies to the app picker and the app shell only. The web admin under /manage stays in English, and the shell strings go back to English when the user leaves the app shell.

Look

The shell uses Fluent UI React (MIT) with Fluent icons, the xrmverse "Indigo" brand theme and the system font stack. The header shows the xrmverse logo and name. Nothing is taken from Power Apps: no script, style sheet, image, icon or font. The layout is the familiar one (site map on the left, grid in the middle); the identity is xrmverse's. The web admin uses the same theme.

Forms

pagetype=entityrecord opens a record on a main form, and without id the empty create form. The page is built from the FormXml the customer wrote: the tabs, columns, sections and cells are their design, the look is xrmverse's. Everything is read and written through the public Web API, so privileges, field security and plug-ins apply as for any other client.

Which form

  • The form selector lists the table's main forms (type 2) that are active (formactivationstate 1). When the app names at least one main form of the table among its form components (componenttype 60), only the app's forms are listed; otherwise every main form is. (Quick view forms listed by an app do not count: they would hide every main form.)
  • A form whose FormXml has <DisplayConditions> with <Role Id> children is listed only for users holding one of those roles. A form without them, or with <Everyone/>, is listed for everyone, and a System Administrator sees every form. A form marked FallbackForm="true" is also listed for a user who holds none of the roles any form of the table names.
  • The order is DisplayConditions/@Order, then the default form (systemform.isdefault), then name.
  • The form that opens is the URL's formid when it is listed (else the first one listed, with the notice "The requested form is not available. The default form opened instead."); without formid, the user's last form of the table when it is still listed, else the first one listed. The form bar's selector switches form and keeps the record.
  • Last form used. A form opened through the URL's formid (the form selector and a script's formSelector navigate that way) becomes the user's last form of the table, stored in userentityuisettings (see Tables in every organization) as Dataverse does, so it follows the user to another browser. Reading or storing it is best effort: a failure is logged in the browser console and the form opens anyway.
  • With no form listed the page says "No form of this table is available in this app."
  • A record that does not exist, or that the user may not read, shows "This record does not exist or you do not have access to it." with a link back to the list.

Layout

  • Tabs are a tab list in order (visible="false" ones are not drawn; the first visible tab opens; arrow keys, Home and End move between tabs). The OpenProject365 Project main form has eight: Summary, WBS, Gantt, Costs, Time, Materials, Purchasing and Team.
  • A tab's columns sit side by side at their percentage widths and their sections stack. A section has its label, its columns count (a cell spans colspan of them) and its label position. Invisible sections and cells are not drawn, and a cell with no control keeps its space. rowspan is not drawn.
  • A cell's label is its label in the user's language, else 1033, else the column's display label; showlabel="false" hides it.
  • The header cells are drawn in a strip under the title and are editable inline (Power Apps edits them in a flyout: same values, same save). The footer cells are drawn read-only.
  • A form with <formLibraries> and <events> runs its scripts (see Form scripts).

Controls

The control comes from the column's metadata; the classid only picks the non-field controls (a real form often carries an old classid on a plain column, so a column asking for another presentation, such as option buttons for a Yes/No column, is drawn with the standard control).

  • Columns: single line and multiple lines of text, whole number, decimal, float, currency (with the record's currency symbol), date only and date and time, choice, multi-select choice, two options, lookup with search (one target or several, owner and customer included), status reason (only the reasons of the record's statecode), file and image. statecode is read-only. Dates are shown and saved in UTC, as the Web API returns them.
  • A column is required when its level is SystemRequired or ApplicationRequired: the label is marked and Save is blocked with "{label}: a value is required." next to the field and in the form notification. Recommended columns get no marker.
  • A column is read-only when its control has disabled="true", when it is not valid for create or for update, when field security denies the write, or when the whole form is read-only: the user lacks write access to the record (RetrievePrincipalAccess), and the form says "You do not have permission to edit this record."
  • A new record starts with the current user as Owner and, for a money record, the user's default currency, else the organization's base currency (when the form carries the column). A value the user enters before a default arrives is kept.
  • Non-field controls: subgrid, quick view, timeline ("The timeline is not available in this version."), web resource and IFrame (an IFrame in the cell: see HTML web resources), and a column with a PCF control, which keeps its standard control and says "Custom controls open in stage UI-1d." under it. An unrecognised control is drawn as an explicit placeholder.

Subgrids and quick views

  • A subgrid shows the rows of the relationship it names (a one-to-many by the referencing column, a many-to-many through its intersect table) with the view it names, the view's columns, paging (RecordsPerPage, default 4 as Dataverse's) and a row click that opens the related record. On the create form it says "Save the record to see related records."; a relationship or view the organization does not have says "This subgrid cannot be shown: {reason}." and leaves the rest of the form alone.
  • A subgrid (and a Related tab item) has New {table} and Add Existing {table} above its rows (#118):
    • New needs prvCreate on the related table. It opens the related table's create form. For a one-to-many, the relationship's lookup is set to this record through extraqs ({lookup}, {lookup}name, {lookup}type). For a many-to-many, the new row is associated with this record after its first save. After that save, the form comes back to this record and the subgrid shows the new row. The create form says "Mapped fields from the parent are not filled (not supported yet)." (#143). If the relationship's lookup is not on the related form, the parameter is reported as ignored, as any form parameter.

    • Add Existing needs prvAppend on the related table and prvAppendTo on this table. It opens a search of the related table, where the user picks one or more rows:

      • for a one-to-many, the search lists rows of no record or of another record, and each chosen row's lookup is PATCHed ({nav}@odata.bind);
      • for a many-to-many, each row is associated with POST …/{nav}/$ref, and a row already associated counts as done.

      A failure stays in the dialog.

  • The Related tab follows the form's own tabs when the FormXml has a <Navigation> with items. It lists the items of NavBar in Sequence order (items with Show="false" are left out) and shows one at a time. A NavBarByRelationshipItem lists the related rows as a subgrid of 10 rows: the table on the other side of the relationship, in its associated view (querytype 2), else its default public view, else its first public view. A relationship that does not relate the form's table says so. Any other item (NavBarItem, a link to a page or a web resource) is listed and says "{label} is not available here", instead of being left out. Scripts cannot select the Related tab.
  • A quick view shows the related record's quick view form (type 6, named in QuickForms for the lookup's target) read-only. An empty lookup, or a target with no quick form in the parameter, shows nothing.

Commands

The form's command bar shows, in this order, New, Save, Save & Close, Delete, Activate or Deactivate, Refresh, Assign and Share. A command the user may not use is hidden, not disabled (custom enable and display rules come in UI-1e).

  • New (create privilege) opens the empty form of the same table and form. The list's + New does the same for users with the create privilege.
  • Save is enabled when something changed. Create POSTs and then puts the new id in the URL; update PATCHes only the changed columns. A server error (plug-in, duplicate detection) is shown as "{code}: {message}" in the form notification and the values stay. Save & Close saves and returns to the list the user came from.
  • Delete asks first, then returns to the list. Refresh reloads the record (and the subgrids), asking first when there are changes.
  • Deactivate (an active record, statecode 0) and Activate (an inactive one) exist on tables whose statecode has at least two states, for users with write access. The dialog lists the status reasons (statuscode) of the target state with its DefaultStatus selected; a table with several inactive states (an activity: Completed, Canceled) also asks which state. The change is one PATCH of statecode and statuscode, asking first when there are changes, and the record is read again. An inactive record opens read-only with the notice "Read-only: this record's status is {state}."; only New, Delete, Activate, Refresh, Assign and Share remain, and getFormType() returns 4 (Disabled).
  • Assign (user- and team-owned tables, with assign access) opens a dialog to pick a user or a team; the record is then read again, because its owner and the user's rights changed.
  • Share (with share access) lists the current shares (RetrieveSharedPrincipalsAndAccess) and lets the user add a user or a team with Read, Write, Delete, Append, Append To, Assign and Share (GrantAccess), and change (ModifyAccess) or remove (RevokeAccess) a share.
  • Unsaved changes. Leaving a changed form (site map, list, subgrid row, form selector, Refresh, Assign, Activate, Deactivate, browser back inside the app) asks "Discard your changes?"; closing the tab asks through the browser's own prompt.
  • Auto-save (#117) follows the organization's isautosaveenabled (on by default, as in Dataverse; a System Administrator turns it off on the web admin Environment page). With it on, an existing record with changes is saved 30 seconds after the last change, and when the user leaves the form through the app, instead of the "Discard your changes?" prompt. The onsave handlers run first with save mode 70 (AutoSave); preventDefault() stops that save. A create form never saves by itself. A prevented or failed auto-save keeps the changes and shows its error; leaving then asks "Discard your changes?", and the timer starts again with the next change (no retries). Nothing is auto-saved while another request of the form runs. The footer says "Unsaved changes", "Saving..." or "Saved". Closing the browser tab is not an auto-save: the browser's own prompt asks.

Server messages added in UI-1b

RetrieveAppComponents, RetrieveUnpublished (returns the published row: xrmverse has no unpublished layer), and over the Web API RetrievePrincipalAccess, RetrieveSharedPrincipalsAndAccess, GrantAccess, ModifyAccess and RevokeAccess. AccessRights is the comma-separated string of Learn (ReadAccess, WriteAccess, …). A user's or team's rights on a record come from the existing access check (privileges, depth, shares, team shares, hierarchy security). systemform has the new columns isdefault and formactivationstate, filled by solution import. Elastic tables refuse RetrievePrincipalAccess with 400.

Web resources

A solution's web resources (webresource rows, filled by solution import) are served by the host, so that a form library, an HTML page, a style sheet or an image keeps the address it has in Dataverse.

URL What it answers
GET /WebResources/{name} The web resource whose name is the whole path after /WebResources/ (op365_/scripts/account.form.js), matched in any case.
GET /%7b{anything}%7d/WebResources/{name} The same, under the cache-busting prefix Dataverse puts in its links; the prefix is ignored.
GET /ClientGlobalContext.js.aspx xrmverse's own short script for HTML web resources: it defines GetGlobalContext() from the app that hosts the page (its parent, or the opener of a window opened by openWebResource) and sets window.Xrm when the page has none. Outside the app, GetGlobalContext() throws. No Microsoft script is served or copied.
  • Content type from webresourcetype: 1 HTML text/html, 2 CSS text/css, 3 script application/javascript, 4 XML, 9 XSL and 12 RESX text/xml, 5 PNG, 6 JPG, 7 GIF, 10 ICO image/x-icon, 11 SVG image/svg+xml; text types carry ; charset=utf-8. Type 8 (Silverlight, XAP) is refused with 415: no browser runs it.
  • Caching. ETag is weak and follows modifiedon; If-None-Match answers 304. Cache-Control: private, no-cache, so the browser asks again every time and a re-imported library is never stale. X-Content-Type-Options: nosniff.
  • Security. The row is read as the caller, through the same pipeline and Read check as a Web API retrieve. A web resource that is missing, or that the caller may not read, is a 404. Basic User reads webresource.
  • Authentication. A <script src>, an IFrame src and a page's own fetch(…, { credentials: "same-origin" }) cannot send a bearer header. The app shell therefore keeps a same-origin session cookie (xrmd.session) that carries the user's access token: HttpOnly, SameSite=Strict, accepted only on /api/data/…, /WebResources/… and /ClientGlobalContext.js.aspx, with a CSRF check on writes. That path list limits what a request from another site could do with the cookie; it does not limit what a web resource can do. Form libraries and HTML web resources run on the shell's own origin, so they can reach the tab's token and call any endpoint as the user, as in Dataverse: treat them as trusted code (see "Web resources are trusted code" in Security, with the cookie's rules under "Web session cookie"). When the cookie cannot be set, the shell shows a warning bar with the server's reason, because form scripts and web resources fail without it.

Form scripts

A main form's <formLibraries> and <events> run as they do in Dataverse; OpenProject365's form scripts run unchanged.

  • Script host. Each open form gets a hidden, same-origin IFrame of its own (about:blank). The form's libraries load into it in FormXml order as <script src="/WebResources/{name}">, so a library's globals (var OP365 = …) belong to that form. Leaving the form (another form, record or app) disposes the IFrame: the old form's handlers and timers stop, and the next form starts with fresh globals.
  • Events.
    • onload handlers run once the record and its metadata are on the form and the form's libraries are loaded. They run again after the Refresh command, after Assign (which reads the record again), and after the first save of a create form (the form becomes the new record's form). formContext.data.refresh() reads the record again without running OnLoad, as in Dataverse.
    • The formContext exists from the moment the form opens, before its libraries finish loading: Xrm.Page (and parent.Xrm.Page from a web resource on the form) is the form at once, also when a library or the script host fails.
    • onsave handlers run before Save and Save & Close, and before a save a script starts with formContext.data.save(). getEventArgs() gives getSaveMode() (1 Save, 2 Save & Close, 70 auto-save; data.save({ saveMode }) passes its own mode), preventDefault() and isDefaultPrevented(). A prevented save does not run.
    • onchange handlers of a column run after a user change (once the user leaves a text or number field with a new value, at once for a choice, a lookup or a date), and after fireOnChange(). A script's setValue does not raise OnChange, so a handler that sets its own column does not loop.
  • Handlers. Handlers of one event run in order. enabled="false" handlers are skipped; passExecutionContext="true" passes the execution context first; parameters is read as a JSON list and passed after it.
  • Errors never break the form. Each failure is a form notification at level error, and the form stays usable (Save still works):
    • a handler that throws: "Script error in {functionName}: {message}". The next handler still runs;
    • a function that is not there: "Script error: {functionName} was not found in {libraryName}.";
    • a library that does not load (404 or a syntax error): "Script error: the library {name} could not be loaded.";
    • parameters that cannot be read: "Script error in {functionName}: the parameters could not be read ({message}).".
  • Promises are not awaited. A promise returned by an OnLoad or OnSave handler is not waited for, as in Dataverse without the app setting for asynchronous handlers (#129).
  • Notifications. setFormNotification shows in the form's notification area with its level (error, warning, info) until clearFormNotification. A control notification (setNotification, addNotification) shows under its field and blocks Save while it is there.
  • Xrm is set on the app shell's window and in each script host. Xrm.Page is the current form's formContext (with Xrm.Page.context, the global context), also on a form without scripts.

Client API

The Client API is reimplemented from its public documentation: names, signatures, enum values and return shapes follow Learn. Only the documented, supported members exist. A documented member that this version does not implement throws {member} is not available in this version (stage {stage}).; a promise-based one rejects with that error. It is never undefined and never a silent no-op. An undocumented member (Xrm.Internal, Mscrm.*, Xrm.Page.context internals) stays undefined, as in Dataverse, and scripts that work on the Power Apps page structure are not supported.

Namespace What works
Execution context getFormContext(), getEventSource(), getDepth(), getEventArgs() (OnSave), getContext() (the global context), getSharedVariable() and setSharedVariable().
formContext getAttribute(…) and getControl(…) (by name, index, predicate or none; null for an unknown name), data, ui.
formContext.data entity, attributes, getIsDirty(), isValid(), save(saveOptions?) and refresh(save?) (promises).
formContext.data.entity getId() ({GUID} upper case, "" on create), getEntityName(), getEntitySetName(), getEntityReference(), getIsDirty(), getPrimaryAttributeValue(), isValid(), save() and save("saveandclose"), attributes, addOnSave, removeOnSave, addOnPostSave, removeOnPostSave.
Attributes getName(), getAttributeType(), getFormat(), getValue(), setValue(), getIsDirty(), getInitialValue(), getRequiredLevel(), setRequiredLevel(), getSubmitMode(), setSubmitMode(), addOnChange(), removeOnChange(), fireOnChange(), controls, getParent(); choices: getOptions(), getOption(), getText(), getSelectedOption(); numbers: getMax(), getMin(), getPrecision(); text: getMaxLength(); lookups: getIsPartyList(). Values have Learn's shapes: a lookup is [{ id, name, entityType }] or null, a choice a number, a multi-select choice an array, a date a Date.
Controls getName(), getControlType(), getAttribute(), getLabel(), setLabel(), getVisible(), setVisible(), getDisabled(), setDisabled(), setNotification(), clearNotification(), addNotification(), setFocus(), getParent(); lookups: addPreSearch(), removePreSearch(), addCustomFilter() (inside a PreSearch handler), getEntityTypes(), setEntityTypes(); choices: addOption(), removeOption(), clearOptions(); subgrids: refresh(), getEntityName(); web resource and IFrame: getSrc(), setSrc(), getInitialUrl(), getObject() (the IFrame element), getContentWindow(). Header controls are named header_{column}, footer controls footer_{column}, a second body control of a column {column}1.
formContext.ui getFormType() (1 create, 2 update, 3 read-only), setFormNotification(), clearFormNotification(), close(), refreshRibbon() (does nothing until UI-1e), setFormEntityName(), tabs (name, label, visibility, getDisplayState(), setDisplayState(), setFocus(), sections), sections (name, label, visibility, controls), controls, formSelector (getCurrentItem(), items with getId(), getLabel(), navigate(), getVisible(), setVisible()).
Xrm.WebApi (and .online) createRecord, retrieveRecord, retrieveMultipleRecords (OData options or ?fetchXml=, maxPageSize, nextLink), updateRecord, deleteRecord, execute (custom and standard actions and functions, bound and unbound, and CRUD requests, from getMetadata()), executeMultiple (one by one), isAvailableOffline() (false). A failure rejects with { errorCode, message, code }.
Xrm.Navigation openForm (with form parameters), navigateTo (entityrecord and entitylist inline, webresource inline or in a dialog), openAlertDialog, openConfirmDialog, openErrorDialog, openUrl (http and https only), openWebResource.
Xrm.Utility getGlobalContext() (userSettings, organizationSettings, getClientUrl(), getCurrentAppUrl(), getCurrentAppName(), getCurrentAppProperties(), getVersion(), prependOrgName(), getQueryStringParameters()), showProgressIndicator(), closeProgressIndicator(), lookupObjects(), getEntityMetadata().
HTML web resources parent.Xrm and parent.Xrm.Page from a page in a form, a page or a dialog; GetGlobalContext() after ClientGlobalContext.js.aspx; the data query parameter; window.close() in a dialog.

The dialogs (alert, confirm, error, lookup) and the progress indicator are the shell's own Fluent UI components with the xrmverse theme; their strings ship in English and Italian. The progress indicator counts: each showProgressIndicator needs its closeProgressIndicator. Leaving the form (another form, record or page) closes it whatever the count, so a handler that threw before closing it does not leave the overlay over the next page.

What throws "not available" (stage after UI-1 unless noted):

  • Xrm.App, Xrm.Copilot, Xrm.Device, Xrm.Encoding and Xrm.Panel, as soon as they are read (#132); Xrm.WebApi.offline; Xrm.Navigation.openFile.
  • Xrm.Utility.getAllowedStatusTransitions, getEntityMainFormDescriptor, getLearningPathAttributeName, getPageContext, getResourceString, invokeProcessAction, and refreshParentGrid (UI-1e).
  • On the global context: client, getAdvancedConfigSetting, getWebResourceUrl, isOnPremises; the user settings dateFormattingInfo, defaultDashboardId, isGuidedHelpEnabled, isHighContrastEnabled, securityRolePrivileges, transactionCurrency, transactionCurrencyId and getSecurityRolePrivilegesInfo; the organization settings attributes, baseCurrency, isTrialOrganization, organizationExpiryDate, useSkypeProtocol and fullNameConventionCode.
  • formContext.data.process, ui.process, ui.navigation, ui.quickForms (business process flows and the rest are out of UI-1), data.addOnLoad, ui.addOnLoad and their remove, ui.getViewPortHeight, getViewPortWidth, data.entity.getDataXml (Learn does not document its format) and save("saveandnew").
  • Attributes: getUserPrivilege, isValid, setIsValid, setPrecision, and getAttributeType for a column type Learn does not list. Controls: lookup addCustomView, getDefaultView, setDefaultView, addOnLookupTagClick; tab addTabStateChange, getContentType, setContentType; IFrame addOnReadyStateComplete; subgrid getGrid and the grid members (UI-1e); PCF getOutputs (UI-1d); the notification members of a control that is not a field.
  • getEventArgs().preventDefaultOnError and disableAsyncTimeout.
  • Options that would change the result: openForm or navigateTo with useQuickCreateForm, createFromEntity, relationship, a process or stage, tabName; navigateTo with an entity record in a dialog (#133) or with dashboard, custom or generative pages; executeMultiple with a change set (#130); lookupObjects with a view that filters on a related table.

HTML web resources

  • On a form. A web resource or IFrame control becomes an IFrame in its cell. A web resource shows /WebResources/{name} with the control's data; an IFrame shows its Url, which must be http or https (any other scheme shows a message instead). When the control passes the record's parameters (PassParameters), the address gets id (empty until the record is saved), typename, orgname, userlcid and orglcid. A same-origin page reaches parent.Xrm and parent.Xrm.Page; a page of another origin is sandboxed (allow-scripts allow-forms allow-popups) and cannot; the control's Security option (restricted cross-frame scripting) sandboxes any page completely. A missing web resource shows "The web resource {name} does not exist or you do not have access to it."
  • As a page. main.aspx?…&pagetype=webresource&webresourceName={name}&data={data} (and navigateTo with target: 1) shows the page full size inside the app shell, with data in its query string.
  • As a dialog. navigateTo with target: 2 opens it in a dialog with the given title, size (pixels or %) and position (centre or side). The page closes the dialog with window.close(), as it does in Dataverse, and the promise resolves then; the dialog's Close button and Esc do the same. A page shown inline or on a form cannot close anything outside itself.
  • Xrm.Navigation.openWebResource opens the page in a new window, which reaches the app through window.opener.

Stages still to come

Stage What it adds
UI-1b (done) Forms: tabs, sections, standard controls, subgrids, quick views, standard commands, the Assign and Share dialogs; RetrieveAppComponents, RetrievePrincipalAccess and RetrieveUnpublished.
UI-1c (done) Web resources (/WebResources/{name}, the session cookie), form scripts (OnLoad, OnSave, OnChange) and the Client API (formContext, Xrm.WebApi, Xrm.Navigation, Xrm.Utility); HTML web resources on forms, as pages and as dialogs.
UI-1d Field PCF controls (customcontrol).
UI-1e Dataset PCF controls and custom commands from RibbonDiffXml.
UI-1f Dashboards and charts (savedqueryvisualization).

Out of UI-1 (declared in the spec): business process flows, client-side business rules, editable grids, offline, canvas apps and custom pages.

Known limits

Each of these is either refused with an error or stated here; none is a silent difference.

  • RetrieveAppComponents, RetrievePrincipalAccess and RetrieveUnpublished are available since UI-1b, to the SDK, SOAP and Web API callers and to plug-ins. A plug-in's RetrievePrincipalAccess and RetrieveSharedPrincipalsAndAccess run in its own transaction, outside the pipeline (no plug-in step fires on them), and see the rows it has not committed yet.
  • No N:N appmoduleroles_association and no collection-valued $expand. appmoduleroles is a plain intersect table, read as an entity set (appmoduleroles?$filter=…); appmodules?$expand=appmoduleroles_association is a 400. The shell joins apps, app roles and the user's roles in the browser, which costs a few extra requests when the app picker loads.
  • appmodulecomponent and appmoduleroles are import-only. A Create, Update, Delete or Assign through the API is a 400 0x80040203 "App modules are created by solution import in this version." appmodule itself stays writable through the API, as in Dataverse: deleting an app deletes its components and its role links with it, and deleting a role removes its app links. appmodulecomponent is not change-tracked (like savedquery and systemform).
  • A role or component the organization lacks is a warning, not a failure. The solution import writes a warning in the import job's log for each missing role, and for each table or site map component it cannot resolve (for example a table the organization does not have), and imports the rest. Dataverse may refuse such an import. View, chart, form and business process flow components keep the id of the file without an existence check: a missing one is stored as is and is not logged.
  • Apps are matched to roles by role id. The server does not fill parentrootroleid, so an app shared with a role of the root business unit matches users holding that role row. If roles are ever copied per business unit, a user holding a child unit's copy will not see the app.
  • Apps without roles are shown to System Administrators only; in Power Apps, makers may see them too.
  • Object type codes 9007 (appmodulecomponent) and 9008 (appmoduleroles) are not on Learn and still have to be verified against a real organization (#105).
  • usersettings.paginglimit accepts any integer on the server (Learn gives no list); the shell pages by 25, 50, 75, 100 or 250 and uses 50 for any other value.
  • No sort on a related table's column. The header of a link-entity column is not sortable and its tooltip says "Sorting on a related table's column is not available in this view." (#100).
  • * in a begins-with quick find condition is matched literally. Dataverse treats * as a wildcard in quick find; the OpenProject365 quick find views use like only, where * is handled.
  • A lookup whose targets have no primary name cannot be matched or sorted by name: a like-family condition or an <order> on it is refused with 400 0x80040203. Dataverse tables always have one, so this concerns only odd metadata.
  • Choice order uses the base-language label. A choice is sorted by the label its formatted value shows, which is the organization's base-language label (1033) for every user; Dataverse sorts by the label in the user's language, so with translated labels the order can differ (#109). In a distinct query an order on a choice by label is refused (400): set useraworderby="true" to sort by value. The group-by order of an aggregate query, and the order on a column that names a table (objecttypecode), stay by stored value. OData $orderby sorts a choice by its value, as Learn states for the Web API; the SQL (TDS) endpoint orders every column by its stored value.
  • Related-table column headers show the column's own label; Power Apps adds the related table's name in parentheses (#104). A related-table lookup without a formatted value is shown empty rather than as a GUID. A related-table uniqueidentifier column that is not a lookup (for example the related table's own id) is shown empty as well, a side effect of never showing a GUID.
  • Personal views are read, not created. The grid lists and opens userquery views; saving a view from the grid comes later (#101). The Web API already accepts userquery writes.
  • Language. /manage stays in English, and the shell strings go back to English when the user leaves the app shell. Only English and Italian shell strings ship.
  • Font. The shell uses the system font stack. The xrmverse brand font (Sora) and its third-party notice are tracked in #102; the brand theme itself is under legal review in #11.

Known limits of forms (UI-1b)

  • Auto-save does not run when the tab is closed (Power Apps saves then too); the browser's unload prompt asks instead.
  • New from a subgrid does not fill mapped columns (the relationship's attribute maps, #143): only the lookup is set. There is no quick create form: New opens the main form.
  • Related tab items that are not relationships (NavBarItem: pages, web resources) are listed but not opened.
  • Recommended columns get no marker, only the levels that block saving.
  • Hidden, not disabled. A command without the privilege disappears; custom rules are UI-1e.
  • Header edited inline, not in a flyout.
  • The control follows the column's metadata, so a classid asking for another presentation of a plain column is ignored.
  • rowspan is not drawn. Dates are shown and saved in UTC.
  • Create-time field security is not detectable: a column the user may not create is not marked read-only on the create form. A secured column the server leaves out of the read shows as "no access".
  • PCF controls render as the standard control (UI-1d). Form scripts run since UI-1c (see Known limits of web resources and form scripts).
  • The classids of the quick view, web resource, IFrame and timeline controls, the DisplayConditions, header, footer and QuickForms shapes and the RecordsPerPage default are written from memory of the documented interface; a wrong one shows as an explicit "unknown control" placeholder, never silently. The checklist compares them.
  • A self-referential many-to-many subgrid shows the record side (Entity1) of the intersect.
  • The extra read on appmodule: RetrieveAppComponents checks the caller's read on appmodule and reads the components with system context, as Dataverse treats app components as app metadata.
  • The save error text is "{code}: {message}", not Dataverse's dialog.
  • Form security matches roles by role id. A form whose DisplayConditions name a role is listed for users holding that role row; as for apps, a user holding a child business unit's copy of the role would not match if roles were ever copied per business unit.
  • Sign out in development sign-in mode does not ask about unsaved changes. It signs out at once and the form's changes are lost. Entra and local sign-in leave the page, where the browser's unload prompt asks first.

Known limits of web resources and form scripts (UI-1c)

  • Promises of OnLoad and OnSave handlers are not awaited, as in Dataverse without the app setting for asynchronous handlers; xrmverse does not read that setting yet (#129). A save does not wait for an OnSave promise.
  • executeMultiple runs the requests one by one, never as a change set: the first failure stops the rest and rejects, and the requests already run stay done. A change set (a nested array) throws "not available" (#130).
  • Dates are UTC in the Client API, as on the form: getTimeZoneOffsetMinutes() is 0 and dateFormattingInfo throws "not available" (#131).
  • Xrm.Panel, Xrm.App, Xrm.Device, Xrm.Encoding and Xrm.Copilot throw "not available" when a script reads them (#132).
  • navigateTo cannot open an entity record in a dialog (pageType: "entityrecord" with target: 2): it rejects with "not available" (#133). Inline it opens the record page.
  • ui.refreshRibbon() does nothing until custom commands arrive (UI-1e); the standard command bar already follows the form's state.
  • addNotification shows its messages but not its actions (the quick fixes).
  • getVersion() is 9.2.0.0, the Web API version xrmverse speaks. A script that checks for a later build takes the older branch.
  • getControlType() follows Learn: choice for a choice and choices for a multi-select choice, and standard for a control xrmverse does not recognise (the form already shows an "unknown control" placeholder there). Scripts that compare with an older value take the other branch; the checklist compares the values with a real environment.
  • getAttributeType() throws "not available" for a column type Learn's list does not include, so a script that walks every attribute fails on such a form, with a notification.
  • Header and footer control names (header_{column}, footer_{column}) are written from memory: Learn names only header_process_ controls.
  • getDataXml() throws "not available": Learn does not document the XML it returns.
  • A prevented or failed data.save() rejects with errorCode 0 and xrmverse's own message: Learn documents the shape, not the values.
  • formSelector.getCurrentItem() is null when the table has a single form, as Learn states.
  • Web API values from memory:
    • retrieveRecord, retrieveMultipleRecords, a CRUD retrieve and functions ask for every annotation, so formatted values and lookup names come back;
    • an enumeration parameter of a function is written as Microsoft.Dynamics.CRM.{Type}'{value}';
    • createRecord, updateRecord and deleteRecord return a bare lower-case id, lookupObjects and lookup values {GUID} upper case, userSettings.userId {GUID} upper case, role, organization and currency ids bare lower case;
    • the CRUD request property names of execute come from Learn's samples;
    • retrieveMultipleRecords without maxPageSize asks for 5,000 rows, Learn's default for a standard table.
  • Global context: userSettings.isRTL is true for Arabic, Hebrew, Urdu and Persian (1025, 1037, 1056, 1065: from memory); organizationSettings.languageId is the number 1033 (Learn types it as a string, and xrmverse has no base language column); uniqueName is the organization's name; defaultCountryCode is empty; prependOrgName() returns the path unchanged, because xrmverse serves one organization at the root of its address; getCurrentAppProperties() gives the app web resource and welcome page as null.
  • getEntityMetadata() gives Learn's shape for what xrmverse models (labels in the user's language, Attributes as a collection, OptionSet, the State and Status helpers); a member it cannot fill in Learn's type throws "not available". The multi-select choice DefaultFormValue is a number, where Learn types it as a Boolean.
  • lookupObjects() applies a view's own filters; a view that filters on a related table rejects with "not available" rather than widen the search. A link-entity with no filter in the view is ignored, so an inner join that only restricts rows to those with a related row does not restrict them: the dialog can list more rows than the view does in Dataverse.
  • Scripts get the shell's objects. Xrm belongs to the shell's window, so the promises and errors it returns come from the shell's JavaScript realm, not the library's: inside a library, e instanceof Error is false for an error from Xrm (e.message works). Check typeof e.message rather than instanceof.
  • openForm() resolves once the form opens, with no value. Learn's success callback does not run for a form either; quick create is not offered (it throws "not available").
  • Form parameters (openForm and extraqs) that name no column of the form, or do not fit it, are ignored with a warning on the form that lists them. Learn says they cause an error.
  • openErrorDialog() with only an errorCode shows the code and xrmverse's generic text; Dataverse looks up a message for known codes.
  • IFrame controls: the Data parameter, the Scrolling values and the web resource name in Url are read from memory of the FormXml; the table type code (type) is not passed with the record parameters (Learn recommends typename); getObject() is the IFrame element and getContentWindow() resolves its window, as Learn states; any same-origin Url is not sandboxed, as in Dataverse, so such a page reaches parent.Xrm. The IFrame's OnReadyStateComplete event is not raised (its methods throw "not available").
  • window.close() in a dialog. The shell replaces the page's close as soon as the frame exists and after every load. A script that calls close() in the first milliseconds of parsing, before that, is ignored by the browser and the dialog stays open (its Close button still works).
  • Content types of ICO (image/x-icon), XSL, RESX and XML (text/xml) are xrmverse's choice: Learn lists the types, not their content types.
  • Errors are xrmverse's own text. The "Script error …" notifications and the "not available" messages are xrmverse's wording; only the codes of Web API errors match Dataverse.
  • Scripts that work on the Power Apps page structure (document.getElementById on the form, window.top.document) are not supported, as in Dataverse. Each library runs in its own hidden window, not in the form's page.

See also the manual acceptance checklists of UI-1a, UI-1b and UI-1c, which compare the same app in Power Apps and in xrmverse.