For the last couple of days I've spent a little time updating some code in the wwHoverPanel AJAX library component. In particular I've been working with the library quite a bit over the last few months and have been making lots of small little tweaks and adjustments. One of the things that has given me a bit of grief in building controls has been the standard script include placement that ASP.NET uses.
In ASP.NET script includes are generally handled throught ClientScript.RegisterClientScriptInclude() or RegisterClientScriptResource() which embed either links (<script src="scriptpath.js">) or links to WebResource.axd to point at a script. When these methods are called the scripts are collected and then stuffed into the document just after the <form> tag when the ASP.NET Page renders.
Things are a little more complex with MS Ajax which can inject itself with the ScriptManager that can also embed scripts. Advantages of the script manager include getting GZipped scripts embedded when feeding from resources, but more importantly that the MS Ajax UpdatePanel page manager is aware of the script references embedded into the page. This is why some time ago I built the ClientScriptProxy class which uses either Page.ClientScript, ScriptManager (if available in System.Web.Extensions and the current project) or if available a custom script compression module (which also GZips and compresses scripts dynamically).
The issue at hand is the script placement. If you look at a typical page that has scripts embedded in it you'd see something like this (from an MS Ajax page):
<form name="form1" method="post" action="SimpleWebSerivceCalls.aspx" id="form2">
<div>
<input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" />
<input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="" />
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwUKMTAyMjk2OTczOGRk5RKkldocoIeLZKqrxrVGw1DNVTY=" />
</div>
<script type="text/javascript">
//<![CDATA[
var theForm = document.forms['form1'];
if (!theForm) {
theForm = document.form1;
}
function __doPostBack(eventTarget, eventArgument) {
if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
theForm.__EVENTTARGET.value = eventTarget;
theForm.__EVENTARGUMENT.value = eventArgument;
theForm.submit();
}
}
//]]>
</script>
<script src="/Atlas/WebResource.axd?d=gESNEWx0SVsdkiovb6SaTw2&t=633310943843145000" type="text/javascript"></script>
<script src="/Atlas/ScriptResource.axd?d=Hn6BjENYT2mZ6h167T2sQ-M0n4_tIBI9ZJE4SpMPbFRFe8vEkTknwoGvZSnmIR2qJay3619b4vDKc9pGXG-ejVBbMNptkwHhKD8qiRP6-rA1&t=633052051003028750" type="text/javascript"></script>
<script src="/Atlas/ScriptResource.axd?d=Hn6BjENYT2mZ6h167T2sQ-M0n4_tIBI9ZJE4SpMPbFRFe8vEkTknwoGvZSnmIR2qJay3619b4vDKc9pGXG-ejZXqbFQ2m0TqodBOqMfoiyo1&t=633052051003028750" type="text/javascript"></script>
<script src="/Atlas/ScriptResource.axd?d=tuAvl3eiOhvXiJfyIVg8vs3R586dtnUMkgh5Cip_VYDnEJLXrj3S4GCcn4cUvNSC0&t=633052078591206250" type="text/javascript"></script>
<script src="Simpleservice.asmx/js" type="text/javascript"></script>
<div id="t" class="pre" ></div>
The script tags that are dynamically embedded into the document end up are generated specifically at the following location:
- After the Form tag
- After hidden content vars
- After the ASP.NET PostBack code snippet (if required for AutoPostBack)
- Before any script code embedded into the page (RegisterClientScriptBlock())
This works fine but it this is fairly unconventional - typically if you look at any HTML document or manual you'll find scripts embedded into the header rather than into the content.
It gets tricky in some situations when you need to establish an expected order for components. The problem is that as a control developer you may need to load certain components but you need to make sure that some script components load before others without actually having control over the load order.
It sure would be nice if there was some way to have multiple places to stick script includes, the same way that you have StartupScript and ScriptBlocks for actual java script snippets that get embedded into the page.
Putting Scripts into the Html Header
So, I got to thinking about adding a couple of methods to my ClientScriptProxy class that provides this functionality by writing scripts optionally into the Html header. This provides two things: More standard placement in the header as you'd expect, the ability to identify the script with a comment (useful for WebResources) and most importantly the ability to have two places where script can be defined to allow at least some limited prioritization with header scripts loading before scripts further down in the document.
The later is especially important for certain control scenarios. For example, I use jQuery in several of my controls and I offer the option to load jQuery from resoruces. But I have to make sure that jQuery is loaded before some of its plug-ins get loaded. It doesn't do to load for example jQuery UI Datepicker before the jQuery.js has loaded. So by using the header I can force 'core library' components into the document header and use the standard methods to embed other scripts into the page as ASP.NET usually does.
The result are two new methods on ClientScriptProxy, RegisterScriptIncludeInHeader() and RegisterClientScriptResourceInHeader() which work similar to their namesake functions. Here's the code:
/// <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="comment"></param>
public void RegisterClientScriptResourceInHeader(Control control, Type type, string resourceName, string comment)
{
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);
if (comment != null)
sb.AppendLine("<!-- " + comment + " -->");
sb.AppendLine(@"<script src=""" + script + @""" type=""text/javascript""></script>");
control.Page.Header.Controls.AddAt(index, new LiteralControl(sb.ToString()));
index++;
HttpContext.Current.Items["__ScriptResourceIndex"] = index;
}
/// <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="comment"></param>
public void RegisterClientScriptIncludeInHeader(Control control, Type type, string Url, string comment)
{
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;
// *** Embed in header
StringBuilder sb = new StringBuilder(200);
if (comment != null)
sb.AppendLine("<!-- " + comment + " -->");
sb.AppendLine(@"<script src=""" + Url + @""" type=""text/javascript""></script>");
control.Page.Header.Controls.AddAt(index, new LiteralControl(sb.ToString()));
index++;
HttpContext.Current.Items["__ScriptResourceIndex"] = index;
}
The code works by writing the script into the Html Header of the page by using Page.Header and adding at the top of the Header collection. Scripts are always added at the 0 position so they will always be the first to render even if the page developer adds scripts into the header. The idea is if I write scripts into the header they will be loaded before anything else.
Notice that both functions need to check for duplicate scripts - you need to make sure the script gets written only once even if the methods are called more than once with the same script or resource. I also need to keep track of the insertion index - I want the items to be added at the first header location but in sequence and so I need to track and increase the page index on the current page level. Both require tracking request specific state value, so here I use HttpContext.Current.Items to track both the added scripts and the index. The Items collection is a great tool for tracking component items that are request specific.
A typical call then looks like this - here for a couple generic wrapper methods around the two library scripts that I frequently load so I can simply do ControlResources.LoadwwScriptLibrary(this):
public static void LoadwwScriptLibrary(Control control)
{
// *** Register jQuery first since we need it - not yet - next rev
// LoadjQuery(control);
ClientScriptProxy p = ClientScriptProxy.Current;
p.RegisterClientScriptResourceInHeader(control, typeof(ControlResources),
ControlResources.SCRIPTLIBRARY_SCRIPT_RESOURCE,
"wwScriptLibrary");
}
public static void LoadjQuery(Control control, string jQueryUrl)
{
ClientScriptProxy p = ClientScriptProxy.Current;
p.RegisterClientScriptResourceInHeader(control.Page,
typeof(ControlResources),
JQUERY_SCRIPT_RESOURCE, "jQuery");
}
which results in proper header placement:
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<!-- jQuery -->
<script src="wwSC.axd?r=V2VzdHdpbmQuV2ViLkNvbnRyb2xzLlJlc291cmNlcy5qcXVlcnkuanM=" type="text/javascript"></script>
<!-- wwScriptLibrary -->
<script src="wwSC.axd?r=V2VzdHdpbmQuV2ViLkNvbnRyb2xzLlJlc291cmNlcy53d3NjcmlwdGxpYnJhcnkuanM=" type="text/javascript"></script>
<title>Client Callback - Raw Ajax</title>
</head>
<body>
Now I can have my libraries loaded at the top of the page always and without having to manually do anything and page developers can add things like jQuery plug-ins or code depending on my library in the header without having to worry about load order issues. Cool.
There are a few caveats to this approach that I can see:
- No header, no workey
- No explicit MS Ajax support
The latter relates to UpdatePanel operation if you have a control that embeds scripts and doesn't do so on the initial page rendering. Since the updates don't go through ScriptManager in the code above changing the scripts or loading them later after a control becomes visible will not update the header in the UpdatePanel call.
Neither of these are a problem in my scenarios, but it's something to keep in mind.
Other Posts you might also like