Model-driven apps
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
AppModulefillsappmodule(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) andappmoduleroles(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 functionsystemusers({id})/Microsoft.Dynamics.CRM.RetrieveUserPrivileges()), theuserquerytable (personal views), theuserentityuisettingstable (the last form used), andusersettings.uilanguageidandusersettings.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
Entityis shown when the user holdsprvRead{SchemaName}of that table at any depth. - Every
<Privilege Entity="…" Privilege="Read,Write"/>child of a subarea must hold too (prvRead…andprvWrite…). - 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 ishttp:,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,querytype0, active), sorted by name, then a separator and the user's personal views (userquery,querytype0, active), sorted by name. A user withoutprvReadUserQuerysees the public views only. - The view that opens. The URL's
viewidwhen 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 andviewTypein 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 namedalias.attrreads 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 setsuseraworderby="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. Forlikeandnot-like,%is appended unless the text ends with*, and a leading*becomes%(a "contains" search).begins-withandnot-begin-withget 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-withornot-end-withcondition 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 neitherlikenornot-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 itsId. - 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 (
type2) that are active (formactivationstate1). When the app names at least one main form of the table among its form components (componenttype60), 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 markedFallbackForm="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
formidwhen it is listed (else the first one listed, with the notice "The requested form is not available. The default form opened instead."); withoutformid, 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'sformSelectornavigate that way) becomes the user's last form of the table, stored inuserentityuisettings(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
columnscount (a cell spanscolspanof them) and its label position. Invisible sections and cells are not drawn, and a cell with no control keeps its space.rowspanis 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.statecodeis read-only. Dates are shown and saved in UTC, as the Web API returns them. - A column is required when its level is
SystemRequiredorApplicationRequired: the label is marked and Save is blocked with "{label}: a value is required." next to the field and in the form notification.Recommendedcolumns 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
prvCreateon 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 throughextraqs({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
prvAppendon the related table andprvAppendToon 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.
- for a one-to-many, the search lists rows of no record or of another record, and each chosen row's lookup is
PATCHed (
- The Related tab follows the form's own tabs when the FormXml has a
<Navigation>with items. It lists the items ofNavBarinSequenceorder (items withShow="false"are left out) and shows one at a time. ANavBarByRelationshipItemlists the related rows as a subgrid of 10 rows: the table on the other side of the relationship, in its associated view (querytype2), 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 (
type6, named inQuickFormsfor 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,
statecode0) and Activate (an inactive one) exist on tables whosestatecodehas at least two states, for users with write access. The dialog lists the status reasons (statuscode) of the target state with itsDefaultStatusselected; a table with several inactive states (an activity: Completed, Canceled) also asks which state. The change is one PATCH ofstatecodeandstatuscode, 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, andgetFormType()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. Theonsavehandlers 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 HTMLtext/html, 2 CSStext/css, 3 scriptapplication/javascript, 4 XML, 9 XSL and 12 RESXtext/xml, 5 PNG, 6 JPG, 7 GIF, 10 ICOimage/x-icon, 11 SVGimage/svg+xml; text types carry; charset=utf-8. Type 8 (Silverlight, XAP) is refused with 415: no browser runs it. - Caching.
ETagis weak and followsmodifiedon;If-None-Matchanswers 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 IFramesrcand a page's ownfetch(…, { 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.
onloadhandlers 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(andparent.Xrm.Pagefrom a web resource on the form) is the form at once, also when a library or the script host fails. onsavehandlers run before Save and Save & Close, and before a save a script starts withformContext.data.save().getEventArgs()givesgetSaveMode()(1 Save, 2 Save & Close, 70 auto-save;data.save({ saveMode })passes its own mode),preventDefault()andisDefaultPrevented(). A prevented save does not run.onchangehandlers 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 afterfireOnChange(). A script'ssetValuedoes 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;parametersis 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.
setFormNotificationshows in the form's notification area with its level (error, warning, info) untilclearFormNotification. A control notification (setNotification,addNotification) shows under its field and blocks Save while it is there. Xrmis set on the app shell's window and in each script host.Xrm.Pageis the current form'sformContext(withXrm.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.EncodingandXrm.Panel, as soon as they are read (#132);Xrm.WebApi.offline;Xrm.Navigation.openFile.Xrm.Utility.getAllowedStatusTransitions,getEntityMainFormDescriptor,getLearningPathAttributeName,getPageContext,getResourceString,invokeProcessAction, andrefreshParentGrid(UI-1e).- On the global context:
client,getAdvancedConfigSetting,getWebResourceUrl,isOnPremises; the user settingsdateFormattingInfo,defaultDashboardId,isGuidedHelpEnabled,isHighContrastEnabled,securityRolePrivileges,transactionCurrency,transactionCurrencyIdandgetSecurityRolePrivilegesInfo; the organization settingsattributes,baseCurrency,isTrialOrganization,organizationExpiryDate,useSkypeProtocolandfullNameConventionCode. formContext.data.process,ui.process,ui.navigation,ui.quickForms(business process flows and the rest are out of UI-1),data.addOnLoad,ui.addOnLoadand theirremove,ui.getViewPortHeight,getViewPortWidth,data.entity.getDataXml(Learn does not document its format) andsave("saveandnew").- Attributes:
getUserPrivilege,isValid,setIsValid,setPrecision, andgetAttributeTypefor a column type Learn does not list. Controls: lookupaddCustomView,getDefaultView,setDefaultView,addOnLookupTagClick; tabaddTabStateChange,getContentType,setContentType; IFrameaddOnReadyStateComplete; subgridgetGridand the grid members (UI-1e); PCFgetOutputs(UI-1d); the notification members of a control that is not a field. getEventArgs().preventDefaultOnErroranddisableAsyncTimeout.- Options that would change the result:
openFormornavigateTowithuseQuickCreateForm,createFromEntity,relationship, a process or stage,tabName;navigateTowith an entity record in a dialog (#133) or withdashboard,customorgenerativepages;executeMultiplewith a change set (#130);lookupObjectswith 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'sdata; an IFrame shows itsUrl, which must be http or https (any other scheme shows a message instead). When the control passes the record's parameters (PassParameters), the address getsid(empty until the record is saved),typename,orgname,userlcidandorglcid. A same-origin page reachesparent.Xrmandparent.Xrm.Page; a page of another origin is sandboxed (allow-scripts allow-forms allow-popups) and cannot; the control'sSecurityoption (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}(andnavigateTowithtarget: 1) shows the page full size inside the app shell, withdatain its query string. - As a dialog.
navigateTowithtarget: 2opens it in a dialog with the given title, size (pixels or%) and position (centre or side). The page closes the dialog withwindow.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.openWebResourceopens the page in a new window, which reaches the app throughwindow.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,RetrievePrincipalAccessandRetrieveUnpublishedare available since UI-1b, to the SDK, SOAP and Web API callers and to plug-ins. A plug-in'sRetrievePrincipalAccessandRetrieveSharedPrincipalsAndAccessrun 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_associationand no collection-valued$expand.appmodulerolesis a plain intersect table, read as an entity set (appmoduleroles?$filter=…);appmodules?$expand=appmoduleroles_associationis 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. appmodulecomponentandappmodulerolesare import-only. A Create, Update, Delete or Assign through the API is a 4000x80040203"App modules are created by solution import in this version."appmoduleitself 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.appmodulecomponentis not change-tracked (likesavedqueryandsystemform).- 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.paginglimitaccepts 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 abegins-withquick find condition is matched literally. Dataverse treats*as a wildcard in quick find; the OpenProject365 quick find views uselikeonly, 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 4000x80040203. 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
distinctquery an order on a choice by label is refused (400): setuseraworderby="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$orderbysorts 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
uniqueidentifiercolumn 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
userqueryviews; saving a view from the grid comes later (#101). The Web API already acceptsuserquerywrites. - Language.
/managestays 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. Recommendedcolumns 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.
rowspanis 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 andQuickFormsshapes and theRecordsPerPagedefault 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:RetrieveAppComponentschecks the caller's read onappmoduleand 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
DisplayConditionsname 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.
executeMultipleruns 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 anddateFormattingInfothrows "not available" (#131). Xrm.Panel,Xrm.App,Xrm.Device,Xrm.EncodingandXrm.Copilotthrow "not available" when a script reads them (#132).navigateTocannot open an entity record in a dialog (pageType: "entityrecord"withtarget: 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.addNotificationshows its messages but not its actions (the quick fixes).getVersion()is9.2.0.0, the Web API version xrmverse speaks. A script that checks for a later build takes the older branch.getControlType()follows Learn:choicefor a choice andchoicesfor a multi-select choice, andstandardfor 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 onlyheader_process_controls. getDataXml()throws "not available": Learn does not document the XML it returns.- A prevented or failed
data.save()rejects witherrorCode0 and xrmverse's own message: Learn documents the shape, not the values. formSelector.getCurrentItem()isnullwhen 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,updateRecordanddeleteRecordreturn a bare lower-case id,lookupObjectsand lookup values{GUID}upper case,userSettings.userId{GUID}upper case, role, organization and currency ids bare lower case;- the CRUD request property names of
executecome from Learn's samples; retrieveMultipleRecordswithoutmaxPageSizeasks for 5,000 rows, Learn's default for a standard table.
- Global context:
userSettings.isRTLis true for Arabic, Hebrew, Urdu and Persian (1025, 1037, 1056, 1065: from memory);organizationSettings.languageIdis the number 1033 (Learn types it as a string, and xrmverse has no base language column);uniqueNameis the organization's name;defaultCountryCodeis 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 asnull. getEntityMetadata()gives Learn's shape for what xrmverse models (labels in the user's language,Attributesas a collection,OptionSet, the State and Status helpers); a member it cannot fill in Learn's type throws "not available". The multi-select choiceDefaultFormValueis 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. Alink-entitywith 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.
Xrmbelongs 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 Erroris false for an error fromXrm(e.messageworks). Checktypeof e.messagerather thaninstanceof. 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 (
openFormandextraqs) 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 anerrorCodeshows the code and xrmverse's generic text; Dataverse looks up a message for known codes.- IFrame controls: the
Dataparameter, theScrollingvalues and the web resource name inUrlare read from memory of the FormXml; the table type code (type) is not passed with the record parameters (Learn recommendstypename);getObject()is the IFrame element andgetContentWindow()resolves its window, as Learn states; any same-originUrlis not sandboxed, as in Dataverse, so such a page reachesparent.Xrm. The IFrame's OnReadyStateComplete event is not raised (its methods throw "not available"). window.close()in a dialog. The shell replaces the page'scloseas soon as the frame exists and after every load. A script that callsclose()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.getElementByIdon 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.