Sticky

This blog has moved to www.dreamingincrm.com. Please update your feed Url. Thank you.

25 May 2015

Leverage Actions to bundle and minify form scripts

EDIT (26/05/15): I forgot to disable the plugin on RetrieveMultiple of webresource, described in the previous post. I also forgot to minify the script that runs on Form Experiment entity. Strangely however, this has reduced the performance. I tested this in CRMOnline, and so I am not sure if this is due to the load on the server at this time of the day. I have modified the performance screenshot, to reflect this.

I tried another approach to dynamically minify and load CRM form script. This time I am utilising Actions. I have tried different ideas with Actions in the past and love how extensible and powerful they are. Of all the approaches I have tried so far, I like this a lot. As usual, if you would rather read code, head to https://github.com/rajyraman/DynamicScriptBundling and fork the code.

This is the basic flow of this design:
  1. Form script just has code to invoke the action using current entityname as input parameter
  2. Action uses the Form Load Sequence entity to collate a list of scripts that have to be minified and concatenated. The minified and concatenated script is stored in the Action's output parameter
  3. The calling form script, evals the returned minified and concatenated script
Before you get defensive aggressive about using evals, please read http://www.nczonline.net/blog/2013/06/25/eval-isnt-evil-just-misunderstood/. IMHO, it is perfectly alright to use eval in this scenario. Here are the steps:

Create the Action
The action doesn't contain much config, other than the input parameter and output parameter. The maximum length of the string, if you set this from the UI is 255, however, if you use a plugin to do the same, there is no limit. We are going to use this behaviour, to set the minifiedscripts with the bundled/concatenated scripts.

Create the Plugin that is triggered by the Action
The Action's input argument is passed on to the Plugin's InputParameter and this is how the plugin knows, which entity it is dealing with. It then proceeds to concatenate and minify the scripts.

Plugin Code
using System.Text;
using DouglasCrockford.JsMin;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Linq;
using System.Xml.Linq;
using Contract = System.Diagnostics.Contracts.Contract;

namespace RYR.Experiments.Plugins
{
    public class PostGetScriptsForEntity : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            Contract.Assert(serviceProvider != null, "serviceProvider is null");
            var pluginExecutionContext =
                (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            var tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            Contract.Assert(tracingService != null, "TracingService is null");

            try
            {
                var factory =
                    (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));

                var organizationService = factory.CreateOrganizationService(pluginExecutionContext.UserId);
                var entityName = pluginExecutionContext.InputParameters["entityname"];

                var scriptLoadOrderFetchXml = string.Format(@"
                    <fetch count='1' >
                      <entity name='ryr_formloadsequence' >
                        <attribute name='ryr_name' />
                        <attribute name='ryr_scripts' />
                        <filter>
                          <condition attribute='ryr_name' operator='eq' value='{0}' />
                        </filter>
                      </entity>
                    </fetch>", entityName);
                var concatenatedScripts = new StringBuilder();
                var loadSequenceResults =
                    organizationService.RetrieveMultiple(new FetchExpression(scriptLoadOrderFetchXml))
                        .Entities;

                if (!loadSequenceResults.Any()) return;

                var scriptsToMerge = loadSequenceResults[0].GetAttributeValue<string>("ryr_scripts").Split(',');
                var webresourceFetchXml = string.Format(@"
                        <fetch>
                          <entity name='webresource' >
                            <attribute name='content' />
                            <attribute name='name' />
                            <filter>
                              <condition attribute='webresourcetype' operator='eq' value='3' />
                              <condition attribute='name' operator='in' >
                                {0}
                              </condition>
                            </filter>
                          </entity>
                        </fetch>", string.Join(string.Empty,
                         scriptsToMerge.Select(x => string.Format("<value>{0}</value>", x))));
                var toBeMergedWebResources = organizationService.RetrieveMultiple(
                    new FetchExpression(webresourceFetchXml)).Entities;

                if (!toBeMergedWebResources.Any()) return;

                foreach (var s in scriptsToMerge)
                {
                    var matchedWebresource = toBeMergedWebResources
                        .FirstOrDefault(x => x.GetAttributeValue<string>("name") == s);
                    if (matchedWebresource != null)
                    {
                        concatenatedScripts.AppendLine(Encoding.UTF8.GetString(Convert.FromBase64String(
                            matchedWebresource.GetAttributeValue<string>("content"))));
                    }
                    else
                    {
                        concatenatedScripts.AppendLine(
                            string.Format(
                                "Xrm.Page.ui.setFormNotification('Unable to load {0}', 'ERROR', '{1}');", s,
                                Guid.NewGuid()));
                    }
                }
                pluginExecutionContext.OutputParameters["minifiedscripts"] = new JsMinifier().Minify(concatenatedScripts.ToString());
            }
            catch (Exception e)
            {
                tracingService.Trace(e.StackTrace);
                throw;
            }
        }
    }
}

Use Sdk.Soap.js to generate Javascript code for Action

Download Sdk.Soap.js  and run the Sdk.SoapActionMessageGenerator.exe executable. Once you choose the organisation details and enter the credentials, the Javascript code for each individual action will be generated.

Modify the generated code to auto-populate entity name

The code generated by the Sdk.SoapActionMessageGenerator is going to be used in the form. We want to simply add this script to any entity, without having to change a single line of code. However, the Action accepts an input parameter called entity, that will obviously be different from entity to entity. Hence we have to make a slight modification to ensure that entityname is set automatically using Client API.

Change

(function() {
this.ryr_GetScriptsForEntityRequest = function (
entityname
)
{
///
/// 
///
///
/// [Add Description]
///
if (!(this instanceof Sdk.ryr_GetScriptsForEntityRequest)) {
return new Sdk.ryr_GetScriptsForEntityRequest(entityname);
}

 to

function () {
this.ryr_GetScriptsForEntityRequest = function ()
{
var entityname = Xrm.Page.data.entity.getEntityName(); 
///
/// 
///
///
/// [Add Description]
///
if (!(this instanceof Sdk.ryr_GetScriptsForEntityRequest)) {
return new Sdk.ryr_GetScriptsForEntityRequest(entityname);
}

I also had problem with strict mode and so I removed it from both Sdk.Soap.js and Sdk.ryr_GetScriptsForEntity.vsdoc.js. Now we merge both these scripts into one and add that as a javascript webresource. This webresource is then added to the form.

Here is the script coming through in the Action response, before getting evaled.



Performance


 291 ms (minified and added to the form)

vs

 440 ms (dynamically minified using Action)

Hope this gives you an idea on how you can use Action to solve the script load sequence issue in CRM2015.

No comments:

Post a Comment