Dynamics 365 portals – Extend Search Results with Handlebars

With the Adxstudio Portals v7 product I can’t count the number of times that Adoxio has had to extend the search functionality with enhancements for customer requirements. With the latest release of Dynamics 365 portals, Microsoft has really made extending and customizing the out of box search extremely easily and powerful. The search is now built on top an asynchronous call to ensure page load performance is fast and the results all rendered with Handlebars templates. They have really gone a step further by taking those Handlebars templates and making them accessible through the Web Templates functionality; results display, paging, sorting, and the new facets are all available as Web Templates that you can customize and configure.

You can read more about the new base search functionality from a blog post on the Dynamics Team MSDN Blog, Search enhancements in Portal capabilities for Microsoft Dynamics 365.

If you’re not familiar with Handlebars you might be wondering what it is. Handlebars is a JavaScript framework, in which you can build semantic templates, with no frustration, or so they say 😉 – read more about why semantic templates help developers from Martin Brennan, Semantic templates with Mustache.js and Handlebars.js. Think of a template as being a view of how you want each item in the results rendered; put the title in an H3, description text in a P, add this class, etc. Why this is important is that the framework has a lot of power that beyond just formatting that allows you to easily modify the templates so you can create your own views, but also extend the functionality by adding helper methods with custom logic.

With the functionality of Handlebars helpers you can build functions to process the data submitted to a template. So if you wanted to do some sort of conditional processing or format on each item rendered, building your own helper can help you meet a certain requirement. Recently on the Dynamics Community Forum a member was looking to extend each result item with additional data from the full entity itself. So if the result was a Knowledge Article they might want to also show the subject or other categorization metadata in the search result. The default data context returned by the search to keep performance high has a limited set of attributes that really only include what you see rendered in a result. With a Handlebars helper and the power of Liquid we actually have the capability to go get the detail data for that particular entity record result.

For this example the Handlebars helper is going to make a synchronous AJAX request passing the entity ID of the result item, which will be then processed by a Web Template using Liquid to go get additional data that will then be included in the result item. First we need to setup our Web Template that will take the entity ID and go get the additional data and format it for us.

{% assign eid = request.params['entityid'] %}
{% assign wpage = entities.adx_webpage[eid] %}

<p><strong>Name:</strong> {{wpage.adx_name}}</p> 

We are going to assume for this that the result is always a web page but you could easily add an additional parameter for the entity logical name and then have conditional branching to handle different entities. Basically the template just takes the query string parameter entityid, uses the liquid entities object to query the web page entity for the desired ID then displays the page name back. You could access any of the attributes in the web page or whatever entity you had queried.

Once the Web Template is created we now need to make it accessible to the AJAX call we are going to later configure. To do so we need to add a Page Template that uses this Web Template. Create a new Page Template from Portals > Page Templates. Ensure to set the Type to “Web Template”, select the Web Template as the template you just created, and deselect the “Use Website Header and Footer”. All we want this page to return is the HTML that is to be included in the result.

search_details_pagetemplate

Now to make your page template accessible via a URL you will need to create a Web Page that surfaces the Web Template your Page Template refers to.

search_details_webpage

You can test your template is accessible by just directly calling its URL in your browser with an ID included, https://testingportal0001.microsoftcrmportals.com/search-details/?entityid=94216ad7-7864-e611-80d7-00155db4fa48. All you should get back is the simple HTML in the template since we removed the Website Header and Footer.

Now we need to register our Handlebars helper that is going to request this template. To do so we need to add some custom JavaScript to the search results page. This can be easily done with the front-side editor or you can access the web page from the CRM (Portals > Web Pages > Search). If using the front-side editor, login with your Administrative account, run a search query and then select the Edit from the CMS control, select the Options tab, and then paste in the following to the custom JavaScript section.

Handlebars.registerHelper("search_details", function (obj) {
    return $.ajax({
      url: "/search-details/?entityid=" + obj.fn(this),
      type: "GET",
      async: false
    }).responseText;
});

Basically it makes a synchronous AJAX request (it needs to be synchronous as the it is already an asynchronous request for the results) to the URL we created with our Web Page passing the entityID. It will return the HTML as the responseText which is then returned as the result of the Handlebars helper. You would be able to put whatever logic you want within this function, whatever JavaScript allows you to, which is A LOT.

Now that we have our helper function, and our template, its time to modify the search results Web Template that is provided out of the box to include a call to our new helper. Navigate in CRM to Portals > Web Templates, open the “Faceted Search – Results Template” and you want to add the following within the {{#each items}} where you want your extra details to show up: {{#search_details}}{{entityID}}{{/search_details}}. This will call our helper method passing the entity ID of the result item.

{{#each items}}
 <li>
  <h3><a title="{{title}}" href="{{url}}">{{title}}</a></h3>
  <p class="fragment">{{{fragment}}}</p>
  {{#search_details}}{{entityID}}{{/search_details}}
  <div>
   {{#each tags}}
    <span class="{{cssClass}}">{{./label}}</span>
   {{/each}}
  </div>
 </li>
{{/each}}

With Dynamics 365 portals you also need to be very aware of entity permissions. Our liquid query will return nothing if we do not provide permission to that entity. You will need to decide exactly how to configure the entity permission, for this example I am just adding an entity permission for Web Page with a Global scope and the Web Roles Anonymous and Authenticated User. It is important to be careful and aware of what you configure with entity permissions as they are what gate and protect the data in your CRM.

Now that it is all configured, head back to the portal and refresh your search results or perform a search query. Your results should now include your extra template on each and every result.

search_details_results

My template for this example was simple, but with the power of liquid querying you could really include anything here. This example also focused on adding additional details but Handlebar helpers can be used for a lot more.

Calling the Dynamics Global Discovery Service

With the launch of Dynamics 365 comes a global version of the CRM Discovery Service that can be used to find instances across regions. This is a Web API only service and with this service you will no longer have to prompt users for their region, as the service can return a list of all instances from all regions at once. For developers building CRM applications this is an important service to be aware of as it can greatly improve the user experience of the the application since users no longer will have to indicate their region before querying for a list of instances.

You can read the documentation for the Global Discovery Service on the Microsoft Developer Network (MSDN) – Discover the URL for your organization using the Web API.

One of the key aspects of using the new Global Discovery Service is getting an authorization token for the Dynamics Web API that allows you to properly access this resource. This example will utilize the new Server-to-Server (S2S) authentication that has been made available in Dynamics 365 as well. To learn more and setup S2S authentication you can view the MSDN documentation, Build web applications using Server-to-Server (S2S) authentication.

Firstly you will need to initiate a OpenID challenge to get an authorization code that we can then use to acquire a token to use in Web API calls to Dynamics. You can do this using the OWIN method described in the in Walkthrough: Multi-tenant server-to-server authentication (note this can be followed even if not creating a multi-tenant application) or you can make this challenge manually without using OWIN. We use the authorization code returned to then request a token to a particular resource, like a Dynamics instance or the discovery service it self.

To call the global discovery service you will need to request a token for the resource https://disco.crm.dynamics.com/ not the global discovery service address https://globaldisco.crm.dynamics.com/. Below is an example that utilizes ADAL (Active Directory Authentication Library) to retrieve a token that will allow that request against the desired Azure AD tenant with client credentials (client id + secret), the authorization code, return URI as well as the resource indicated above.

// setup parameters for authentication context
var tenantId = "<guid-of-azure-ad-instance>";
var aadInstance = "https://login.microsoftonline.com";

// define auth context
var authContext = new Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext($"{aadInstance}{tenantId}");

// setup parameters to get token
var code = "<code-returned-from-OpenID-challenge>";
var credential = new ClientCredential("<ClientId>", "<ClientSecret>");
var uri = "<same-return-URI-from-OpenID-challenge>";
var resource = "https://disco.crm.dynamics.com/";

// request token with parameters
var authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(code, uri, credential, resource);

Once you have your token we will now use it in a call to the global discovery service. The token you received back must be included in the request authorization header in the call to the global discovery service Web API endpoint https://globaldisco.crm.dynamics.com/api/discovery/v1.0/Instances.

// setup http client for GET of instances from global discovery service
var discoEndpoint = "https://globaldisco.crm.dynamics.com/api/discovery/v1.0/Instances";
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("GET"), discoEndpoint);

// send http client request
var response = await httpClient.SendAsync(request);

The first thing we are going to want to do with the response is validate that is was successful.

 // validate status code OK
if (response.StatusCode != HttpStatusCode.OK)
{
    throw new Exception($"Global Discovery Service response status code: {response.StatusCode}");
}

If it was not successful then review the WWW-Authenticate in the response header to check to see if it wants another resource to be specified in the token (it might also be as simple as you missed a trailing slash in the resource). If you do see another resource indicated in the response you will need to re-request a token with the resource specified in the response header then re-make your request to the global discovery service. With the refresh token that is included in the authentication result of the AcquireTokenByAuthorizationCodeAsync you can easily re-request a token for a new resource instead of requiring the authorization code. ADAL will cache all your tokens and you will build up a multi-resource token repository by using this method. Below is an example of using the refresh token to now access the Canadian discovery service after getting the following response with the WWW-Authenticate in the header.

Bearer authorization_uri=https://login.windows.net/common/oauth2/authorize, resource_id=https://disco.crm3.dynamics.com/
var newResource = "https://disco.crm3.dynamics.com/";
var authResultCRM4 = await authContext.AcquireTokenByRefreshTokenAsync(authResult.RefreshToken, credential, newResource);

Note: The tenants I have worked with thus far have all used the North American Discovery Service resource (even tenants started on CRM4 for instance).

Once you have successfully been able to get a response with a HTTP status code of OK using the correct token, the body of the response should be a JSON string that can be converted to an object for you to bind or return to other interfaces. The response JSON content if returning the entire instance object should deserialize into a list of the following DiscoInstanceModel class.

public class DiscoInstanceModel
{
    public Guid Id { get; set; }
    public string UniqueName { get; set; }
    public string UrlName { get; set; }
    public string FriendlyName { get; set; }
    public int State { get; set; }
    public string Version { get; set; }
    public string Url { get; set; }
    public string ApiUrl { get; set; }
    public DateTime LastUpdated { get; set; }
}

You can deserialize the data using Newtonsoft.Json.

// read response content to string
var data = await response.Content.ReadAsStringAsync();

// parse the string to a JObject and deserialize to list of DiscoInstanceModel from the content value
var dataValues = JObject.Parse(data); 
var discoInstances = JsonConvert.DeserializeObject<List<DiscoInstanceModel>>(dataValues["value"].ToString());

The service just like the other Dynamics Web API services provided support for $select, $filter as well as $metadata with the instances entity. So if for instance you wanted to get back only instances of a certain version you could do so with the following filter for all those v8.2.x instances.

https://globaldisco.crm.dynamics.com/api/discovery/v1.0/Instances?$filter=startswith(Version,'8.2.')

A lot of the code provided here is meant as a sample, it will need to be adapted to how you will use this service within your own applications. There are lots of ways of using ADAL to obtain and maintain a token to access the service, so it will require you to implement one that works for individual use cases.

Dynamics 365 trials now include portals!

If you create a new Dynamics 365 trial since around the holiday time frame you may have noticed that a portal can be configured as part of this trial. If not read on for instructions on how to configure your portal as part of the trial.

Once you have created your Dynamics 365 trial, (suggested to select the “All of these” option when provisioning your trial) you should be taken to your Dynamics 365 instance.

trial_options

You will need to get to the Dynamics 365 Administration Center to be able to configure your portal as part of the trial. You can do so by navigating to the Office 365 Admin Center or by directly going to the following URL – https://port.crm3.dynamics.com/g/manage/index.aspx. You may need to change crm3 to whichever region your instance resides in, ie CRM for NA/USA, CRM3 for Canada – the full list of regions can be found here – http://www.xrmcoaches.com/2016/01/current-list-of-dynamics-crm-online-regions/.

Once into the administration center, select Applications from the top and you should have a number of applications listed if you selected the “All of these” choice when provisioning your demo including a Not Configured portal! Highlight the portal application called “Portal Add-On” in the list and select Manage.

portal_applications

You should now be able to continue configuring your portal with your desired configuration for your demo or trial.

If you have not had a portal add-on added automatically to your trial you can still request a trial key from the the CRM Managed Trial site – https://crmmanagedtrials.dynamics.com/.

Thanks to Adoxio’s Kunal Tripathy for pointing out this great addition to trials.

Note: This post also appears on Adoxio Business Solutions Team Blog.