Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

Client Script Resources in ASP.NET Controls revisited


:P
On this page:

I've been mucking around a bit with my ClientScriptProxy class over the last couple of days as I'm reworking some of my controls in my client library. Many of the controls optionally can use resources and are fully self contained so they load all scripts (optionally) including jQuery which is a core library that critically must be loaded before any add-ins or other dependencies are loaded.

So the scenario for me is this: I have a custom control that uses some library script resources  - for example jQuery - that can get automatically loaded into the page, as well as some custom resources that belong to my own library that depend on jQuery (my client library). I then need to also be able to manage the scripts in such a way that if the page developer adds additional libraries which might also depend on jQuery can still work correctly.

Sound confusing - well it is, and this is why many people will suggest outright - don't embed script code, but just reference it externally. While that works it can also be a pain if you're working on many projects and you need to keep versions of these libraries in sync which is something I struggle with a lot as I have a ton of projects that use these components.

So as you might guess I'm pretty happy with using Resources, while at the same fully realizing that not everybody shares that sentiment. One big requirement of any resource usage in my components is that you have to be able to NOT use the resources - either specifying a manual URL instead or not loading anything at all. And this is what this post is about (in a round about way <s>).

ClientScript or ScriptManager?

Ah you say - isn't that what ClientScript and ScriptManager are for and yes to some degree they do provide that functionality - but only in a fairly rigid manner because you get no choice on where that script code is dropped. If you want to build controls that include script resources and potentially want to interact with other script on the page there's usually a bit more flexibility involved.

Think about this scenario: Let's say I want to drop my control - which also loads jQuery (optionally) on the page -  and then add my own script library AND also be able to manually drop a few jQuery.ui components onto the page.

So in code I might do something like this:

protected override void OnLoad(EventArgs e)
{
    ClientScript.RegisterClientScriptResource(typeof(ControlResources), 
                                              ControlResources.JQUERY_SCRIPT_RESOURCE);
    ClientScript.RegisterClientScriptResource(typeof(ControlResources), 
                                              ControlResources.WWJQUERY_SCRIPT_RESOURCE);
}

If I do this I'll end up with this HTML:

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Test Page</title>
    <script src="scripts/ui.core.js" type="text/javascript"></script>
    <script src="scripts/ui.draggable.js" type="text/javascript"></script>
    <link href="Standard.css" rel="stylesheet" type="text/css" />
</head>
<body>    
    <form name="form1" method="post" action="MethodCallback.aspx" id="form2">
<div>
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwUKMTcxMzE4OTIxM2RkaKl2DV4GkXjRsmqC81jFi3+ZZy8=" />
</div>

<script src="wwSC.axd?r=Westwind.Web.Controls.Resources.jquery.js" type="text/javascript"></script>
<script src="wwSC.axd?r=Westwind.Web.Controls.Resources.ww.jquery.js" type="text/javascript"></script>
<div>

where the wcSC.axd is my a replacement for what would be WebResource.axd or ScriptResource.axd.

There's a big problem in that the Resource loaded libraries loaded AFTER the jQuery plug-ins which depend on jQuery so the above results in JavaScript errors when the page loads.

One alternative that is perfectly reasonable is to simply not use the jQuery resource (and I can do that easily do that and I'll come back to that) and manually put it into the page in the right place, but then you do give up some of the advantages of embedded resources, like auto-compression, minimization and keeping the version always up to date.

ClientScriptProxy - Script Component Abstraction

Some time ago I created a ClientScriptProxy component that acts as go between between ScriptManager and the Page.ClientScript object, detecting whether script manager is available and if not using ClientScript or if available my own custom script compression module that I talked about yesterday.

For a control developer something like ClientScriptProxy is absolutely necessary if you want to build controls that recognize ASP.NET AJAX  and can take advantage of script compression and a few other features. I've been using the ClientScriptProxy for all of my control development talking to it rather than the ScriptManager or CleintScript object directly.

One big advantage of this wrapper is that I don't have to worry about which component is available - the ClientScriptProxy figures that out and it'll use the right component under the covers. One more level of abstraction. But more importantly this abstraction also allows me to add some additional functionality.

One of the features I added some time ago is to allow Script resources to get embedded into the header rather than into the content as SM and CS do. There are a couple of methods that match the CS and SM methods that RegisterClientScriptIncludeInHeader() and RegisterClientScriptResourceInHeader().

/// <summary>
/// Registers a client script reference in the header instead of inside the document
/// before the form tag. 
/// 
/// The script tag is embedded at the bottom of the HTML header.
/// </summary>
/// <param name="control"></param>
/// <param name="type"></param>
/// <param name="Url"></param>
/// <param name="bool loadAtTop">Determines if the resource is laoded at the to of the header or the bottom</param>
public void RegisterClientScriptIncludeInHeader(Control control, Type type, string Url, bool loadAtTop)
{
    if (control.Page.Header == null)
    {
        this.RegisterClientScriptInclude(control, type, Url, Url);
        return;
    }

    // *** Keep duplicates from getting written
    const string identifier = "headerscript_";
    if (HttpContext.Current.Items.Contains(identifier + Url.ToLower()))
        return;
    else
        HttpContext.Current.Items.Add(identifier + Url.ToLower(), string.Empty);

    // *** Retrieve script index in header
    object val = HttpContext.Current.Items["__ScriptResourceIndex"];
    int index = 0;
    if (val != null)
        index = (int)val;

    StringBuilder sb = new StringBuilder(256);

    // *** Embed in header
    sb.AppendLine(@"<script src=""" + Url + @""" type=""text/javascript""></script>");

    if (loadAtTop)
        control.Page.Header.Controls.AddAt(index, new LiteralControl(sb.ToString()));
    else
        control.Page.Header.Controls.Add(new LiteralControl(sb.ToString()));

    index++;
    HttpContext.Current.Items["__ScriptResourceIndex"] = index;
}
/// <summary>
/// Inserts a client script resource into the Html header of the page rather 
/// than into the body as RegisterClientScriptInclude does.
/// 
/// Scripts references are embedded at the bottom of the Html Header after
/// any manually added header scripts.
/// </summary>
/// <param name="control"></param>
/// <param name="type"></param>
/// <param name="resourceName"></param>
/// <param name="topOfHeader">Determines whether script gets embedded at the beginning or end of header</param>
public void RegisterClientScriptResourceInHeader(Control control, Type type,  string resourceName, bool topOfHeader)
{
    // Can't do this if there's no header to work with - degrade
    if (control.Page.Header == null)
    {
        this.RegisterClientScriptResource(control, type, resourceName);
        return;
    }

    // *** Keep duplicates from getting written
    const string identifier = "headerscript_";
    if (HttpContext.Current.Items.Contains(identifier + resourceName))
        return;
    else
        HttpContext.Current.Items.Add(identifier + resourceName, string.Empty);

    object val = HttpContext.Current.Items["__ScriptResourceIndex"];
    int index = 0;
    if (val != null)
        index = (int)val;

    // *** Retrieve the Resource URL adjusted for MS Ajax, wwScriptCompression or stock ClientScript
    string script = GetClientScriptResourceUrl(control.Page, typeof(ControlResources), resourceName);
    
    // *** Embed in header
    StringBuilder sb = new StringBuilder(200);

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

    if (control.Page.Header == null)
        throw new InvalidOperationException("Can't add resources to page: missing <head runat=\"server\"> tag in the page.");

    if (topOfHeader)
        control.Page.Header.Controls.AddAt(index, new LiteralControl(sb.ToString()));
    else
        control.Page.Header.Controls.Add(new LiteralControl(sb.ToString()));
    
    index++;
    HttpContext.Current.Items["__ScriptResourceIndex"] = index;
}

        /// <summary>
/// Inserts a client script resource into the Html header of the page rather 
/// than into the body as RegisterClientScriptInclude does.
/// 
/// Scripts references are embedded at the bottom of the Html Header after
/// any manually added header scripts.
/// </summary>
/// <param name="control"></param>
/// <param name="type"></param>
/// <param name="resourceName"></param>        
public void RegisterClientScriptResourceInHeader(Control control, Type type, string resourceName)
{
    this.RegisterClientScriptResourceInHeader(control, type, resourceName, false);
}
 

With these two methods in place I can now register resources more precisely like this:

protected override void OnLoad(EventArgs e)
{
    // Register in page header at the top
    ClientScriptProxy.Current.RegisterClientScriptResourceInHeader(this.Page,typeof(ControlResources), 
                                                                   ControlResources.JQUERY_SCRIPT_RESOURCE,
                                                                   true);

    // Register in the page header on the bottom
    ClientScriptProxy.Current.RegisterClientScriptResourceInHeader(this.Page,typeof(ControlResources),
                                              ControlResources.WWJQUERY_SCRIPT_RESOURCE);

    // Register in the code - as ASP.NET natively does
    ClientScriptProxy.Current.RegisterClientScriptResource(this.Page,typeof(ControlResources),
                                                            ControlResources.WWJQUERY_SCRIPT_RESOURCE);
}

Notice that I can specify whether to load at the top or bottom of the header - so jQuery or any type of library can be forced to the top to ensure it loads before anything else. This may seem like an awful lot of choices to do a simple thing, but it does give control developers a lot more options about how script is placed in the page and at least provide the expected behavior even if there is user added dependent libraries in the page (ui.core.js and ui.draggable.js):

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <script src="wwSC.axd?r=Westwind.Web.Controls.Resources.jquery.js" type="text/javascript"></script>
    <title>Test Page</title>
    <script src="scripts/ui.core.js" type="text/javascript"></script>
    <script src="scripts/ui.draggable.js" type="text/javascript"></script>
<link href="Standard.css" rel="stylesheet" type="text/css" /> <script src="wwSC.axd?r=Westwind.Web.Controls.Resources.ww.jquery.js" type="text/javascript"></script> </head> <body> <form name="form1" method="post" action="MethodCallback.aspx" id="form2"> <div> <input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwUKMTcxMzE4OTIxM2RkaKl2DV4GkXjRsmqC81jFi3+ZZy8=" /> </div> <script src="wwSC.axd?r=Westwind.Web.Controls.Resources.wwscriptlibrary.js" type="text/javascript"></script> <div>

As you can see there are three distinct places where script code is embedded.

I would boil down those three locations to:

  • Header Top - Core Libraries that other components might use
  • Header Bottom - Support libraries that depend on core libraries or no dependencies
  • Document - Libraries that depend on both core and support libraries.

In the example of jQuery it's a library so it goes to the top and precedes anything the user puts in the ASPX markup's header. The bottom of the header goes to support libraries that have dependencies on libraries or are fully self contained. Putting script into the body go scripts that have the most dependencies on other libraries.

This gives a fair bit of control over the process, but it's still to many decisions that code has to make over the environment.

Even more generic for Control Development

I've been thinking that maybe one more level of abstraction would help greatly specifically addressing how Resources should be exposed at the control level. Specifically what I do with all of me control's resources now is define them as a string property that can 3 'types' of value:

  • WebResource - the resource is loaded from the default Web Resource
  • Url - User specifies a relative Url to the resource
  • Blank - value is left blank and the control doesn't do anything to load a resource

For example:

/// <summary>
/// Determines where the ww.jquery.js resource is loaded from. WebResources, Url or an empty string (no resource loaded)
/// </summary>
[Description("Determines where the ww.jquery.js resource is loaded from. WebResources, Url or leave empty to do nothing"),
DefaultValue("WebResource"), Category("Resources")]
public string ScriptLocation
{
    get { return _ScriptLocation; }
    set { _ScriptLocation = value; }
}
private string _ScriptLocation = "WebResource";


/// <summary>
/// Determines where the jquery.js resource is loaded from. WebResources, Url or leave empty to do nothing
/// </summary>
[Description("Determines where the jquery.js resource is loaded from. WebResources, Url or leave empty to do nothing"),
DefaultValue("WebResource"), Category("Resources")]

public string jQueryScriptLocation
{
    get { return _jQueryScriptLocation; }
    set { _jQueryScriptLocation = value; }
}
private string _jQueryScriptLocation = "WebResource";

In order to handle these type of control scenarios I have code like the following inside of my control's load code that is responsible for loading the appropriate resources.

protected override void OnLoad(EventArgs e)
     {
         if (!this.IsCallback)
         {
             // If we're not in a callback provide script to client 
             this.ClientScriptProxy = ClientScriptProxy.Current;

             // load script references (or not)
             this.ClientScriptProxy.LoadControlScript(this, this.jQueryScriptLocation, ControlResources.JQUERY_SCRIPT_RESOURCE, true);
             this.ClientScriptProxy.LoadControlScript(this, this.ScriptLocation, ControlResources.WWJQUERY_SCRIPT_RESOURCE);
             return;
         }
}

In the code I know what default resources are associated with a particular property so I can pass that down although the resource name may not be used - it's merely the identifier. The ClientScriptProxy.LoadControlScript method is the one that decides exactly how the script is to be embedded into the page:

/// <summary>
/// Helper function that is used to load script resources for various AJAX controls
/// Loads a script resource based on the following scriptLocation values:
/// 
/// * WebResource
///   Loads the Web Resource specified out of ControlResources. Specify the resource
///   that applied in the resourceName parameter
///   
/// * Url/RelativeUrl
///   loads the url with ResolveUrl applied
///   
/// * empty (no value) 
///   No action is taken
/// </summary>
/// <param name="control">The control instance for which the resource is to be loaded</param>
/// <param name="scriptLocation">WebResource, a Url or empty (no value)</param>
/// <param name="resourceName">The name of the resource when WebResource is used for scriptLocation</param>
/// <param name="topOfHeader">Determines if scripts are loaded into the header whether they load at the top or bottom</param>
public void LoadControlScript(Control control, string scriptLocation, string resourceName, bool topOfHeader)
{
    // *** Specified nothing to do
    if (string.IsNullOrEmpty(scriptLocation))
        return;

    if (scriptLocation == "WebResource")
    {
        if (ClientScriptProxy.LoadScriptsInHeader)
            RegisterClientScriptResourceInHeader(control, control.GetType(), resourceName, topOfHeader);
        else
            RegisterClientScriptResource(control, control.GetType(), resourceName);

        return;
    }

    // *** It's a relative url
    if (LoadScriptsInHeader)
        this.RegisterClientScriptIncludeInHeader(control, control.GetType(),
                                                control.ResolveUrl(scriptLocation), topOfHeader);
    else
        RegisterClientScriptInclude(control, control.GetType(),
                                Path.GetFileName(scriptLocation), control.ResolveUrl(scriptLocation));
}
public void LoadControlScript(Control control, string scriptLocation, string resourceName)
{
    this.LoadControlScript(control, scriptLocation, resourceName, false);
}
/// <summary>
/// Global flag that can be set once per application and determines how 
/// script is loaded into the page.
/// 
/// Preferrably set once in a static constructor or in Application_Start
/// to set global script resource behavior. Applies only when loading      
/// </summary>
public static bool LoadScriptsInHeader = false;
 

The final piece is a global static LoadScriptsInHeader flag - which can be set once in Application_Start or a static constructor to globally force all scripts that are loaded through this function from within controls to load either in the header or using the 'normal' approach of rendering in the body.

What makes this nice is that now I have a very simple way to deal with script resources in controls with a single line of code for each of them that handles both script embedding on any of the options (WebResource,Url or do nothing):

this.ClientScriptProxy.LoadControlScript(this, this.jQueryScriptLocation, ControlResources.JQUERY_SCRIPT_RESOURCE, true);

Plus I do get the control to decide the load location at least within the header. So I can make absolutely sure that jQuery will always load before any jQuery add-ins loaded onto the page manually or via code (by not specifying them to load at the top).

I think this is pretty cool because it provides a simple API to talk to that is easy to read in control code, plus because it sits in an abstraction of the actual script managers I'm free to pick and choose how the implementation talks to the script managers. It gives a lot of flexibility to the control developer and also all the necessary option for the page developer.

So, what do you think? Is this approach of allow controls to have a single Resource property for each embedded resource with the three options to load from WebResource, a Url or no loading enough?

If you want to check out the code you can browse or download from:
 http://www.west-wind.com:8080/svn/jQuery/trunk/jQueryControls/Support/.

Posted in ASP.NET  

The Voices of Reason


 

Guy Harwood
September 09, 2008

# re: Client Script Resources in ASP.NET Controls revisited

Nice article.

I ripped some of the code from your original take on the subject, and its working exactly how i need it to - just inserting the jQuery include in the header of my site master.

I still dont understand why something like this is not a part of the .NET API

Bertrand Le Roy
September 09, 2008

# re: Client Script Resources in ASP.NET Controls revisited

Well, the ScriptManager enables you to specify if the scripts go to the top or the bottom of the form, and scripts get inserted in the order in which they are registered. Duplicates are being removed automatically. This way, each component can independently register all the scripts it depends on in the right order and everything works just fine. Agreed, ScriptManager currently always inserts MicrosoftAjax.js but there are ways around that (http://weblogs.asp.net/bleroy/archive/2008/07/07/using-scriptmanager-with-other-frameworks.aspx) and we're fixing that in 4.0.

Guy Harwood
September 09, 2008

# re: Client Script Resources in ASP.NET Controls revisited

@Bertrand - Thats just it - top or bottom of form, no header.

Look forward to seeing what v4.0 has to offer :-)

Rick Strahl
September 09, 2008

# re: Client Script Resources in ASP.NET Controls revisited

@Bertrand - yes top or bottom of the form but not on a per script basis. Only for all of them together in batch. The ClientScriptProxy also ensures that no duplicates are added for resources or real paths.

The lack of placement is a problem IMHO and it's bugged me for some time. It can seem to confusing to provide though which why I'd guess this isn't supported in ASP.NET. That and because it probably assumes you do one or the other - resource embedding, but not extensive mixing of manual scripts and WebResource scripts.

Mike Gale
September 09, 2008

# re: Client Script Resources in ASP.NET Controls revisited

We're dealing here with independent components that need to work together, where there can be interesting dependencies.

Taking a big step back (I haven't worked through the practicalities) here's some ideas. I'm thinking in the framework and IDE here. (Rick your approach is good for what we have now.)

1) Aim for all scripts in a single block. (In head under title.)
2) Each script has a defined ordinal position (for that page).

The ordinal position can be derived from some annotation, on the script files maybe, or worked out by a "Script Fusion" engine. That "Script Fusion" engine could be run at design time or later.

(I use JSLint when validating my pages, I put such annotations in there to make that tool work. jQuery, at least my dev version, needs be protected from JSLint to stop it emitting unwanted messages.)

A useful engine feature would be to suggest script mergings, to keep the script-count down.

(Scripts have their own sort of "DLL hell", as ably illustrated here, so a robust answer may need to be quite powerful, to have any chance of working solidly)

Forgive the brainstorm!

tomas
December 05, 2008

# re: Client Script Resources in ASP.NET Controls revisited

I ripped some part of code, it work fine. thanks you.

Jens Madsen
February 19, 2009

# re: Client Script Resources in ASP.NET Controls revisited

Used some of your code, and it helped circumvent spme those annoying ASP.NET holes :D

Thanks a lot.

Ryan Mrachek
January 04, 2010

# re: Client Script Resources in ASP.NET Controls revisited

As always, very handy. thank you!

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024