Finding Document Templates for Entities in Dynamics 365 – Part 1: Creating the Control

Recently, a Dynamics 365 client had a need to add a new button to the to an entity’s ribbon that allowed for the client to:

  • open a popup window,
  • select a document template designated for the entity,
  • generate a document based on the template, and
  • add it to the the entity’s annotations

There is tremendous value in this flow in that it allows for users to generate a set of standard documents to accompany a CRM record and to share out those documents to team members.

In the next three posts, we will be reviewing how we were able to deliver this functionality to our client. All the files for this project can be found here.

This post will be focused on creating the page to host the functionality, querying Dynamics CRM’s Web API for the document templates available for the record type, and displaying the options.

Creating the Popup

The first step in developing this functionality was to create the pop-up we will be displaying. For the pop-up, we will be building a basic web page using HTML, CSS, and JavaScript. The control we will be creating will look like this when we are done.

Pop-up HTML and CSS Base

The base HTML and CSS for this page is pretty basic. The purpose here is to create the minimum required code to achieve our functionality.

Below is the HTML:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en">
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <title>Document Templates</title>
    <link rel="stylesheet" type="text/css" href="SelectDocumentTemplate.css" />
    <script src="ClientGlobalContext.js.aspx" type="text/javascript"></script>
</head>
<body>
    <h1>Select the Template to Use</h1>
    <input type="radio" name="templateType" value="2"/> <label>Word Document</label><br />
    <input type="radio" name="templateType" value="1"/> <label>Excel Document</label><br />
    <select id="templateSelect">
        <option value="1">Template 1</option>
        <option value="2">Template 2</option>
    </select>
    <div id="buttons">
        <button id="selectButton" onclick="documentTemplateSelected()">Select Document Template</button>
      <button id="cancelButton" onclick="window.close()">Cancel</button>
    </div>
    <script type="text/javascript" src="SelectDocumentTemplate.js"></script>
</body>
</html>

And the CSS:

body {
  font-family: 'Segoe UI';
  max-width: 800px;
  padding-left: 10px;
}

select {
  font-size: 11px;
}

#buttons {
  position: absolute;
  bottom: 20px;
  right: 20px;
}

select {
  min-height: 30px;
  min-width: 250px;
  font-size: 24px;
  margin-top: 20px;
}

Note that there is a reference to ClientGlobalContext.js.aspx (line 7 in the HTML). This will allow us to use the GetGlobalContext command to gather some information about the operation context under which the window operates. We will need this context later in this post to know which entity we are working with.

More information about the ClientGlobalContext.js.aspx can be found here.

Setting up Our JavaScript File

Below is the shell of the JavaScript file we will be filling out to wire up our control. We will be borrowing the concept of state from React. Although our state has only one property for now, we will build it out in case we need to manage additional state in the future.

// Gets DOM references to the controls we will be working with
const selectControl = document.getElementById("templateSelect");
const selectButton = document.getElementById("selectButton");

// Defines the state variables we will be working with
const state = {
    templates: []
};

// Function we will call on state change to update our controls
function stateChanged() {
    // TODO
}

// Function we call when the radio button selection is changed
function onTemplateTypeChange(value) {
    // TODO
}

// The function that is called when the 'Select Document Template' is clicked
function documentTemplateSelected() {
  // Do document template stuff here. We will show how to submit this to an action in a later blog post.
}

// Function we call to requests the templates from Dynamics CRM's Web API. We will be returning a promise
function requestTemplates(value) {
    return new Promise((resolve, reject) = {
      // TODO
    })
}

Control Business Rules

Our basic page will have the following business rules:

  • Upon opening, it will automatically have Word Documents selected.
  • When the radio button is changed, we query Dynamics CRM’s Web API for the templates for the type selected
  • When the template query returns, the templates in the select control is changed to display the templates for the newly selected template type
  • When no templates are available, the Select Document Template button is disabled

The first business rule can be enforced in JavaScript or HTML. For cleanliness, we will enforce it in the HTML. The other business requirements will be accomplished using JavaScript.

Word Document Selected When Control Opens

The first business rule can be enforced by adding the checked attribute to the first radio button:

<input type="radio" name="templateType" value="2" checked/> <label>Word Document</label>

Additionally, we will need to submit a query to Dynamics CRM’s Web API in the background for the templates that are of type “Word Document”. To do this, we will add a call to the onTemplateTypeChange function with the value of “2” at the bottom of our script:

// Gets DOM references to the controls we will be working with
const selectControl = document.getElementById("templateSelect");
const selectButton = document.getElementById("selectButton");

// Defines the state variables we will be working with
const state = {
    templates: []
};

// Function we will call on state change to update our controls
function stateChanged() {
    
}

// Function we call when the radio button selection is changed
function onTemplateTypeChange(value) {

}

// The function that is called when the 'Select Document Template' is clicked
function documentTemplateSelected() {
  // Do document template stuff here. We will show how to submit this to an action in a later blog post.
}

// Function we call to requests the templates from Dynamics CRM's Web API. We will be returning a promise
function requestTemplates(value) {
    return new Promise((resolve, reject) = {
      // TODO
    })
}

onTemplateTypeChange("2");

Don’t worry about the contents of that method, we will be building it out in a later segment.

Select Document Template Button Disabled When No Templates Are Available

For this, we want to toggle whether the button is enabled or disabled based on the state.template‘s length. If it is 0, we will disable the button. Otherwise, we will enable it. The JavaScript we can use to achieve this would be:

function stateChanged() {
    // Disable buttons if no templates exist
    if(state.templates.length === 0) {
        selectButton.disabled = true;
    } else {
        selectButton.disabled = false;
    }
}

Which can be simplified to:

function stateChanged() {
    // Disable buttons if no templates exist
    selectButton.disabled = state.templates.length === 0;
}

Having this in our stateChanged function will ensure that the button will be properly enabled or disabled whenever there is a change in the control’s state.

Query for Templates When Template Type Selection Changes

For handling the change of radio buttons, we will be using the onTemplateTypeChange function. First, we need to wire up these function to the radio controls. We can do this by altering our HTML to be:

<input type="radio" name="templateType" value="2" onclick="onTemplateTypeChange(this.value)" checked /> <label>Word Document</label>
<br />
<input type="radio" name="templateType" value="1" onclick="onTemplateTypeChange(this.value)" /> <label>Excel Document</label>

Next, we will write the code that will respond to these radio buttons. It will need to do several things:

  1. Clear the templates in the state. They will no longer be valid as we have changed the template type we are looking for.
  2. Submit a request to Dynamics CRM’s Web API to get the new templates.
  3. Let our control know that its state has changed.

Clearing the templates from state is as simple as resetting the state.templates variable back to an empty array.

The request to Dynamics CRM’s Web API will be in the requestTemplates function that we created. That function takes the template type as a parameter and returns a Promise. That Promise will resolve with the templates that we need.

Finally, we need to call the stateChanged method to indicate to our control that we’ve cleared the templates.

So far, our code looks like this:

// Function we call when the radio button selection is changed
function onTemplateTypeChange(value) {
    // Clear the templates, as our template type has changed
    state.templates = [];

    // Submit a request for the new templates
    requestTemplates(value).then((templates) => {
        // Handle response once we get a response from Dynamics CRM
    });

    // Indicate that we've cleared the templates and therefore have changed the state
    stateChanged();
}

When the Promise resolves, we will grab the templates and set the state.templates to the templates we got back from Dynamics CRM.

Finally, we will indicate that the state has changed by calling the stateChanged method.

Our final code in this method will be:

// Function we call when the radio button selection is changed
function onTemplateTypeChange(value) {
     // Clear the templates, as our template type has changed
    state.templates = [];

    // Submit a request for the new templates
    requestTemplates(value).then((templates) => {
        // Set the state for the templates we've received
        state.templates = templates;

        // Indicate that we've received the templates and therefore have changed the state
        stateChanged();
    });

    // Indicate that we've cleared the templates and therefore have changed the state
    stateChanged();
}

Update the Select Control with the Templates

For this, we want to set the template select control based on the templates contained in the state.template property.

Before we add options to our dropdown, we will want to clear its previous content. That can be done by setting its length property to 0;

selectControl.length = 0;

Next, we will want to loop through all of the templates in the state.templates property and create an option for it. The format we will be using for the template object will be:

{
    name: string,
    id:   string
}

Where name represents the name of the document template, and id represent’s its GUID. The code below iterates through the templates and creates an option for each one we have retrieved.

// Repopulate the dropdown with new templates
for (template of state.templates) {
    var option = document.createElement("option");
    option.text = template.name;
    option.value = template.id;
    selectControl.add(option);
}

Final stateChanged Code

With that, our business rules have been fully implemented. Our final code looks like this:

// Gets DOM references to the controls we will be working with
const selectControl = document.getElementById("templateSelect");
const selectButton = document.getElementById("selectButton");

// Defines the state variables we will be working with
let state = {
    templates: []
};

// Function we will call on state change to update our controls
function stateChanged() {
    // Disable buttons if no templates exist
    selectButton.disabled = state.templates.length === 0;

    // Clear the dropdown 
    selectControl.length = 0;

    // Repopulate the dropdown with new templates
    for (template of state.templates) {
        var option = document.createElement("option");
        option.text = template.name;
        option.value = template.id;
        selectControl.add(option);
    }
}

// Function we call when the radio button selection is changed
function onTemplateTypeChange(value) {

    // Clear the templates, as our template type has changed
    state.templates = [];

    // Submit a request for the new templates
    requestTemplates(value).then((templates) => {

        // Set the state for the templates we've received
        state.templates = templates;

        // Indicate that we've received the templates and therefore have changed the state
        stateChanged();
    });

    // Indicate that we've cleared the templates and therefore have changed the state
    stateChanged();
}

// The function that is called when the 'Select Document Template' is clicked
function documentTemplateSelected() {
  // Do document template stuff here. We will show how to submit this to an action in a later blog post.
}


// Function we call to requests the templates from Dynamics CRM's Web API
function requestTemplates(value) {
    return new Promise((resolve, reject) => {
        // To do in the next section
    });
}

onTemplateTypeChange("2");

Getting the Document Templates for an Entity

In order to retrieve the document templates from Dynamics CRM, we will make a request to its Web API. The Web API is a REST web service that supports ODATA queries.

The general format for making requests is:

[Organization URI]/api/data/[API Version]/[entity type]

For our example, our entity type will be documenttemplates and with the 9.0 API version. And so our requests will be going to:

 https://codevanguard.dynamics.com/DemoOrg/api/data/v9.0/documenttemplates

Additionally, because we are using ODATA, we can do some filtering to reduce the size of the result we receive to only the records and data we are interested in. This will allow us to reduce the data over the wire.

Trimming our Query

First, we will declare a variable called columns which will hold the names of the columns we want to get back from the service as a string array.

// Define the columns we want to retrieve
const columns = ["associatedentitytypecode", "name", "documenttemplateid", "documenttype"];

Then, we will format a select segment of a query string to append to our ODATA endpoint. The select segment of ODATA queries act similar to the SELECT command in SQL. It allows you to specify which columns you want returned in your query.

The format of the select segment is a comma-delimited string. Thus, our code will look like this:

// Function we call to requests the templates from Dynamics CRM's Web API
function requestTemplates(templateType) {
  // Define the columns we want to retrieve
  const columns = ["associatedentitytypecode", "name", "documenttemplateid", "documenttype"];
  const selectFilter = '$select=' + columns.join();

    return new Promise((resolve, reject) = {
      // TODO
    })
}

Next, we will want to filter the document templates so that we get the ones that are:

  • Associated with the entity for which we clicked the button
  • Of the type we selected with our radio buttons

Getting the Entity Logical Name

Earlier in the post, while we were setting up the HTML page, we made sure to include the ClientGlobalContext.js.aspx page. In this case, we can use it to grab the query string parameters fed in to the window. How that gets set will be covered by our next post.

For now, we’ll just assume that there is a parameter in the query string called typename that has the entity’s logical name for the record we’re opening from. Thus, we can write the following code to get the entity’s logical name from the query string parameters:

// Get Entity Logical Name
var parameters = GetGlobalContext().getQueryStringParameters();
const entityLogicalName = parameters.typename;

Filtering for Document Template Type

If you will recall from before, our requestTemplates function will receive template type as an argument. This makes creating our filter rather easy:

// Filter for only the entity type and template type we're interested in
const filter = "$filter=associatedentitytypecode eq '" + entityLogicalName + "' and documenttype eq " + templateType;

Here, we are using ODATA filters, which are similar to WHERE clauses in SQL, allowing us to find only the records that match our criteria. Finally, we can add our select and filter clauses to our API endpoint to get the final request URL:

// Function we call to requests the templates from Dynamics CRM's Web API
function requestTemplates(templateType) {
  // Define the columns we want to retrieve
  const columns = ["associatedentitytypecode", "name", "documenttemplateid", "documenttype"];
  const selectFilter = "$select=" + columns.join();

  // Get Entity Logical Name
  var parameters = GetGlobalContext().getQueryStringParameters();
  const entityLogicalName = parameters.typename;

  // Filter for only the entity type and template type we're interested in
  const filter = "$filter=associatedentitytypecode eq '" + entityLogicalName + "' and documenttype eq " + templateType;

  // Create the request URL from the columns and filter
  const requestUrl = "https://codenvanguard.dynamics.com/DemoOrg/api/data/v9.0/documenttemplates?" + selectFilter + "&amp;" + filter;

    return new Promise((resolve, reject) = {
      // TODO
    })
}

Submitting our Query and Handling the Result

To submit the request, we will be using an XMLHttpRequest. More information about XMLHttpRequests can be found here. Below is the basic shell of our request. The code below is setting the request type, location, and headers that are required for our request. Additionally, we have a handler that we’ve attached to handle when the request has completed.

// Create an XML HTTP Request for the data
var req = new XMLHttpRequest();

// Set our request location
req.open('GET', requestUrl);

// Add the required headers to the request
req.setRequestHeader('OData-MaxVersion', '4.0');
req.setRequestHeader('OData-Version', '4.0');
req.setRequestHeader('Accept', 'application/json');
req.setRequestHeader('Content-Type', 'application/json; charset=utf-8');
req.setRequestHeader('Prefer', 'odata.include-annotations="OData.Community.Display.V1.FormattedValue"');

// Add our handler for request state changes
req.onreadystatechange = function() {
    // Process when request is completed
    if (this.readyState === 4) {
        // Remove callback
        req.onreadystatechange = null;
        if (this.status === 200) {
            // TODO handle success
        } else {
            // TODO handle error
        }
    }
};

req.send();

Handling Responses and Errors

The results of the query will be in the response property of the request and in JSON format. First, we will parse out the result from the response. Next, we will get the array from the value field of the result. We will default it to an empty array if the value property doesn’t exist. Finally, we will resolve the templates promise.

var result = JSON.parse(this.response);
var templates = result.value || [];
resolve(templates);

For now, for errors, we will simply reject the promise. For your specific case, you should log the error and handle the response in a way that best suits your need.

Thus, our final code will look like this:

// Function we call to requests the templates from Dynamics CRM's Web API
function requestTemplates(templateType) {
  // Define the columns we want to retrieve
  const columns = ["associatedentitytypecode", "name", "documenttemplateid", "documenttype"];
  const selectFilter = "$select=" + columns.join();

  // Get Entity Logical Name
  var parameters = GetGlobalContext().getQueryStringParameters();
  const entityLogicalName = parameters.typename;

  // Filter for only the entity type and template type we're interested in
  const filter = "$filter=associatedentitytypecode eq '" + entityLogicalName + "' and documenttype eq " + templateType;

  // Create the request URL from the columns and filter
  const requestUrl = "https://codenvanguard.dynamics.com/DemoOrg/api/data/v9.0/documenttemplates?" + selectFilter + "&amp;" + filter;

  // Create a promise that will resolve when the request is complete
  return new Promise((resolve, reject) => {
    // Create an XML HTTP Request for the data
    var req = new XMLHttpRequest();

    // Set our request location
    req.open('GET', requestUrl);

    // Add the required headers to the request
    req.setRequestHeader('OData-MaxVersion', '4.0');
    req.setRequestHeader('OData-Version', '4.0');
    req.setRequestHeader('Accept', 'application/json');
    req.setRequestHeader('Content-Type', 'application/json; charset=utf-8');
    req.setRequestHeader('Prefer', 'odata.include-annotations="OData.Community.Display.V1.FormattedValue"');

    // Add our handler for request state changes
    req.onreadystatechange = function() {
      // Process when request is completed
      if (this.readyState === 4) {
        // Remove callback
        req.onreadystatechange = null;
        if (this.status === 200) {
          var result = JSON.parse(this.response);
          var templates = result.value || [];

          resolve(templates);
        } else {
          reject(this.statusText);
        }
      }
    };

    req.send();
  });
}

And that’s it! Our control should be ready for publishing!

In our next post, we will show how to upload your web resources and how to create a button on your record to open this dialogue.

Dynamics Controls for your Organization

Code Vanguard offers a collection of premium Dynamics 365 controls for your organization. Built on top of the Microsoft’s Fabric UI and Google’s Material UI, Dynamics UI is a set of responsive UI elements for Dynamics that looks good on any device! Check out our Dynamics UI page to learn more.


Check out some of our other posts about Dynamics CRM!

Tags:
James Stephens About the author

James is the founder of Code Vanguard and one of its developers. He is an applied mathematician turned computer programming. His focuses are on security, DevOps, automation, and Microsoft Azure.

No Comments

Post a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: