thinking in rules

Dynamic Surveys in Dynamics CRM Part II: Managing Dependencies

Josh Elster | 9/29/2015

Note: this post makes extensive references to my preceding post, Surveys in Dynamics CRM Part I: Don’t Be a Monkey With Your Survey. If you are the type of person who likes to get the full story, you should go back and read the first post before tackling this one. If you would simply like to see the code and read how it works, carry on!

The InRule User Community Meeting is mere days away here in Chicago, and I've been hard at work putting together a talk about CRM dynamic surveys. In the process of creating the talk, I realized I had far more material to talk about than I had time, which is not a bad problem to have. I decided to take that material and turn it into a blog post to complement my previous post on the subject. This post will focus on how the survey is hosted and launched, as well as how the various dependencies between components are managed. For reference, here’s a manifest of the UI components involved in the survey application:

Component

Purpose

Microsoft Dynamics® CRM SDK

DAL and application infrastructure

Knockout.js

MVVM framework

Underscore.js

JavaScript utility framework

jQuery/jQueryUI

DOM manipulation/General JavaScript framework

Require.js

Dependency management

HTML5/CSS3

Presentation

To review, this survey application framework is both business problem- and platform-agnostic. It doesn't require the survey to be related to an insurance quote nor does it require Microsoft Dynamics® CRM. CRM is simply the platform hosting the solution. The basic sequence of survey operations is recapped in this diagram:

The Launcher

Web resources in Dynamics CRM are typically placed on a Form and are rendered into an iframe. Because we don't necessarily want the survey to embed itself into the hosting form, we need some way to initiate the dialog sequence and kick off the survey UI. This takes place in another web resource I call the survey launcher. This simple HTML file, called survey-launcher.html, has as its main feature a bit of JavaScript which initializes the survey dialog, registers a callback to return feedback to the hosting CRM form, composes a query string, and then launches the survey (some code containing variable declarations and namespace definitions has been omitted in the interests of brevity).

 
var messageTypes = [ "INFO", "WARNING", "ERROR"];
 function refresh(v) { 
if (self.parent.Xrm.Page.data) {
self.parent.Xrm.Page.data.refresh();
}
if (self.parent.Xrm.Page.ui && v) {
var msgType = v.Type >= 0 || v.Type <= 2 ? messageTypes[v.Type] : 0;
var message = v.Message ? v.Message : typeof(v) !== 'string' ? JSON.stringify(v) : v;
self.parent.Xrm.Page.ui.setFormNotification(message, msgType);
}
}
 
if (GetGlobalContext != "undefined") {
                    IrSurvey.SurveyWebResource = GetGlobalContext().getClientUrl() + "/webresources/inrule_/survey/surveydisplay.html";
                }
                else {
                    IrSurvey.SurveyWebResource = "surveydisplay.html";
                }                
            
                var dlgOptions = new Xrm.DialogOptions();
                dlgOptions.width = 720;
                dlgOptions.height = 720;
            
                var dialogWindow;
                IrSurvey.launchSurvey = function (typename, id) {
                    if (!typename || !id) {
                       throw "invalid parameters";
                       return;
                    }
                    var resourceUrl = IrSurvey.SurveyWebResource + "?typename=" + typename + "&id=" + id;
                    dialogWindow = new Mscrm.CrmDialog(Mscrm.CrmUri.create(resourceUrl), window, dlgOptions.width, dlgOptions.height);
                    dialogWindow.setCallbackReference(refresh);
                    dialogWindow.show();                 
                };
 
$(document).ready(function() {
        var qs = Xrm.Page.context.getQueryStringParameters();
        $("#survey-launcher-link").click(function() {
                IrSurvey.launchSurvey(qs.typename, qs.id);
            });
        }); 

 

In the above snippet, GetGlobalContext is used to try and retrieve the URL in a "proper" fashion. If that is not successful, it simply falls back to specifying the name of the survey resource as a relative URL. The Mscrm.CrmDialog code leverages the Dynamics® JavaScript SDK to render the survey inside of a "native" dialog (aside: this method of popping the dialog isn't really documented and may break in future updates to CRM).

Send Data Back to the Calling Form

The cool thing about using this is that it's possible to register a callback to pass data from the child survey window to the main form. The refresh method in the snippet above is invoked when that happens, displaying a CRM notification or message (if applicable) before asynchronously refreshing the form data on the page. This enables some really neat scenarios that display feedback from the Rules Engine (in the form of notifications or validations) to the user in a familiar fashion.

Linking to the Survey

When the page has completed loading, the document.ready event fires, and the CRM entity type name and the CRM GUID for the "root" entity hosting the survey are extracted from the iframe's query string and injected into the URL of the survey View for use by the ViewModel. Finally, a click handler is attached to the "Launch Survey" link which invokes the previously defined launchSurvey function to present the survey View.

Managing Dependencies Between Scripts and Resources

Now that we’ve covered how the survey’s View is laid out as well as how the survey is launched, it’s time to discuss how the various scripts and their dependencies are loaded. If you’re using any version of Dynamics CRM newer than CRM 2011 U1 you may be aware that scripts are loaded asynchronously (e.g., <script async=”true” …/>). This means you cannot guarantee the order which scripts are executed, which makes for large headaches when you have scripts with dependencies.

Different Approaches to Dependency Management

A bit of searching uncovers a lot of conflicting, mostly outdated advice, caveat emptor. Back in 2013, Scott Durow wrote an overview of the issue and some approaches to work around the problem, which range from bundling everything you need into a single script (my advice: don’t do that) to implementing a manual awaiter (advice: don’t do this either!), but he does discuss using requireJS. His reasons for not using it don’t seem to me to be valid. Specifically, he says:

I didn't settle for the RequireJs/HeadJs option for the following reasons:

      1. It requires you to manually set the Cache key to ensure that scripts are not downloaded every time they are needed
      2. It completely bi-passes the Dynamics CRM script loading mechanism in an 'unsupported' way
      3. There is some possibility that a backward compatibility option may be added into a future release (I'm ever the optimist!). Adopting this approach would make it harder to revert back to the standard script registration approach.

Regarding the first point, I’ve specifically looked for this issue in my testing and have not been able to create a situation where my browser is loading old or cached resources – perhaps the caching behavior of CRM has changed since Scott’s post was written. The second point is partially valid, save that I would take exception to the ‘unsupported’ nature of the mechanism – there’s no specific support true, but there’s nothing in the documentation or product that I can find that specifically contraindicates the use of a loading framework like requireJs. The third reason Scott provided seems lost in time – we’re closing out 2015 pretty soon, and it doesn’t seem like such a radical reversion of such a fundamental piece of the CRM product will be changed again.

One Script Reference to Rule Them All

Using requireJS in this project was straight-forward, once I learned a bit about the internals of both requireJs and Dynamics® CRM. In the Survey’s View, I have a script tag which points to the requireJs web resource in CRM. It also has a data-main attribute which points require to a script containing initialization routines for the rest of the client-side logic:

<script data-main="/webresources/inrule_/survey/Scripts/irSurvey" src="/webresources/inrule_require.js"></script>

Although the script tag specifies how to find the initialization routines for the client application, it doesn’t specify any other configuration properties. Instead, there’s a self-executing anonymous function in a script block at the end of the HTML markup which performs all of the necessary configuration of the various script libraries and dependencies used by the survey.

Shim Down, Require Up

The configuration specifies aliases as well for libraries which serves to abstract file names from resource names and versions, and defines “shims” for some resources which do not conform to the AMD specification (Asynchronous Module Definition).

 
(function () {
      var requireConfig = {        
          paths:{
              jquery: '/_static/_common/scripts/jquery1.7.2.min',
              jqueryui: '/_static/_common/scripts/jquery_ui_1.8.21.min',
              underscore: '/webresources/inrule_/survey/Scripts/underscore_min',
              knockout: '/webresources/inrule_/survey/scripts/knockout_3.3.0',
              inrule_SdkSoap: '/webresources/inrule_SdkSoap',
              inrule_RulesEngineAction: '/webresources/inrule_RulesEngineAction',
              inRule: '/webresources/inrule_invokeCustomAction',              
          },
          shim: { 'inrule_SdkSoap': { exports: 'Sdk'},
                  'inrule_RulesEngineAction': { deps: ['inrule_SdkSoap'], exports: 'Sdk' },
                  'jqueryui': { deps: ['jquery'], exports: '$' }
          }          
      };  
      if (!window['require']) {
          window['require'] = requireConfig;
      } else {
          requireConfig.callback = function () {
          };
          require.config(requireConfig);
      }  
      requirejs.config(requireConfig);            
    })();  

 

Of particular note are the shim declarations for inrule_SdkSoap and inrule_RulesEngineAction. These two resources are from the Sdk.Soap.js library, which is a JavaScript implementation of many of the SOAP messages and objects in the Dynamics CRM SDK, and code generated to implement the SOAP message to trigger the custom Action, respectively.

The End Result

What all of that does is make it so that I only need add the survey-launcher web resource to a form, and all dependent resources and libraries will automatically be loaded when needed:


define(['knockout', 'underscore', 'jqueryui', 'inrule_RulesEngineAction', 'inrule_SdkSoap'],
    function (ko, _, $, Sdk) {…} 

 

The define function (see: http://www.requirejs.org/docs/whyamd.html for more on define) takes an array of strings containing the names of all of the modules which the current module is dependent upon. The resolved (e.g., object representing a loaded script library) dependencies are passed as arguments – in the order specified – to the second parameter, which is the survey module’s initialization function. Simply including the name of the component will instruct requireJs to make the necessary request to load the script, based on the configuration set previously. If a module has already been loaded, then a reference to the module is returned – it is not possible to accidentally load the same script resource more than once.

Conclusion

So that’s a look at how the survey is hosted and launched, and how requireJs is being used to manage the various script libraries and dependencies. The survey-launcher constructs a hyperlink which launches the survey dialog from query string parameters and mediates passing data back from the survey to the hosting CRM form. RequireJs is configured on the survey’s markup form (the View) with alias and paths for relevant script libraries so that the survey ViewModel loads its’ dependencies in the right time and place.

In future entries to this series, we’ll look a lot more closely into the Knockout View Model as well as the actual View’s layout and markup before delving into some custom binding logic and client-side querying of CRM. Thanks for tuning in – please post any questions, problems, or comments in the discussion below!

References and Further Reading:
http://dreamingincrm.com/2014/04/15/using-requirejs-in-crm2013/
http://hippieitgeek.blogspot.se/2013/07/load-jquery-ui-with-requirejs.html
http://www.requirejs.org/docs/whyamd.html  


comments powered by Disqus