Alongside the Web API series, this blog has carried a handful of very short posts over the years — a code snippet, a sentence of explanation, and not much else. Individually they were notes I wrote so I would not have to work the same thing out twice. Together they cover something more useful: the small pieces of JavaScript and metadata plumbing you end up needing on almost every Dynamics 365 form.
I have merged five of those posts into this one, kept the code exactly as it worked, and added the context that was missing the first time round: when to use each one, what breaks, and what has changed now that Xrm.Page is deprecated.
1. Make every field on a form read-only
This comes up whenever a record reaches a state where it should be locked — an approved order, a closed case, a submitted application. Rather than disabling forty fields one at a time in business rules, walk the controls collection and disable everything.
function makeFieldsReadOnly() {
var controls = Xrm.Page.ui.controls.get();
for (var i in controls) {
var control = controls[i];
if (control.getDisabled && control.setDisabled && !control.getDisabled())
control.setDisabled(true);
}
}Three things the original post did not say, and should have.
The guard on control.getDisabled && control.setDisabled is not defensive padding. Not every control on a form supports being disabled — sub-grids, iframes, web resources and timelines do not expose those methods, and calling setDisabled on them throws. The check is what keeps the loop from stopping halfway.
The !control.getDisabled() part means an already-disabled control is skipped. That matters if you later want to re-enable the form selectively, because you have not lost the record of which fields were locked by configuration rather than by this script.
And most importantly: this is a UI lock, not a security control. Disabled fields are read-only in the form, and nowhere else. The record can still be updated through the Web API, through a bulk edit, through a workflow, or by any user who opens it in another client. If the requirement is that the data cannot change, enforce it server-side with a plugin or with security roles and use this only to make the form reflect that.
On version 9 and later, take the execution context instead of reaching for the global:
function makeFieldsReadOnly(executionContext) {
var formContext = executionContext.getFormContext();
formContext.ui.controls.forEach(function (control) {
if (control.getDisabled && control.setDisabled && !control.getDisabled()) {
control.setDisabled(true);
}
});
}Remember to tick Pass execution context as first parameter in the form handler properties, otherwise executionContext arrives undefined and the whole thing fails silently.
2. Filter what a lookup is allowed to show
Out of the box, a lookup shows every active record of the target table. Most of the time the business wants something narrower: only marketing lists of a certain type, only contacts belonging to the account on the form, only active suppliers.
function onPageLoad()
{
var LookupControl = Xrm.Page.getControl("new_marketinglist");
if (LookupControl != null) {
var fetchXml = "<filter type='and'><condition attribute='createdfromcode' operator='eq' value='2' /></filter>";
LookupControl.addPreSearch(function () {
LookupControl.addCustomFilter(fetchXml);
});
}
}The pattern that makes this work is the pairing of addPreSearch and addCustomFilter. addCustomFilter on its own does nothing useful, because the filter has to be applied at the moment the user opens the lookup, not at the moment the form loads. addPreSearch registers a handler that fires immediately before the search runs, and that is where the filter belongs.
Points worth knowing:
- The FetchXML you pass is a bare
<filter>element, not a whole<fetch>document. Passing a full query is the most common reason this silently does nothing. addCustomFiltertakes an optional second argument, the entity logical name, which matters when the lookup can target more than one table — a customer lookup that accepts both accounts and contacts, for example. Without it, the filter is applied to every target and will exclude everything from the tables where the attribute does not exist.- Filters added this way stack with the view's own filter rather than replacing it. If your filter returns nothing, check whether the underlying lookup view is already narrowing the set.
- If the filter depends on another field on the form, read that field inside the
addPreSearchcallback, not outside it. Read it outside and you capture the value as it was on load, which is almost never what the user expects.
3. Autocomplete on a plain text field
Dynamics gives you a lookup or an option set when the values live in the system. Sometimes you want neither — a free-text field that suggests sensible values as the user types, without creating a table to hold them. Years are the classic case.
Call this on form load. It populates the field with the last fifty years as the user types into it.
function year_AutoComplete() {
var keyPressFcn = function (ext) {
try {
resultSet = {
results: new Array()
};
var dt = new Date();
for (i = 0; i < 50; i++) {
resultSet.results.push({
id: i,
fields: [dt.getFullYear() - i]
});
}
if (resultSet.results.length > 0) {
ext.getEventSource().showAutoComplete(resultSet);
} else {
ext.getEventSource().hideAutoComplete();
}
} catch (e) {
console.log(e);
}
};
Xrm.Page.getControl("new_year").addOnKeyPress(keyPressFcn);
}The shape of resultSet is fixed and fussy. Each result needs an id and a fields array; the first entry in fields is the text shown and written back, and you may add a second and third entry to render a subtitle and an icon. Get the shape wrong and nothing appears, with no error.
The obvious extension is to filter the list by what has been typed so far. Read the current value inside the handler with ext.getEventSource().getValue() and only push matching entries; showing fifty suggestions regardless of input is fine for years but poor for anything longer.
Two limitations to be aware of: autocomplete is a control-level feature and does not appear on the mobile clients in the same way, and it is purely a convenience — nothing stops the user typing a value that is not in your list, so validate on save if the value actually matters.
4. Find the object type code for a table
Object type codes come up during data migration, in ribbon rules, and anywhere a URL wants etc= rather than a logical name. Where you get them depends on where the organisation runs.
On-premises, the fastest route is the database:
select Name, ObjectTypeCode from EntityView order by ObjectTypeCodeCustom tables only — their codes always start above 9999:
select Name, ObjectTypeCode from EntityView WHERE ObjectTypeCode > 9999 order by ObjectTypeCodeOnline, there is no database to query, so ask the metadata service. Paste this into the browser while signed in to the environment:
[org Url]/api/data/v9.2/EntityDefinitions?$select=LogicalName,ObjectTypeCodeAnd for custom tables only:
[org Url]/api/data/v9.2/EntityDefinitions?$select=LogicalName,ObjectTypeCode&$filter=ObjectTypeCode gt 9999One warning that has bitten more than one migration: object type codes for custom tables are not stable across environments. They are assigned in creation order, so the same custom table can be 10012 in development and 10008 in production. For system tables the codes are fixed and safe to rely on; for custom ones, look the code up at run time from the metadata rather than hard-coding it, or use the logical name wherever the API accepts one.
5. Read the status reasons that belong to a given status
Every table has a statecode (Active, Inactive, and so on) and a statuscode (the finer-grained reason underneath it). The two are linked in metadata, and if you want to present the user with only the status reasons that are valid for the state a record is in, you have to read that relationship rather than guess it.
function getStatusAttributeMetadata(entityName) {
var data = null;
var webApiQuery = Xrm.Page.context.getClientUrl() + "/api/data/v8.2/EntityDefinitions(LogicalName='" + entityName + "')/Attributes/Microsoft.Dynamics.CRM.StatusAttributeMetadata?$expand=OptionSet";
var req = new XMLHttpRequest();
req.open('GET', webApiQuery, false);
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("OData-MaxVersion", "4.0");
req.setRequestHeader("OData-Version", "4.0");
req.send();
if (req.readyState == 4) {
if (req.status == 200) {
var results = JSON.parse(req.response);
data = results.value[0];
}
else {
var error = JSON.parse(req.response).error;
console.log(error.message);
}
}
return data;
}And to pick out the reasons belonging to one state:
var statusReasons = getStatusAttributeMetadata("lead");
if (statusReasons != null) {
var options = statusReasons.OptionSet.Options;
for (var i = 0; i < options.length; i++) {
if (options[i].State == 2) {
// options[i].Value is the statuscode
// options[i].Label.UserLocalizedLabel.Label is the display text
}
}
}Each option carries a State property, and that is the link back to statecode. Filter on it and you have exactly the reasons that are legal for that state. The label is nested a little deeper than people expect — Label.UserLocalizedLabel.Label — and in a multi-language organisation you may want Label.LocalizedLabels instead so you can pick the right language.
Metadata like this changes rarely and costs a round trip every time you ask for it, so cache the result for the life of the form rather than calling it inside a loop.
What to change if you are writing this today
All five snippets above use Xrm.Page, which has been deprecated since version 9.0. New form scripts should accept the execution context and work from the form context:
function onLoad(executionContext) {
var formContext = executionContext.getFormContext();
var control = formContext.getControl("new_marketinglist");
}Use Xrm.Utility.getGlobalContext().getClientUrl() in place of Xrm.Page.context.getClientUrl(), and prefer Xrm.WebApi over hand-rolled XMLHttpRequest calls — it is asynchronous, which the synchronous calls above are not, and a synchronous request freezes the form until it returns.
I have kept the original versions here because you will meet them constantly in solutions written before version 9, and reading them accurately is the first step to modernising them safely.
If there is a snippet you would like added to this collection, or you have found a case where one of these behaves differently than described, the contact page is the quickest way to reach me.

Like
Report
*This post is locked for comments