Thursday, April 23, 2009

Composing javascript external files and the Microsoft ajax framework.

I have seen a small video presentation about the new ASP.Net 3.5 SP1 Script Combining feature. Basically you take a bunch of scripts (like the ones from AjaxControlToolkit) and you bundle them together in a single cacheable file. This decreases the number of concurrent connections on your production site.

The problem was that you had to use some component to see what script files were being loaded and then manually add them to the ScriptManager CompositeScript Scripts collection. And this applies only to correctly registered scripts, not stuff that is embedded in the HTML or what not. Isn't it easier to just parse the generated HTML and then replace the script tags, I thought?

Well, I did a small Response filter/IHttpHandler in about two hours. It would take all consecutive external file references and combine them in a single cached and cacheable call. Then I tested it with Asynchronous postbacks. Epic Fail! The main problem was that the combined scripts would re-register themselves at postback and throw all kinds of errors therefore. But how did they know if they were registered or not before?!

I vaguely remembered an old post of mine about the notifyScriptLoaded function that must be called at the end of every external javascript registrations. Examining the system a little I realised that the flow was like this:
  1. register the script (either include it in the HTML in regular render or sending it to the UpdatePanel javascript engine to be registered as a new dynamically script element in the Head page section)
  2. if the registration is an async dynamic one, check in an array if the script is loaded and if it is, don't register it
  3. in the script call Sys.Application.notifyScriptLoaded() which would take the src of the script element and use it as a key in the above mentioned array to declare it has been loaded

Of course, that means combining all the scripts in a single file registers only that file and you get the original files registered again. Then you get errors like 'Type [something] has already been registered.' or (because there are more than one script file bundled together) 'The script [something] contains multiple calls to Sys.Application.notifyScriptLoaded(). Only one is allowed.'.

Well, I did manage to solve the problem by following these steps:
  1. forcefully remove Sys.Application.notifyScriptLoaded() from bundled scripts
  2. add Sys.Application.notifyScriptLoaded() at the end of all scripts
  3. surround the various scripts with a code that looks like
    sb.AppendFormat(@"if (!window.Sys||!Sys._ScriptLoader||!Sys._ScriptLoader.isScriptLoaded('{0}')) {{
    if (window.Array&&Array.add&&window.Sys&&Sys._ScriptLoader) Array.add(Sys._ScriptLoader._getLoadedScripts(), '{2}');
    ", HttpUtility.HtmlEncode(src), content.Replace("Sys.Application.notifyScriptLoaded();", ";"),src);

With problem solved, the AutoScriptCombiner works, but it feels wrong. I wanted to post it on CodePlex, you see, but in the end I've decided not to. However I did learn something about how the Asp.Net Ajax framework functions internally and I wanted to share it with you.