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/.
Other Posts you might also like