Sunday, February 21, 2010

Another Talk about Referencing External JS Files in MSCRM Form

There have been numerous blog posts on Internet talking about loading external JavaScript files in MSCRM form for reuse purpose. I am trying to stir the water with some of my thoughts.

Why External JS Files?

There could be a number of reasons that you might want to use external JS files for MSCRM form development, as it provides a number of advantages when comparing to embedding JS code in the CRM form itself.

  • Using external JS files can make your script files reusable across CRM forms, possibly even across CRM projects. It's very common in every CRM project to have some shared code to be used on different forms, it's always not best practice to simply copy/paste the same code here and there, which will cause quite some maintenance headache in the future.
  • The text editor provided by CRM customization tool is not really a productive tool when being used on a day-to-day basis. The editor doesn't have intellisense, no auto-completion, not even syntax highlighting. Storing form script in external files, you can use any development tool of your own favorite to write code faster with less errors.
  • Using external JS files make it possible to version control your JS code using your own version control software. Although you can store your CRM customization files in your SCM repository, it is very difficult to track what changes have been made from version to version due to the size of customization file.
  • You might want to use third-party JavaScript libraries in your form script, such as jQuery (I usually try to avoid this but you may have your own reason for doing this), in which case you may find that it doesn't make much sense to copy a whole big chunk of such library code to every form that you might need to use.

How do you do it?

It's well-known in the CRM community that there are two approaches to help you reference external JS files.

  1. The first approach injects the JS files to the head tag of CRM form's HTML file, which is basically a DOM-based technique.
    // Load external JS file - CrmServiceToolkit.js. 
    // ******* Not my recommendation though *******
    var script2Load = document.createElement("script");
    script2Load.language = "javascript";
    script2Load.src = "/ISV/CrmServiceToolkit/CrmServiceToolkit.js";
    document.getElementsByTagName("HEAD")[0].appendChild(script2Load);
    script2Load.onreadystatechange = function () {
        if (event.srcElement.readyState == "loaded") {
            // Do stuff here
        }
    };
  2. The second approach uses IE browser's XMLHttpRequest object to download the script files, then uses window.eval() or window.execScript() function to execute the code in a synchronous fashion. This approach was inspired by Robert Amos's load_script code.
    // Function to load external script
    function loadExternalScript(url)
    {
        var x  = new ActiveXObject("Msxml2.XMLHTTP"); 
        x.open("GET", url, false); 
        x.send(null); 
        window.execScript(x.responseText); 
    }
    
    loadExternalScript("/ISV/MyApp/Scripts/Common.js");
    loadExternalScript("/ISV/MyApp/Scripts/FormScripts/Account.js");
    Note: Be advised, window.execScript is an IE proprietary function, which means that if MSCRM ever becomes a cross-browser application in the future, this technique will not work. Hopefully by then, MSCRM will officially support external custom script files. ;)

My preference is the second approach due to its simplicity and synchronous fashion.

Using the first approach, if you ever need to load more than one JS file (which is often the case), you will have to check each file's ready status before running any of your JS code. CRM MVP Adi Katz has devised a smart solution to help address this issue that allows you to load multiple JS files using one single JS function. However the code still seems too complicated for its own simple purpose.

You may have noticed that Odynia's orignial code has a few extra lines of code than mine, as he used eval() function, which involves a tricky eval scope issue. When eval() function is used, any variable or function defined in the external JS files in the following format, which you might be expecting them living in the global scope, are actually running in the eval local scope, so as soon as the eval() finishes, your functions or variables defined in external JS files are out of scope, which makes them useless. So Odynia used the extra lines of code to make them explicitly global citizens.

// If you define you variable or function this way in external JS files, 
// you will have to use Odynia's extra code to make them available in global scope. 
var myVal = 1;
function myFunc() {
    // Do something
}

Note: If a web browser other than IE is used, there is a way to use eval() function to evaluate such variables or functions to global scope, but simply not for IE, which is the only browser supported by MSCRM at this moment.

For this reason, there is a derived simplified version at Henry's blog (section of Addition 2) based on Odynia's code using eval() function. However, there is a catchy when using Henry’s code (InjectScript function of Addition 2), you will have to make any variables or functions defined in the external JS files as implicit global ones in the following format, otherwise you will run into the eval scope issue which I just mentioned above.

// Implicit global variables and functions
myVal = 1;
myFunc = function () {
    // Do something
}

Note: Be advised, implicit global variables are usually considered bad coding practice.

Another option is to explicitly define the scope of your variable and function in global window object, as shown below:

// Explicit global variables and functions
window.myVal = 1;
window.myFunc = function () {
    // Do something
}

Either of the above code will have certain impact on your code’s maintainability.

Best Practices of CRM Form Script Development

With the above code handy, I think I am ready to offer some suggestions about the best practices of CRM form script development.

  1. In order to reuse JavaScript code and have CRM form script being version controlled in SCM, it's recommended to use a common function to load all project shared JavaScript library and form-specific code in the form’s OnLoad event. The location of commonly shared JavaScript library shall be "/ISV/MyOrgName/Scripts/", or "/ISV/MyAppName/Scripts", and the form-specific script should go to its sub-folder called FormScripts. So an entity form’s OnLoad event code might look like this:
    // Function to load external script
    function loadExternalScript(url)
    {
        var x  = new ActiveXObject("Msxml2.XMLHTTP"); 
        x.open("GET", url, false); 
        x.send(null); 
        window.execScript(x.responseText); 
    }
    
    loadExternalScript("/ISV/CrmServiceToolkit/CrmServiceToolkit.min.js"); // Third party libraries
    loadExternalScript("/ISV/MyApp/Scripts/Common.js");  // Shared JS library for the project
    loadExternalScript("/ISV/MyApp/Scripts/FormScripts/MyEntity.js"); //Form specific JS code
    Note: The number of shared JavaScript files should be kept to minimum.

    In case you may wonder what the heck CRM Service Toolkit is, please check out its homepage at codeplex and my another blog post for more details.

  2. Taking advantage of the above script for the benefit of code reusability and better maintainability, each CRM entity should have its own JavaScript files using the following naming convention:
    Item Name
    Entity OnLoad event <EntityName>.js
    Entity OnSave event <EntityName>_OnSave.js
    JavaScript code shared by the entity’s OnLoad and OnSave event <EntityName>_Shared.js
    Attribute OnChange event <AttributeName>_OnChange function, which resides in <EntityName>.js file

    Note: In most cases, you don't need 2nd and 3rd file, so most likely your entity will only need one JavaScript file, which is <EntityName>.js.

    Note: As mentioned in the above table, you should avoid putting CRM attribute’s onchange event in separate JS files, as it only causes client-side lag and increases server load. You can include such event function in <EntityName>.js file, such as:
    /*
      JS File: /ISV/MyApp/Scripts/FormScripts/account.js
    */
    
    // BEGIN: CRM Field Events
    PrimaryContact_OnChange = function()
    {
        // Do stuff
    };
    // END: CRM Field Events
    CRMAttributeOnChangeEvent

  3. Using or modifying anything through HTML DOM is usually considered unsupported unless that has been documented in Microsoft Dynamics CRM Client-side SDK. Such code may not be compatible with future version of Microsoft Dynamics CRM. Avoid the following unless no alternative choice:
    • Removing elements from the DOM.
    • Moving elements in the DOM.
    • Modifying any one of the form controls.
    • Reusing undocumented crmForm functions.
    • Anything that affects the structure of the DOM.

    Note: If you ever need to write unsupported script, you should try to make them centralized.

  4. Avoid event handler assignment unless you really intend to do so, as doing so will overwrite all existing event handler. In most cases, it’s more preferable to use attachEvent function.
    // Not recommended 
    crmForm.all.name.onmouseover = function() {
        // Implementation of the event function 
    }; 
    
    // More preferable
    crmForm.all.name.attachEvent("onmouseover", function() {
        // Implementation of the event function 
    });

Hope this helps.

Friday, February 12, 2010

Use CRM Assign Message in Plug-in Code

Assign message is not very often-used in MSCRM development, the information about the message is scattered here and there in the online community with no complete sample, so I am trying to give a simple example which demonstrates how to use it in your plug-in code to ensure a CRM record will only be assigned to users with a particular CRM role.

The following plug-in code is based on the requirement that the records of myapp_myentity CRM entity can only be used to be assigned to the users who have "Schedule Manager" role.
using System;
using System.Xml;
using Microsoft.Crm.Sdk;

namespace MyEntity
{
    /// <summary>
    /// CRM Plugin to demonstrate how to use Assign message.
    ///
    /// The Plugin should be registered as :
    ///
    /// Primary Entity: myapp_myentity
    /// Message:        Assign
    /// Stage:          Pre Stage
    /// Mode:           Synchronous
    /// </summary>
    public class PreAssign : IPlugin
    {
        const string entityName = "myapp_myentity";
        const string allowedCrmRole = "Schedule Manager";

        public void Execute(IPluginExecutionContext context)
        {
            // Verify the message context
            if (context.InputParameters.Properties.Contains("Target") ||
                context.InputParameters.Properties["Target"] is Moniker == false ||
                context.PrimaryEntityName != entityName)
            {
                return;
            }

            // Retrieve the new assignee from plug-in context
            SecurityPrincipal assignee = (SecurityPrincipal) context.InputParameters.Properties["Assignee"];

            // Ensure the new assignee has the specified role
            using (ICrmService crmService = context.CreateCrmService(true))
            {
                EnsureNewAssigneeIsInRole(crmService, assignee, allowedCrmRole);
            }
        }

        private void EnsureNewAssigneeIsInRole(ICrmService crmService, SecurityPrincipal assignee, string role)
        {
            // Query to check if the user is in the role.
            string fetchXml = string.Format(@"
<fetch mapping='logical' aggregate='true'>
   <entity name='systemuser'>
      <attribute name='systemuserid' aggregate='count' alias='count' />
      <filter>
         <condition attribute='systemuserid' operator='eq' value='{0}' />
      </filter>
      <link-entity name='systemuserroles' from='systemuserid' to='systemuserid' link-type='inner'>
         <link-entity name='role' from='roleid' to='roleid' link-type='inner'>
            <filter>
               <condition attribute='name' operator='eq' value='{1}' />
            </filter>
         </link-entity>
      </link-entity>
   </entity>
</fetch>
", assignee.PrincipalId, role);

            string fetchResult = crmService.Fetch(fetchXml);

            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(fetchResult);

            XmlNode xn = xmlDoc.SelectSingleNode("//resultset/result/count");
            if (Convert.ToDecimal(xn.InnerText) == 0)
            {
                string exceptionMsg = string.Format("The CRM record can only be assiged to a user with \"{0}\" role. ", role);
                throw new InvalidPluginExecutionException(exceptionMsg);
            }
        }
    }
}
The key to work with Assign message is to retrieve the Assignee secruity principal from the plug-in execution context object by using Assignee property of the input parameters, as shown below:
SecurityPrincipal assignee = (SecurityPrincipal) context.InputParameters.Properties["Assignee"];
An additional note, if you need to get the record's ID, you can use the following code:
Moniker moniker = (Moniker)context.InputParameters.Properties["Target"];
Guid id = moniker.Id;
As you can tell from the code, the plug-in has to be registered with the target entity's Assign message of Pre Stage using Synchronous execution mode.

Hope this helps.