External js file and CRM

Published 5/27/2008 by Henry in AJAX | CRM | Javascript

While doing a project where MS Dynamics CRM  is used a lot of customizations are performed by JavaScript.
Usually the way to it is to perform some JavaScript actions in the OnLoad of the Page.
MS Dynamics CRM has a extention point, where you can control the OnLoad of Detail Forms by entering JavaScript.

Now when you need to deploy your CRM configuration to more than one system (like we do at my project, it is sold as part of a product), you want to use a centralized Javascript file so you can change your url's etc. all in one place.
To do this (unsupported by Microsoft!) I learnt the following technique from CRM Specialists:

First technique

   1:  var script = document.createElement('script');
   2:  script.language = 'javascript';
   3:  script.src = '/_customscript/customscript.js';
   4:  script.onreadystatechange = OnScriptReadyState;
   5:  document.getElementsByTagName('head')[0].appendChild(script);
   6:   
   7:  function OnScriptReadyState()
   8:  {
   9:      if (event.srcElement.readyState == 'complete')
  10:     {
  11:          // Perform onload script
  12:          //Doit();
  13:      }
  14:  }

Listing 1

The drawback with this technique is that the first time CRM loads (and every time the cache is empty) the script is not executed. Leaving the user to think the application does not work. After some time it really annoyed me, so I started to ask uncle Google again for a solution. I found the following on http://blog.odynia.org/archives/1-Javascript-Includes.html.

What this guy does is doing an AJAX call, to get the js file.
Next he loads the javascript as a string, eval() it, and imports all functions found into the current namespace, so you can access them.

It needs functionnames a-z, it cannot handle numeric values in the name of the function, but i will fix this before I will use it.
Otherwise I think it rocks! Async technique (no first time drawback)

   1:  function load_script (url) 
   2:  { 
   3:      var x = new ActiveXObject("Msxml2.XMLHTTP"); 
   4:      x.open('GET', url, false); 
   5:      x.send(''); 
   6:      eval(x.responseText); 
   7:      var s = x.responseText.split(/\n/); 
   8:      var r = /^function\s*([a-z_]+)/i; 
   9:      for (var i = 0; i < s.length; i++) 
  10:      { 
  11:          var m = r.exec(s[i]); 
  12:          if (m != null) 
  13:          {
  14:              window[m[1]] = eval(m[1]); 
  15:          }
  16:      } 
  17:  } 
  18:   
  19:  load_script("/_customscript/customscript.js"); 
  20:   
  21:  //perform onload scripts
  22:  //DoIt();

Listing 2

Addition:

As I mentioned numbers in the functionname caused the code to fail. So I changed the regex pattern in line 8 from listing 2 into:

   1:  var r = /^function\s*([a-zA-Z_0-9]+)/i; 

Listing 3

With this regex pattern functions with numbers in the name also are added to the namespace. I added the uppercase A-Z not because functions with uppercase characters in the name where not added, but as a best practice. also you can never be sure browsers keep on using IgnoreCase as default setting.

As you can read in the comments, Marc-Andre uses the following pattern:

   1:  var r = /^(?:function|var)\s*([a-zA-Z_]+)/i; 

He wants some vars (which he uses as constants) to be added to the namespace also, maybe I would add the 0-9 here also. anyway, I think it is a good suggestion to mention here.

Addition 2:

Steve Le Mon made a very good suggestion and tried out a few things, he found a way around the parsing of the functions and/or vars and adding them to the current namespace.
I tweaked his code a little bit and ended up with the following:

   1:  function InjectScript(scriptFile)
   2:  {
   3:      var netRequest = new ActiveXObject("Msxml2.XMLHTTP"); 
   4:      netRequest.open("GET", scriptFile, false); 
   5:      netRequest.send(null); 
   6:      eval(netRequest.responseText); 
   7:  }
   8:   
   9:  InjectScript('/_customscript/customscript.js');
  10:   
  11:  //CallFunctionInExternalFile(); 

Listing 4

This technique removes the overhead of the parsing of the functions and vars so will perform faster.

Henry Cordes
My thoughts exactly...


Comments (27) -

Robert | Reply

7/1/2008 2:42:12 AM #

Agreed, this is a very interesting mehanism for doing this.

The one caution with any of these solutions is that they don't work when you have to support offline outlook clients.

It's hard to connect to your central .js file when offline.
Thus, you will have to do the extra deployment steps to get your .js files onto the offline client.
Not prohibitive, but must be kept in mind.

11/26/2008 8:09:34 AM #

Hi Robert,

I just now read your comment to my post. I think it is true that you need to keep in mind that this will provide a challenge when you have to support offline outlook clients.
But that said, in cases where you do not have that requirement it is will give better maintainability of the solution.
In particular when CRM is part of a product that will be rolled out at different clients (that DO NOT use offline outlook clients) and for example have different external interfaces with other services and applications.
When using an external .js file the configuration has to take place in one file only, instead of in every OnLoad function of the details form for every entity that uses these external interface or service.

I really like it that a co-avanade colleague took the time to comment on my blog! See you in the communities...

Henry

Jeff | Reply

12/17/2008 5:20:08 AM #

This is great thanks!! I Had been trying the first technique with no luck

12/17/2008 11:14:07 AM #

Thx Jeff,
Glad to help.

Henry

Ersin Öztürk | Reply

2/12/2009 8:31:55 AM #

Very good, thanks,

Rich | Reply

2/18/2009 5:59:40 AM #

Is the reason that it isn't supported because of the offline client issues?

2/18/2009 11:11:39 AM #

I suspect you are right, that the fact that when using the offline client, the externally reference .js file can not be found by the page if the client is offline.
There are ways around this, but they are cumbersome.
Like Robert mentions in his comment: [quote]Thus, you will have to do the extra deployment steps to get your .js files onto the offline client.[/quote]  

You need to find a way to deploy the file to all users and when the file changes, all clients need the new changed file.
One of the nice things about web applications is that the deployment model is easy, you only have to deploy one time, for all your users to recieve the new version, this javascript file that is on the server can make things much more complex.

Marc-Andre | Reply

3/15/2009 5:31:02 AM #

Hi Henry,
Great piece of code! It worked the first try for me (although I had a syntax error message but that was due to a real syntax error in the JS file I was trying to import!)

Besides, I also have declared "constants" (by they really are jscript variables) in my js file and would have liked them to be imported as well.
So I adapted your script to do that. The only change is in the regex pattern in fact. So here's the line I've changed:

var r = /^[b](?:function|var)[/b]\s*([a-zA-Z_]+)/i;


3/15/2009 8:04:20 AM #

Great addition Marc-Andre!
I will add your argument in the post.
As mentioned in the post the regex pattern also has problems reading numbers in function names, this will also be the case with var's, until you use:
var r = /^(?:function|var)\s*([a-zA-Z_0-9]+)/i;

Thanks.

Marc-Andre | Reply

3/16/2009 2:43:41 AM #

Right, I did not notice the 0-9 pattern addition when I first came on this page. I will add it to my code for sure!

Thanks.

Steve Le Mon | Reply

4/20/2009 11:10:20 PM #

This is really cool and ideal for editing code outside of the CRM form modifer, but how could this code be used to help with code behind change events.

One would still have to go into the form design to put a call to the outside source file, or is there a way around this?

4/21/2009 6:12:14 PM #

@Steve Le Mon, I hope I understand your question right.
If you want to hook on to change events (in javascript) of a textbox or dropdownlist on a CRM detail form, you can do it by using attachEvent.
CodeBehind, as in a C# code behind file (.cs) for an .aspx file cannot be injected.

Hope this helps...

Steve Le Mon | Reply

4/24/2009 6:49:54 PM #

Cheers Henry

I last comment if I may. This code is changed the way I develop solutions winthin CRM, for which I'm eternally greatful. This is such a breeze now I don't have to round-trip into customisations etc. etc.

One problem I have notice is that the javascript files are being cache by IE and I'm having to clear my cache each time I make a change. It's not as bad as having to re-publish etc. but is there a way to put something in the scripts to force a reload of the jscript files.

Other sites have random data after the path name e.g. "myscript.js?nocache=" + dateTime()

But this trick does not work on your script loader as I've tried it. How do you get around this issue?

Regards

Steve

4/24/2009 10:32:12 PM #

No problem Steve, I actually like to answer comments Smile

There are a few things you can do about the caching of your js files.
Look here:
thecrmgrid.wordpress.com/.../
and here:
ronaldlemmen.blogspot.com/.../...-and-caching.html

Steve Le Mon | Reply

4/25/2009 3:26:12 PM #

Hi Henry

I've been having a debate with Adi Katz about this technique and he has written the following blog about the issues I've been having and this code in particular. mscrm4ever.blogspot.com/.../...-script-loader.html

Taking his comments on board, I accept that eval and split/regex may be a bit slow, but the timing issues of his approach may be to dam fiddly to live with.

Then I had an eureka moment... I use your code to load my javascript files, but I want to keep the code within the forms to a minimum so I actual hold your dynamic script loader code in an external java script file and load it as follows:

var netRequest = new ActiveXObject("Msxml2.XMLHTTP");
netRequest.open("GET", "/isv/Company/Project/_scripts/DynamicScriptLoader.js", false);
netRequest.send(null);
eval(netRequest.responseText);

I then use the function to inject the other scripts I wish to load.

Instead of using the code within the load_script function, what is wrong with just using the four lines above as is to load script?

var netRequest = new ActiveXObject("Msxml2.XMLHTTP");

netRequest.open("GET", "/isv/Company/Project/_scripts/LoadCodeModule1.js", false);
netRequest.send(null);
eval(netRequest.responseText);

netRequest.open("GET", "/isv/Company/Project/_scripts/LoadCodeModule2.js", false);
netRequest.send(null);
eval(netRequest.responseText);

I've tested this out and it seems to work okay, so that's the difference between this and the function that parses every function or variable???

Getting confused!!!

4/25/2009 3:53:11 PM #

Hi Steve, nice additions!
I was not aware of the technique used by Adi Katz, I can surely understand your comment of the timing issues being fiddly.

The script in my post that parses every function, registers every function into the current namespace.
In my tests just executing the script untill line 6 (like below) did not work:

var x = new ActiveXObject("Msxml2.XMLHTTP");
x.open('GET', url, false);
x.send('');
eval(x.responseText);

That is what you are doing aren't you?. In my case the registering into the current namespace has to take place, I am not sure why it is working without this in your case.
I sure would like to know. What happened in my case was that the functions in my external file where not found....

Steve Le Mon | Reply

4/25/2009 4:24:49 PM #

The plot thinkens! Here is the code I've just re-tested in a lead.

*** This Code goes in the Lead Form OnLoad Event ***
var netRequest = new ActiveXObject("Msxml2.XMLHTTP");
netRequest.open("GET", "/isv/_scripts/lead.js", false);
netRequest.send(null);
eval(netRequest.responseText);

netRequest.open("GET", "/isv/_scripts/lead_functions.js", false);
netRequest.send(null);
eval(netRequest.responseText);

alert("in form_load calling Lead_OnLoad");
Lead_OnLoad();

*** Now create a javafile called lead.js in isv/_scripts/ with the following code ***
function Lead_OnLoad()
{
  alert("we are in the Lead_OnLoad()");
  
  alert("calling Lead_Functions1");
  Lead_Functions1()
  
  alert("calling Lead_Functions2");
  Lead_Functions2()
}
*** end of lead script ***

*** Now create a javafile called lead_functions.js in isv/_scripts/ with the following code ***
function Lead_Functions1()
{
  alert("this is lead function 1");
}

function Lead_Functions2()
{
  alert("this is lead function 2");
}

*** end of lead_functions script ***


Give this ago, it works for me and I'm wondering if I can streamline you code injector to those four lines in the form load event? I've shown that it would inject several files if needed, but most of the time there would only be 1 file.

Let me know how you get on as I think this would surfice.

Steve

4/26/2009 2:50:10 AM #

Hi Steve,

I took the time to try it out.
I must say I am surprised to find out that your suggestion works really well.
It removes the overhead of the parsing of the functions and/or variables, I tried it on a vpc, but will run it on our dev server soon.

Thank you for keeping at it.

I will however do it this way (using your sample code in your comment, also the two .js files in the /isv/_scripts/ directory):
In the form load of the Crm entity Form I will add this function at the top of the OnLoad function:

/*  This Code goes in the entity Form OnLoad Event  */

function InjectScript(scriptFile)
{
    var netRequest = new ActiveXObject("Msxml2.XMLHTTP");
    netRequest.open("GET", scriptFile, false);
    netRequest.send(null);
    eval(netRequest.responseText);
}

// Next I will call the function with the path to and the filename in the scriptFile parameter.
InjectScript('/isv/_scripts/lead.js');
InjectScript('/isv/_scripts/lead_functions.js');

alert("in form_load calling Lead_OnLoad");
Lead_OnLoad();

/*  End of the entity Form OnLoad Event   */

I will add this to my post, because I think it really is valueable.

4/26/2009 3:01:31 AM #

@Steve: I am curious as to what extend this change has affected your development caching issue?

Steve Le Mon | Reply

4/26/2009 5:47:58 AM #

You still have to clear out your cache if you change any of the remote javascript as IE caches the physical files. The trick of adding a ?nochache + math.random() does not work with this kind of injector.

It only seems to work with script loaded into the header like such:
script2Load.src = "/ISV/Entity/account.js?nocach=" + Math.random();
document.getElementsByTagName("HEAD")[0].appendChild(script2Load);

But this is a small price to pay for the convenience one gets. I've been trying out the version from Adi' blog and I just can't get it work as well as yours. I've decide to do it your way (minus the function parsing) as its quick enough for me.

Thanks for taking the time to sanity-check me.

Cheers

4/26/2009 1:36:18 PM #

Good to hear you are pleased.
My caching solution is quite simple, I do not go and tweak IIS, although you could do that, my solution are .bat files, simple but effective.
I got .bat files for doing iisreset, restart Async service and emtying the cache. This way I am in control over when these actions take place and when I execute them they are quick.

Here is the script inside my emptyIECache.bat:

RunDll32.exe InetCpl.cpl,ClearMyTracksByProcess 255

7/18/2009 12:41:23 AM #

I have tried your example above (the final solution, same as on dynamics community) and I keep getting an "Object Expected" error when calling one of the functions in the external javascript file. I know for a fact that the function exists and I don't get any errors loading the .js file. Any ideas?

8/6/2009 10:39:57 AM #

@ Troy,
Looks like the function in your js file cannot be found.
Most of the time this is because you need to empty the Internet Explorer cache.

sorry for the delay in answering your question

8/25/2009 3:18:55 PM #

Thank you!
Glad it is appreciated

5/2/2010 7:15:45 AM #

I was trying some of these methods today, but kept running into that problem with the cache.  I combined a couple of these methods into one that seemed to work for me.

dynamicscrmtips.blogspot.com/.../...t-in-your.html

Thanks!

John H | Reply

8/13/2010 1:31:10 AM #

This is great, but like Troy above, I get the "Object Expected" error. It seems to be a scoping issue not a caching issue. I can get it to work by taking the eval out of the function:

function InjectScript(scriptFile)
{
var netRequest = new ActiveXObject("Msxml2.XMLHTTP");
netRequest.open("GET", scriptFile, false);
netRequest.send(null);
return netRequest.responseText;
}

eval( InjectScript('/isv/_scripts/lead.js') );
eval( InjectScript('/isv/_scripts/lead_functions.js') );

Lead_OnLoad();

Khaled El Sheikh Oman | Reply

1/14/2013 3:50:13 PM #

Thanks alot for your topic

Add comment




  Country flag
biuquote
  • Comment
  • Preview
Loading