Creating a dynamic dialog launcher menu for Dynamics CRM
I love Dynamics CRM dialogs. In fact, I think they are one of the best features of CRM 2011. What I don't like about dialogs is how the user has to run them when working with a entity record. On a "classic" mode form, the user has to go to the ribbon, click start dialog and then find the dialog in a list. If there are lots of dialogs for that entity type, the user then has to spend a lot of time looking, change the view or search. That experience is still better than on an "updated" mode forms where the user can't run dialogs at all without switching to classic mode. It is possible to launch a dialog via a URL from a web resource embedded in a CRM form using the format described here, but that approach requires you hardcode the GUID for the dialog you want to open. In this post I will show how to create a web resource dialog "launcher" that you can embed in a CRM form (both classic and updated modes) with JavaScript and an OData query.
Originally when I started looking at this, I was thinking about just a single dialog launch button, but then I remembered the "macros" system I created originally for CRM 3, and I realized that a dynamic dialog menu would achieve a lot of the same goals without anywhere near as much custom code. That being said, the approach I am going to show will allow you to create a menu that displays either a single dialog or multiple dialogs depending on exactly how you want to make them available to your end users.
Retrieving the dialogs
First, we have to retrieve the dialogs. Normally, I am a big fan of using FetchXML for all my multi-record CRM data retrieval, but I decided to get the list of dialogs via OData because the query is extremely simple, and I doubt anyone will run into a situation where the number of dialogs to retrieve will exceed the OData maximum limit.
Here is an OData query for retrieving an account-related dialog named Lucas Demo Account Dialog:
/WorkflowSet?$filter=Category/Value eq 1 and Type/Value eq 1 and StatusCode/Value eq 2 and PrimaryEntity eq 'account' and Name eq 'Lucas Demo Account Dialog' &$select=Name, WorkflowId, Description, Category, Type, PrimaryEntity&$orderby=Name
Let's take a closer look at the query parameters.
- The workflow set contains workflows and dialogs, so we use Category/Value eq 1 to get only dialogs.
- In addition to containing workflow and dialog definitions (and templates), the workflow set contains a separate entry for each activation. That is if you create and activate a dialog, then deactivate / edit / reactivate, etc., you will see the same dialog name multiple times in the results. This is why we use Type/Value eq 1 to get only the actual definition.
- A user can't execute a draft dialog, so we use StatusCode/Value eq 2 to retrieve only published dialogs.
- PrimaryEntity eq 'account' is used to retrieve only dialogs related to the account entity.
- Name eq 'Lucas Demo Account Dialog' returns only dialogs with the name of Lucas Demo Account Dialog.
That query is fine if you only want users to be able to launch a single dialog from your menu, but what if you want to have multiple dialogs? The easiest way to do that is to remove the Name parameter, so a query for all account-related dialogs would look like this:
/WorkflowSet?$filter=Category/Value eq 1 and Type/Value eq 1 and StatusCode/Value eq 2 and PrimaryEntity eq 'account' & $select=Name, WorkflowId, Description, Category, Type, PrimaryEntity&$orderby=Name
This is OK, but there's a better approach that will allow for retrieving single or multiple records without having to use a different query format. Here's the query:
/WorkflowSet?$filter=Type/Value eq 1 and Category/Value eq 1 and StatusCode/Value eq 2 and PrimaryEntity eq 'account' and startswith(Name, 'XXX') &$select=Name, WorkflowId, Description, Category, Type, PrimaryEntity&$orderby=Name
Using the "startswith" operator gives you a lot more control. If you do startswith(Name, ''), you get everything. If you do startswith(Name, 'Lucas Demo Account Dialog'), you get just the single dialog (assuming you only have one dialog with that name/entity combination). This approach also allows you to show a subset of dialogs. Imagine you have two kinds of account dialogs, sales and service. If you prefix your sales dialogs with SALES- and your service dialogs with SVC-, you can filter the dialogs to build a service-only menu using the following query.
/WorkflowSet?$filter=Type/Value eq 1 and Category/Value eq 1 and StatusCode/Value eq 2 and PrimaryEntity eq 'account' and startswith(Name, 'SVC-')&$select=Name, WorkflowId, Description, Category, Type, PrimaryEntity&$orderby=Name
At this point, we have everything we need from an OData query perspective to write a separate web resource for every entity/functional combination, but I think it's better to use a generic web resource that takes the entity and name parameters via a query string. Unfortunately JavaScript doesn't have native query string parsing capability, so we need to use a custom function to extract the query string values. This is based on something I found on Stack Overflow.
function parseQueryString(qs) {
var result = {}, keyValuePairs = qs.split('&');
keyValuePairs.forEach(function(keyValuePair) {
keyValuePair = keyValuePair.split('=');
result[keyValuePair[0]] = keyValuePair[1] || '';
});
return result;
}
//For IE8- support include this tiny Array.forEach* polyfill:
if ( !Array.prototype.forEach ) {
Array.prototype.forEach = function(fn, scope) {
for(var i = 0, len = this.length; i < len; ++i) {
fn.call(scope, this[i], i, this);
}
}
}
We then decode the querystring like this:
var queryObj = parseQueryString(location.search.slice(1));
Because we will be passing the dialog name and entity name to the web resource as custom parameter data, we have to URI decode the "data" object and then parse it. We can then pass the URI-decoded data object to the parseQueryString function to parse it like this:
var dataObj = parseQueryString(decodeURIComponent(queryObj["data"]));
With the query string fully parsed, here's the function to retrieve the list of dialogs.
function getDialogList() {
var dialogname = dataObj["dialogname"];
var entitytypename = dataObj["entitytypename"];
//only execute query if the record already exists
if((recordId != null) && (recordId != '')) {
//set the path to the odata rest service
//this can be hardcoded using the Organization Rest Service URL under Customizations->Developer Resources
var odataPath = "https://" + window.location.host + "/XRMServices/2011/OrganizationData.svc";
//no point inif entitytypename is provided
if((entitytypename != null) && (entitytypename != ''))
{
//set up the odata query
var retrieveReq = new XMLHttpRequest();
var odataQuery = odataPath + "/WorkflowSet?$filter=Type/Value eq 1 and Category/Value eq 1 and StatusCode/Value eq 2";
odataQuery += " and PrimaryEntity eq '"+entitytypename+"'";
odataQuery += " and startswith(Name, '"+dialogname+"')";
odataQuery += "&$select=Name, WorkflowId, Description, Category, Type, PrimaryEntity&$orderby=Name";
retrieveReq.open("GET", odataQuery, false);
//make sure we get json back
retrieveReq.setRequestHeader("Accept", "application/json");
retrieveReq.setRequestHeader("Content-Type", "application/json; charset=utf-8");
//request/response will execute asynchronously, so we need a callback function
retrieveReq.onreadystatechange = function () { generateLauncherMenu(this); };
//send the request
retrieveReq.send();
}
}
}
Generating the launcher menu
Once the dialogs have been retrieved by the OData query, we can use jQuery to easily generate the contents of the launcher menu. There's lots of ways you can represent the dialogs to launch - buttons, hyperlinks, drop-down menu entries, etc. - but I think buttons are the best UI for a small set of dialogs.
The following function writes dialog-launching buttons to a div called launchDiv.
function generateLauncherMenu(retrieveReq) {
//4 means a complete response
if (retrieveReq.readyState == 4) {
//parse response to json
var retrieved = this.parent.JSON.parse(retrieveReq.responseText).d;
for (var i = 0; i < retrieved.results.length; i++) {
//create a button and append it to a launchdiv div
var paramString = "\"" + retrieved.results[i].WorkflowId + "\",\"" + retrieved.results[i].PrimaryEntity+ "\",\"" +recordId + "\"";
$('#launchDiv').append( "<button onclick='openDialog("+paramString+")'>Launch "+retrieved.results[i].Name+"</button><br />" );
}
};
}
And then this function opens the dialog window:
function openDialog(processId, primaryEntity, entityId) {
var url = "/cs/dialog/rundialog.aspx?DialogId=" + processId + "&EntityName=" + primaryEntity + "&ObjectId=" + entityId.replace('{','').replace('}','');
window.open(parent.Xrm.Page.context.getClientUrl() + url);
}
Putting it all together
Here's the complete web page you can upload as a web resource to your CRM system: dialog_launcher.htm (3.91 kb)
All you have to do is embed the web resource on your entity form and set the properties and custom parameter data like in the following image:
I have two account dialogs in my CRM system, Demo-Dialog #1 and Demo-Dialog #2. This is what the launcher menu looks like in a pre-Polaris, read-only form:
Here is what it looks like in a classic-mode form:
As always, happy coding!