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.

8 comments:

  1. Fantastic! I was using this approach, which used more then a page of code to check if the files are loaded before moving on: http://kiavashshakibaee.blogspot.com/2009/02/reference-external-javascript-file-in.html

    So this is a big improvement in simplicity and readability! Thanks a lot!

    ReplyDelete
  2. Thanks Nate. I believe it's the nature of life that we grow by learning from each other.

    ReplyDelete
  3. Hi Daniel !

    THanks a alot for this code, i'm using the second approach since this morning.

    Everything is fine, but i have one question : I've modified an event function just to add an alert to see how it refreshes when i modify my script files...
    The fact is that the new code doesn't seem to execute, even if i reload (then re-inject the scripts) the form...

    Is it normal ?

    Thanks in Advance

    Eugene Moulin

    ReplyDelete
  4. Daniel,

    It is ok now, i've just modified your loadexternalscript function.

    function loadExternalScript(url)
    {
    var x = new ActiveXObject("Msxml2.XMLHTTP");
    x.open("GET", url + '?nocache=' + Math.random, false);
    x.send(null);
    window.execScript(x.responseText);
    }

    I've added a fake parameter at the end of the url so that the cache cannot recognize it...

    Thanks again for your code !

    EuG

    ReplyDelete
  5. Hi EuG,

    That's precisely what I am using here. When it comes to dealing with browser cache issue, I found that the IE developer tools is very handy. I can clean IE browser cache with one single mouse click. IE 8 has the feature out-of-box. Just a heads up, in case you may not notice before.

    Cheers,
    Daniel

    ReplyDelete
  6. Hi Daniel,

    I didn't know that IE dev tools included this feature.

    Thanks for the info :)

    EuG

    ReplyDelete
  7. Hi Daniel,

    Thanks for sharing. I used to insert scripts in the head tag. I think your solution is more elegant. I have one issue however, it seems to be impossible to debug the js files (with ie8 developer) when included using your aproach. Do you have any thoughts on this?

    Regards,
    Joost

    ReplyDelete
  8. @Joost, you should be able to debug your js file. I usually add debugger; statement in the file wherever I want to start to debug. This seems to be more efficient than IE8 developer tool, as I think it takes tremendous effort to locate the particular line of code that you want to debug using IE8 developer tool.

    The key is to make sure that your JS file doesn't have any syntax error.

    ReplyDelete