Rendering ASP.NET Script References into the Html Header

Posted by Rick Strahl on West-Wind See other posts from West-Wind or by Rick Strahl
Published on Sat, 02 Jan 2010 11:26:11 GMT Indexed on 2010/03/07 23:12 UTC
Read the original article Hit count: 1181

Filed under:
|

One thing that I’ve come to appreciate in control development in ASP.NET that use JavaScript is the ability to have more control over script and script include placement than ASP.NET provides natively. Specifically in ASP.NET you can use either the ClientScriptManager or ScriptManager to embed scripts and script references into pages via code.

This works reasonably well, but the script references that get generated are generated into the HTML body and there’s very little operational control for placement of scripts. If you have multiple controls or several of the same control that need to place the same scripts onto the page it’s not difficult to end up with scripts that render in the wrong order and stop working correctly. This is especially critical if you load script libraries with dependencies either via resources or even if you are rendering referenced to CDN resources.

Natively ASP.NET provides a host of methods that help embedding scripts into the page via either Page.ClientScript or the ASP.NET ScriptManager control (both with slightly different syntax):

  • RegisterClientScriptBlock
    Renders a script block at the top of the HTML body and should be used for embedding callable functions/classes.
  • RegisterStartupScript
    Renders a script block just prior to the </form> tag and should be used to for embedding code that should execute when the page is first loaded. Not recommended – use jQuery.ready() or equivalent load time routines.
  • RegisterClientScriptInclude
    Embeds a reference to a script from a url into the page.
  • RegisterClientScriptResource
    Embeds a reference to a Script from a resource file generating a long resource file string

All 4 of these methods render their <script> tags into the HTML body. The script blocks give you a little bit of control by having a ‘top’ and ‘bottom’ of the document location which gives you some flexibility over script placement and precedence. Script includes and resource url unfortunately do not even get that much control – references are simply rendered into the page in the order of declaration.

The ASP.NET ScriptManager control facilitates this task a little bit with the abililty to specify scripts in code and the ability to programmatically check what scripts have already been registered, but it doesn’t provide any more control over the script rendering process itself. Further the ScriptManager is a bear to deal with generically because generic code has to always check and see if it is actually present.

Some time ago I posted a ClientScriptProxy class that helps with managing the latter process of sending script references either to ClientScript or ScriptManager if it’s available. Since I last posted about this there have been a number of improvements in this API, one of which is the ability to control placement of scripts and script includes in the page which I think is rather important and a missing feature in the ASP.NET native functionality.

Handling ScriptRenderModes

One of the big enhancements that I’ve come to rely on is the ability of the various script rendering functions described above to support rendering in multiple locations:

/// <summary>
/// Determines how scripts are included into the page
/// </summary>
public enum ScriptRenderModes
{
    /// <summary>
    /// Inherits the setting from the control or from the ClientScript.DefaultScriptRenderMode
    /// </summary>
    Inherit,
    /// Renders the script include at the location of the control
    /// </summary>
    Inline,
    /// <summary>
    /// Renders the script include into the bottom of the header of the page
    /// </summary>
    Header,
    /// <summary>
    /// Renders the script include into the top of the header of the page
    /// </summary>
    HeaderTop,
    /// <summary>
    /// Uses ClientScript or ScriptManager to embed the script include to
    /// provide standard ASP.NET style rendering in the HTML body.
    /// </summary>
    Script,
    /// <summary>
    /// Renders script at the bottom of the page before the last Page.Controls
    /// literal control. Note this may result in unexpected behavior 
    /// if /body and /html are not the last thing in the markup page.
    /// </summary>
    BottomOfPage        
}

This enum is then applied to the various Register functions to allow more control over where scripts actually show up. Why is this useful? For me I often render scripts out of control resources and these scripts often include things like a JavaScript Library (jquery) and a few plug-ins. The order in which these can be loaded is critical so that jQuery.js always loads before any plug-in for example.

Typically I end up with a general script layout like this:

  • Core Libraries- HeaderTop
  • Plug-ins: Header
  • ScriptBlocks: Header or Script depending on other dependencies

There’s also an option to render scripts and CSS at the very bottom of the page before the last Page control on the page which can be useful for speeding up page load when lots of scripts are loaded.

The API syntax of the ClientScriptProxy methods is closely compatible with ScriptManager’s using static methods and control references to gain access to the page and embedding scripts.

For example, to render some script into the current page in the header:

// Create script block in header
ClientScriptProxy.Current.RegisterClientScriptBlock(this, typeof(ControlResources),
                "hello_function", "function helloWorld() { alert('hello'); }", true,
                ScriptRenderModes.Header);

// Same again - shouldn't be rendered because it's the same id
ClientScriptProxy.Current.RegisterClientScriptBlock(this, typeof(ControlResources),
         "hello_function", "function helloWorld() { alert('hello'); }", true,
         ScriptRenderModes.Header);

// Create a second script block in header
ClientScriptProxy.Current.RegisterClientScriptBlock(this, typeof(ControlResources),
    "hello_function2", "function helloWorld2() { alert('hello2'); }", true,
    ScriptRenderModes.Header);


// This just calls ClientScript and renders into bottom of document
ClientScriptProxy.Current.RegisterStartupScript(this,typeof(ControlResources),
                "call_hello", "helloWorld();helloWorld2();", true);

which generates:

<html xmlns="http://www.w3.org/1999/xhtml" >
<head><title>
</title>
<script type="text/javascript">
function helloWorld() { alert('hello'); }
</script>

<script type="text/javascript">
function helloWorld2() { alert('hello2'); }
</script>
</head>
<body>
…    
<script type="text/javascript">
//<![CDATA[
helloWorld();helloWorld2();//]]>
</script>
</form>
</body>
</html>

Note that the scripts are generated into the header rather than the body except for the last script block which is the call to RegisterStartupScript. In general I wouldn’t recommend using RegisterStartupScript – ever. It’s a much better practice to use a script base load event to handle ‘startup’ code that should fire when the page first loads. So instead of the code above I’d actually recommend doing:

ClientScriptProxy.Current.RegisterClientScriptBlock(this, typeof(ControlResources),
    "call_hello", "$().ready( function() { alert('hello2'); });", true,
    ScriptRenderModes.Header);

assuming you’re using jQuery on the page.

For script includes from a Url the following demonstrates how to embed scripts into the header. This example injects a jQuery and jQuery.UI script reference from the Google CDN then checks each with a script block to ensure that it has loaded and if not loads it from a server local location:

// load jquery from CDN
ClientScriptProxy.Current.RegisterClientScriptInclude(this, typeof(ControlResources),
                "http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js",
                ScriptRenderModes.HeaderTop);

// check if jquery loaded - if it didn't we're not online
string scriptCheck =
    @"if (typeof jQuery != 'object')  
        document.write(unescape(""%3Cscript src='{0}' type='text/javascript'%3E%3C/script%3E""));";

string jQueryUrl = ClientScriptProxy.Current.GetWebResourceUrl(this, typeof(ControlResources),
                ControlResources.JQUERY_SCRIPT_RESOURCE);            
ClientScriptProxy.Current.RegisterClientScriptBlock(this, typeof(ControlResources),
                "jquery_register", string.Format(scriptCheck,jQueryUrl),true,
                ScriptRenderModes.HeaderTop);                            

// Load jquery-ui from cdn
ClientScriptProxy.Current.RegisterClientScriptInclude(this, typeof(ControlResources),
                "http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/jquery-ui.min.js",
                ScriptRenderModes.Header);

// check if we need to load from local
string jQueryUiUrl = ResolveUrl("~/scripts/jquery-ui-custom.min.js"); 
ClientScriptProxy.Current.RegisterClientScriptBlock(this, typeof(ControlResources),
    "jqueryui_register", string.Format(scriptCheck, jQueryUiUrl), true,
    ScriptRenderModes.Header); 


// Create script block in header
ClientScriptProxy.Current.RegisterClientScriptBlock(this, typeof(ControlResources),
                "hello_function", "$().ready( function() { alert('hello'); });", true,
                ScriptRenderModes.Header);

which in turn generates this HTML:

<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
    if (typeof jQuery != 'object')
        document.write(unescape("%3Cscript src='/WestWindWebToolkitWeb/WebResource.axd?d=DIykvYhJ_oXCr-TA_dr35i4AayJoV1mgnQAQGPaZsoPM2LCdvoD3cIsRRitHKlKJfV5K_jQvylK7tsqO3lQIFw2&t=633979863959332352' type='text/javascript'%3E%3C/script%3E"));
</script>
<title>
</title>
<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/jquery-ui.min.js" type="text/javascript"></script>
<script type="text/javascript">
    if (typeof jQuery != 'object')
        document.write(unescape("%3Cscript src='/WestWindWebToolkitWeb/scripts/jquery-ui-custom.min.js' type='text/javascript'%3E%3C/script%3E"));
</script>

<script type="text/javascript">
    $().ready(function() { alert('hello'); });
</script>
</head>
<body>
…
</body> </html>

As you can see there’s a bit more control in this process as you can inject both script includes and script blocks into the document at the top or bottom of the header, plus if necessary at the usual body locations. This is quite useful especially if you create custom server controls that interoperate with script and have certain dependencies. The above is a good example of a useful switchable routine where you can switch where scripts load from by default – the above pulls from Google CDN but a configuration switch may automatically switch to pull from the local development copies if your doing development for example.

How does it work?

As mentioned the ClientScriptProxy object mimicks many of the ScriptManager script related methods and so provides close API compatibility with it although it contains many additional overloads that enhance functionality. It does however work against ScriptManager if it’s available on the page, or Page.ClientScript if it’s not so it provides a single unified frontend to script access. There are however many overloads of the original SM methods like the above to provide additional functionality.

The implementation of script header rendering is pretty straight forward – as long as a server header (ie. it has to have runat=”server” set) is available. Otherwise these routines fall back to using the default document level insertions of ScriptManager/ClientScript. Given that there is a server header it’s relatively easy to generate the script tags and code and append them to the header either at the top or bottom. I suspect Microsoft didn’t provide header rendering functionality precisely because a runat=”server” header is not required by ASP.NET so behavior would be slightly unpredictable. That’s not really a problem for a custom implementation however.

Here’s the RegisterClientScriptBlock implementation that takes a ScriptRenderModes parameter to allow header rendering:

/// <summary>
/// Renders client script block with the option of rendering the script block in
/// the Html header
/// 
/// For this to work Header must be defined as runat="server"
/// </summary>
/// <param name="control">any control that instance typically page</param>
/// <param name="type">Type that identifies this rendering</param>
/// <param name="key">unique script block id</param>
/// <param name="script">The script code to render</param>
/// <param name="addScriptTags">Ignored for header rendering used for all other insertions</param>
/// <param name="renderMode">Where the block is rendered</param>
public void RegisterClientScriptBlock(Control control, Type type, string key, string script, bool addScriptTags, ScriptRenderModes renderMode)
{
    if (renderMode == ScriptRenderModes.Inherit)
        renderMode = DefaultScriptRenderMode;

    if (control.Page.Header == null || 
        renderMode != ScriptRenderModes.HeaderTop && 
        renderMode != ScriptRenderModes.Header &&
        renderMode != ScriptRenderModes.BottomOfPage)
    {
        RegisterClientScriptBlock(control, type, key, script, addScriptTags);
        return;
    }

    // No dupes - ref script include only once
    const string identifier = "scriptblock_";
    if (HttpContext.Current.Items.Contains(identifier + key))
        return;
    HttpContext.Current.Items.Add(identifier + key, string.Empty);

    StringBuilder sb = new StringBuilder();

    // Embed in header
    sb.AppendLine("\r\n<script type=\"text/javascript\">");
    sb.AppendLine(script);            
    sb.AppendLine("</script>");

    int? index = HttpContext.Current.Items["__ScriptResourceIndex"] as int?;
    if (index == null)
        index = 0;

    if (renderMode == ScriptRenderModes.HeaderTop)
    {
        control.Page.Header.Controls.AddAt(index.Value, new LiteralControl(sb.ToString()));
        index++;
    }
    else if(renderMode == ScriptRenderModes.Header)
        control.Page.Header.Controls.Add(new LiteralControl(sb.ToString()));
    else if (renderMode == ScriptRenderModes.BottomOfPage)
        control.Page.Controls.AddAt(control.Page.Controls.Count-1,new LiteralControl(sb.ToString()));

    
    HttpContext.Current.Items["__ScriptResourceIndex"] = index;            
}

Note that the routine has to keep track of items inserted by id so that if the same item is added again with the same key it won’t generate two script entries. Additionally the code has to keep track of how many insertions have been made at the top of the document so that entries are added in the proper order.

The RegisterScriptInclude method is similar but there’s some additional logic in here to deal with script file references and ClientScriptProxy’s (optional) custom resource handler that provides script compression

/// <summary>
/// Registers a client script reference into the page with the option to specify
/// the script location in the page
/// </summary>
/// <param name="control">Any control instance - typically page</param>
/// <param name="type">Type that acts as qualifier (uniqueness)</param>
/// <param name="url">the Url to the script resource</param>
/// <param name="ScriptRenderModes">Determines where the script is rendered</param>
public void RegisterClientScriptInclude(Control control, Type type, string url, ScriptRenderModes renderMode)
{
    const string STR_ScriptResourceIndex = "__ScriptResourceIndex";

    if (string.IsNullOrEmpty(url))
        return;

    if (renderMode == ScriptRenderModes.Inherit)
        renderMode = DefaultScriptRenderMode;

    // Extract just the script filename
    string fileId = null;
    
    
    // Check resource IDs and try to match to mapped file resources
    // Used to allow scripts not to be loaded more than once whether
    // embedded manually (script tag) or via resources with ClientScriptProxy
    if (url.Contains(".axd?r="))
    {
        string res = HttpUtility.UrlDecode( StringUtils.ExtractString(url, "?r=", "&", false, true) );
        foreach (ScriptResourceAlias item in ScriptResourceAliases)
        {
            if (item.Resource == res)
            {
                fileId = item.Alias + ".js";
                break;
            }
        }
        if (fileId == null)
            fileId = url.ToLower();
    }
    else
        fileId = Path.GetFileName(url).ToLower();

    // No dupes - ref script include only once
    const string identifier = "script_";
    if (HttpContext.Current.Items.Contains( identifier + fileId ) )
        return;
    
    HttpContext.Current.Items.Add(identifier + fileId, string.Empty);

    // just use script manager or ClientScriptManager
    if (control.Page.Header == null || renderMode == ScriptRenderModes.Script || renderMode == ScriptRenderModes.Inline)
    {
        RegisterClientScriptInclude(control, type,url, url);
        return;
    }

    // Retrieve script index in header            
    int? index = HttpContext.Current.Items[STR_ScriptResourceIndex] as int?;
    if (index == null)
        index = 0;

    StringBuilder sb = new StringBuilder(256);

    url = WebUtils.ResolveUrl(url);

    // Embed in header
    sb.AppendLine("\r\n<script src=\"" + url + "\" type=\"text/javascript\"></script>");

    if (renderMode == ScriptRenderModes.HeaderTop)
    {
        control.Page.Header.Controls.AddAt(index.Value, new LiteralControl(sb.ToString()));
        index++;
    }
    else if (renderMode == ScriptRenderModes.Header)
        control.Page.Header.Controls.Add(new LiteralControl(sb.ToString()));
    else if (renderMode == ScriptRenderModes.BottomOfPage)
        control.Page.Controls.AddAt(control.Page.Controls.Count-1, new LiteralControl(sb.ToString()));                

    HttpContext.Current.Items[STR_ScriptResourceIndex] = index;
}

There’s a little more code here that deals with cleaning up the passed in Url and also some custom handling of script resources that run through the ScriptCompressionModule – any script resources loaded in this fashion are automatically cached based on the resource id. Raw urls extract just the filename from the URL and cache based on that. All of this to avoid doubling up of scripts if called multiple times by multiple instances of the same control for example or several controls that all load the same resources/includes.

Finally RegisterClientScriptResource utilizes the previous method to wrap the WebResourceUrl as well as some custom functionality for the resource compression module:

/// <summary>
/// Returns a WebResource or ScriptResource URL for script resources that are to be
/// embedded as script includes.
/// </summary>
/// <param name="control">Any control</param>
/// <param name="type">A type in assembly where resources are located</param>
/// <param name="resourceName">Name of the resource to load</param>
/// <param name="renderMode">Determines where in the document the link is rendered</param>
public void RegisterClientScriptResource(Control control, Type type, 
                                         string resourceName, 
                                         ScriptRenderModes renderMode)
{ 
    string resourceUrl = GetClientScriptResourceUrl(control, type, resourceName);
    RegisterClientScriptInclude(control, type, resourceUrl, renderMode);
}
/// <summary>
/// Works like GetWebResourceUrl but can be used with javascript resources
/// to allow using of resource compression (if the module is loaded).
/// </summary>
/// <param name="control"></param>
/// <param name="type"></param>
/// <param name="resourceName"></param>
/// <returns></returns>
public string GetClientScriptResourceUrl(Control control, Type type, string resourceName)
{            
    #if IncludeScriptCompressionModuleSupport

    // If wwScriptCompression Module through Web.config is loaded use it to compress 
    // script resources by using wcSC.axd Url the module intercepts
    if (ScriptCompressionModule.ScriptCompressionModuleActive) 
    {
        string url = "~/wwSC.axd?r=" + HttpUtility.UrlEncode(resourceName);
        if (type.Assembly != GetType().Assembly)
            url += "&t=" + HttpUtility.UrlEncode(type.FullName);
        
        return WebUtils.ResolveUrl(url);
    }
    
    #endif

    return control.Page.ClientScript.GetWebResourceUrl(type, resourceName);
}

This code merely retrieves the resource URL and then simply calls back to RegisterClientScriptInclude with the URL to be embedded which means there’s nothing specific to deal with other than the custom compression module logic which is nice and easy.

What else is there in ClientScriptProxy?

ClientscriptProxy also provides a few other useful services beyond what I’ve already covered here:

Transparent ScriptManager and ClientScript calls

ClientScriptProxy includes a host of routines that help figure out whether a script manager is available or not and all functions in this class call the appropriate object – ScriptManager or ClientScript – that is available in the current page to ensure that scripts get embedded into pages properly. This is especially useful for control development where controls have no control over the scripting environment in place on the page.

RegisterCssLink and RegisterCssResource
Much like the script embedding functions these two methods allow embedding of CSS links. CSS links are appended to the header or to a form declared with runat=”server”.

LoadControlScript

Is a high level resource loading routine that can be used to easily switch between different script linking modes. It supports loading from a WebResource, a url or not loading anything at all. This is very useful if you build controls that deal with specification of resource urls/ids in a standard way.

Check out the full Code

You can check out the full code to the ClientScriptProxyClass here:

ClientScriptProxy.cs

ClientScriptProxy Documentation (class reference)

Note that the ClientScriptProxy has a few dependencies in the West Wind Web Toolkit of which it is part of. ControlResources holds a few standard constants and script resource links and the ScriptCompressionModule which is referenced in a few of the script inclusion methods.

There’s also another useful ScriptContainer companion control  to the ClientScriptProxy that allows scripts to be placed onto the page’s markup including the ability to specify the script location and script minification options.

You can find all the dependencies in the West Wind Web Toolkit repository:

West Wind Web Toolkit Repository

West Wind Web Toolkit Home Page

© Rick Strahl, West Wind Technologies, 2005-2010
Posted in ASP.NET  JavaScript  
kick it on DotNetKicks.com

© West-Wind or respective owner

Related posts about ASP.NET

Related posts about JavaScript