Thoughts, Tips and Tricks on what I'm currently do for a living. Currently most of my spare time is spent on contributing to Akka.NET.

Sunday, July 27, 2008

How to include scripts after asp.net ajax framework's

When registering script includes using ScriptManager.RegisterClientScriptInclude("myFile.js") the files are always included before the frameworks. This causes errors if your code depends on the ajax framework's. Errors like "Type is not defined" are common since the first line in the included script file often tries to register a namespace: Type.registerNamespace('My.Namespace');

On the other hand, files registered as ScriptReferences on the ScriptManagerProxy will be included AFTER the framework's includes.

<asp:ScriptManagerProxy runat="server" id="ScriptManagerProxy1">
    <Scripts><asp:ScriptReference Path="~/anotherFile.js" /></Scripts>
</asp:ScriptManagerProxy>

Not exactly the expected behavior and certainly not what we want.

When calling ScriptManager.RegisterClientScriptInclude() it delegates to the class ClientScriptManager. This class holds, internally, a list of registered scripts, and new entries ends up at the end. So the call ScriptManager.RegisterClientScriptInclude("myFile.js") will put myFile.js at the end of that list. Like this:

ClientScriptManagerClientScripts = { ..., "~/myFile.js" }

The references in the ScriptManagerProxies are handled a bit differently. The ScriptManager will, on the event PagePreRenderComplete, collect all references from the proxies in a list, and first in that list put the ajax framework's files.

List in ScriptManager = { ajax framework files, "~/anotherFile.js" }

After being collected the files are registered with the ClientScriptManager and are appended at the end of it's list. Since we registered myFile.js before PagePreRenderComplete (which occurs late in the asp.net page's life cycle), myFile.js is before the framework's files:

ClientScriptManagerClientScripts = { ..., "~/myFile.js", ..., ajax framework files, "~/anotherFile.js"}

These are rendered to the page in this order and it explains why anotherFile.js may use the framework directly and myFile.js not.

Solution 1. Use a proxy

The simplest solution is to put a ScriptManagerProxy on the page (or user control, or MasterPage) and either, as shown above declare a ScriptReference, or add one by code:

ScriptManagerProxy1.Scripts.Add(new ScriptReference("~/myFile.js"));

Solution 2. Register the file after PagePreRenderComplete

If you somehow manage to register myFile.js after PagePreRenderComplete (i.e. after ScriptControl has added all files) it will end up after the framework's files. But you'll need to do it before the rendering takes place. Between PreRender and Render phases is the SaveViewState phase. If we override SaveViewState on a control (Page is also a control) and registers myFile.js there it will be added to the list after the framework's files. This is sort of a hack and it's not guaranteed to work when new versions of the framework are released, but: It works.

protected override object SaveViewState()
{
    ScriptManager.RegisterClientScriptInclude(this,GetType(),"myFile","~/myFile.js");
    return base.SaveViewState();
}

Solution 3. Your own ScriptManager

This is, at least to me, the most elegant solution. Basically: you create the class MyScriptManager, which derives from ScriptManager; you replace the ScriptManager on the page (or user control, or MasterPage) with MyScriptManager; add the method RegisterClientScriptInclude(string url) to MyScriptManager and by some magic inside MyScriptManager makes sure that files registered thru the new method will be rendered after the ajax framework's includes.

When the ScriptManager collects all the ScriptReferences from the proxies (as explained above) it also collects ScriptReferences from all controls that have been registered with the ScriptManager as ScriptControls, by calling the method GetScriptReferences, that ScriptControls must implement. These files will be added after the proxies' files.

This is what we'll do: The new RegisterClientScriptInclude method will only add the url to a list, and not register it anywhere else. We let MyScriptManager be a ScriptControl (i.e. implement IScriptControl) and register itself as such. In the method GetScriptReferences we will, when requested, supply the list of registered files, and they will end up after the framework's.

Not much code is needed.

public class MyScriptManager : ScriptManager, IScriptControl
{
    private List<string> _registeredScripts = new List<string>();

    public virtual void RegisterClientScriptInclude(string url)
    {
        _registeredScripts.Add(url);
    }

    protected override void OnPreRender(EventArgs e)
    {
        //Register this instance as a ScriptControl.
        RegisterScriptControl(this);
        base.OnPreRender(e);
    }

    public new static MyScriptManager GetCurrent(Page page)
    {
        return (MyScriptManager) ScriptManager.GetCurrent(page);
    }

    #region IScriptControl Members
    IEnumerable<ScriptDescriptor> IScriptControl.GetScriptDescriptors()
    {
        return null;
    }

    IEnumerable<ScriptReference> IScriptControl.GetScriptReferences()
    {
        //For each element in _registeredScripts create a
        //ScriptReference and return the IEnumerable
        return _registeredScripts.ConvertAll(s => new ScriptReference(s));
    }
    #endregion
}

Please note that I've simplified the code in order to show the concept. In production code you'll want to encapsulate the list in a public property that creates the list if needed. And you want two protected virtual versions of GetScriptDescriptors and GetScriptReferences that the existing ones will call.

To register a file to be included after the framework's files you use:

MyScriptManager.GetCurrent(Page).RegisterClientScriptInclude("~/myFile.js");

If you're using AjaxControlToolkit change the inheritance to ToolkitScriptManager.

Note the order the files are included:

  1. ScriptManager.RegisterClientScriptInclude registered files
  2. Ajax framework's files
  3. All ScriptManagerProxy ScriptReferences
  4. MyScriptManager.RegisterClientScriptInclude registered files

If you want your files to be included between 2 and three you're in deep water. It's probably doable but far from trivial, since ScriptManager is pretty closed for extension. It would be nice if Microsoft adhered the Open/Closed-principle a bit more. :)