ASP.NET 4.0 ScriptManager Improvements
.NET Framework 4 Beta 2 has been out for a little while now. There are some subtle improvements to the ScriptManager control in ASP.NET 4.0 that few have picked up on yet. Allow me to introduce you to them! First, let me say that this is strictly about features in the server-side ASP.NET ScriptManager control, not the Ajax library in general. Also – if you do not use the ASP.NET Ajax library but you are a WebForms developer, I assume you, this article is still for you!
EnableCdn? Yes please.
This one has been blogged about by ScottGu already, but for completeness, here it is. The ASP.NET Ajax scripts are now hosted in a Microsoft CDN, and you can tell ScriptManager to load them from there by simply enabling this property. For virtually no work you get better performance, less bandwidth usage, and a cleaner rendering due to those ScriptResource.axd urls going away from your HTML. Do read the linked post for details. But wait, there’s more!
What has not really been blogged about is this: The EnableCdn property isn’t only for ASP.NET Ajax scripts. Even the scripts in System.Web.dll are on the CDN. So if you are using a GridView, TreeView, or Validators, for example – those scripts will load from the CDN, too. It wouldn’t have been the best experience if only only some scripts were affected by this setting, right?
But what if you are using custom scripts, or 3rd party controls? How would that work? Here’s how.
Normally, when you embed a script within an assembly for use by a WebForm (either via ScriptManager or the classic GetWebResourceUrl and RegisterScriptResource APIs), you have to define a WebResourceAttribute for it, which allows it to be accessed via the WebResource.axd or ScriptResource.axd handlers:
[assembly: WebResource("Foo.js", "application/x-javascript")]
Now, there’s a new property on WebResourceAttribute: CdnPath. Don’t get too caught up on the fact it’s a hard coded url. More on that later.
[assembly: WebResource("Foo.js", "application/x-javascript", CdnPath = "http://foo.com/foo/bar/foo.js")]
ScriptManager looks for this property when the EnableCdn property is set to true, and simply uses that path instead of the ScriptResource.axd path to the assembly resource. Pretty simple. And this means that you too can provide your own CDN paths for your own assembly resource scripts. As for where to host your script, well, that’s up to you.
AjaxFrameworkMode.Disabled
ScriptManager does some interesting and useful things, like: script combining, serving scripts from assemblies, script localization, script globalization, removing of duplicate references, automatic switching between debug and release scripts, and now automatic switching to a CDN. Trouble is, it comes with a bit of a tax: It always includes the Microsoft Ajax library (MicrosoftAjax.js), and by default, MicrosoftAjaxWebForms.js (for partial rendering support with UpdatePanel). The partial rendering script could be removed by setting EnablePartialRendering=false, but there was no way to disable MicrosoftAjax.js. So if you wanted to use the useful features of ScriptManager without using MicrosoftAjax.js, well, you couldn’t (although Bertrand once blogged a way of hacking it by making use of it’s de-duping process).
The AjaxFrameworkMode property adds two new modes to ScriptManager’s behavior. Enabled (the default, and the same behavior as before), Explicit (does not include any scripts by default but still assumes Microsoft Ajax is to be utilized), and Disabled (does not include any scripts by default and does not assume Microsoft Ajax will be used).
So here I have a ScriptManager that is including a custom script, without also loading MicrosoftAjax.js or any of the inline script it normally produces.
<asp:ScriptManager runat="server" AjaxFrameworkMode="Disabled">
<Scripts>
<asp:ScriptReference Path="~/scripts/foo.js" />
</Scripts>
</asp:ScriptManager>
AjaxFrameworkMode.Explicit
The client side script for ASP.NET AJAX 3.5 (the one baked into ASP.NET 3.5) is basically just MicrosoftAjax.js and MicrosoftAjax.debug.js. There are some others, but that’s the main part of the client-side framework. But more often than not, you don’t need the entire framework. It contains a lot of features, like support for WebServices, History management, and Globalization. In 4.0, we have split MicrosoftAjax.js into several parts. When you use Explicit mode, you include these scripts manually in order to ‘pick and choose’ what parts of the framework you need, thus reducing the overall size of the javascript your page requires.
- MicrosoftAjaxCore.js (contains the type system like registerClass)
- MicrosoftAjaxComponentModel.js (contains Sys.Application, Sys.Component, Sys.UI.Control, as well as DomEvent for event abstraction and DomElement helpers)
- MicrosoftAjaxNetwork.js (contains WebRequest and WebRequestManager related classes)
- MicrosoftAjaxWebServices.js (contains WebServiceProxy and is used to talk to asmx and wcf services, or any service that can serve JSON)
- *MicrosoftAjaxApplicationServices.js (talks to the Profile, Role, and Authentication services)
- MicrosoftAjaxSerialization.js (contains the JavaScriptSerializer class)
- MicrosoftAjaxHistory.js (contains extensions to Sys.Application to add client-side history management support)
- MicrosoftAjaxGlobalization.js (contains formatting and parsing logic for culture-aware Date, Number, String conversion)
MicrosoftAjax.js still exists, too, which is now considered a composite script of all these scripts. The one (*) exception is that MicrosoftAjaxApplicationServices.js is now its own script, not part of MicrosoftAjax.js, and so must be included explicitly even if you don’t use Explicit mode and you want to use any of the application services.
Keep in mind when using this mode that you are “on your own” with determining which parts of the framework you need. This is strictly for those of you who want to lower the footprint of your pages by a little bit. MicrosoftAjax.js in ASP.NET 4.0 is somewhere around 98kb, which is gzipped when served, which takes it down into the 30kb range (numbers from memory – and by the way, these numbers are lower in the recently announced ASP.NET Ajax Beta release on CodePlex by the CodePlex foundation). By comparison, jQuery is something like 20kb once gzipped. So it’s not going to make a big difference, but every bit counts! You can use the existing CompositeScript feature of ScriptManager to include the separate parts of the framework and yet still have them served as a single script.
Oh – yes, these scripts are all also on the CDN and work with EnableCdn, of course.
Replacing System.Web Scripts
When a script component on the page requires an ajax script, it implements the IScriptControl interface, which allows it to tell ScriptManager what scripts it requires. Typically a component will ship with its scripts embedded in its own assembly. In previous versions, you could use a statically declared ScriptReference to override the script used by that component. For example, if a component used the ‘Foo.js’ resource from the ‘CustomControls’ assembly, you could replace the resource based script reference with a static (and possibly, customized) copy like so:
<asp:ScriptManager runat="server">
<Scripts>
<asp:ScriptReference Name="Foo.js" Assembly="CustomControls"
Path="~/scripts/customfoo.js" />
</Scripts>
</asp:ScriptManager>
(In red to indicate this is not a new feature)
One problem with this feature was that it only worked for scripts that were registered via ScriptReferences or through the IScriptControl interface. But some components still use ClientScript.RegisterClientScriptResource or ScriptManager.RegisterClientScriptResource. The controls in System.Web for example, like the TreeView, GridView, etc. If you wanted to replace one of those scripts with a static copy, you couldn’t (actually for System.Web scripts there was a way, but as a legacy feature I won’t go into). Now in 4.0, you can. So for example – if you find a bug in WebUIValidation.js for a Validator control in System.Web, or there’s just something about it you don’t like – or even, you want to customize it for some reason, just override the reference:
<asp:ScriptManager runat="server">
<Scripts>
<asp:ScriptReference Name="WebUIValidation.js" Assembly="System.Web"
Path="~/scripts/customWebUIValidation.js" />
</Scripts>
</asp:ScriptManager>
An interesting benefit to this is that it is now possible to ScriptManager’s CompositeScript capabilities to combine System.Web scripts! If you are using a GridView and a Validator, for example, you will by default get two WebResource.axd’s to Gridview.js and WebUIValidation.js. Most likely you will also get WebForms.js which has some postback logic in it. Now you can combine them into one:
<asp:ScriptManager runat="server">
<CompositeScript>
<Scripts>
<asp:ScriptReference Name="WebForms.js" Assembly="System.Web" />
<asp:ScriptReference Name="GridView.js" Assembly="System.Web" />
<asp:ScriptReference Name="WebUIValidation.js" Assembly="System.Web" />
</Scripts>
</CompositeScript>
</asp:ScriptManager>
Now, that is still going to render as an 'AXD' script -- to ScriptResource.axd. The scripts still live in an assembly, after all. But a feature you probably didn't know about with CompositeScripts (and has been there since ASP.NET 3.5 SP1) is that you can point them at static paths just like regular ScriptReferences. For ultimate in performance and a clean HTML rendering, you can not only combine all those scripts into one, but have them referenced via a simple static script block too. No more 'AXD' with a long, encrypted query string.
<asp:ScriptManager runat="server">
<CompositeScript Path="~/scripts/combined.js">
<Scripts>
<asp:ScriptReference Name="WebForms.js" Assembly="System.Web" />
<asp:ScriptReference Name="GridView.js" Assembly="System.Web" />
<asp:ScriptReference Name="WebUIValidation.js" Assembly="System.Web" />
</Scripts>
</CompositeScript>
</asp:ScriptManager>
Simple Assembly Names
A small but welcomed addition. Previously, if you wanted to reference a script embedded in an assembly via a static script reference, you had to include the full assembly name in the ‘assembly’ attribute. If the assembly is a strong named one, that meant including the fully qualified named, including the publicKeyToken which you undoubtedly would have to lookup somewhere (although I have a friend who has memorized his company’s publicKeyToken! LOL). Something like this:
<asp:ScriptManager runat="server">
<Scripts>
<asp:ScriptReference Name="Foo.js"
Assembly="CustomAssembly, Version=1.2.3.4, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</Scripts>
</asp:ScriptManager>
Now you can just use simple assembly name. The caveat to this is you do at least need to have the assembly in your bin, or it must be referenced in the <assemblies> section of your web.config. Otherwise, the simple name could be ambiguous to multiple versions in the GAC. In the previous example, notice I used only ‘System.Web’ when overriding WebUIValidation.
<asp:ScriptManager runat="server">
<Scripts>
<asp:ScriptReference Name="Foo.js" Assembly="CustomAssembly" />
</Scripts>
</asp:ScriptManager>
ScriptResourceMapping
This is definitely my favorite new feature, so I saved it for last. Normally, ScriptManager determines where to get a script from by looking at the Name, Assembly, and Path properties. Name and Assembly go together to indicate an embedded resource in the assembly, and Path is just a path to a static script in your application. Since the beginning, you could override an assembly-based reference with a path-based reference. For example, to get MicrosoftAjax.js to load from a path in your site rather than from an assembly, you could declare a script reference like so:
<asp:ScriptManager runat="server">
<Scripts>
<asp:ScriptReference Name="MicrosoftAjax.js" Path="~/scripts/MicrosoftAjax.js" />
</Scripts>
</asp:ScriptManager>
The assembly is assumed to be System.Web.Extensions if not specified. With this script reference, now even if script controls on the page registered a requirement for MicrosoftAjax.js, this one would be recognized as the same script thanks to the Name being the same. A static reference on ScriptManager trumps all, so your custom path wins, thus replacing the assembly reference with a path one (and worth noting – it still supports auto switching to the debug version and to localized versions).
Great – there are some problems to consider:
- What if I want every page in my app to do this? If you’re using a Master Page, you may only have a few ScriptManager’s to deal with, so that may not be so bad. But even then, there’s a maintenance factor here.
- What if I am using a 3rd party component and it does not define a CdnPath? Is there any way to make it work with EnableCdn if I can host it somewhere myself?
- What if I want to change which CDN is used for a particular resource (remember, we hard coded the path earlier)?
- How can I get the benefits of automatically switching between debug and release scripts for a non assembly-based resource?
- How can I get the benefits of automatically switching between debug and release scripts for a script like jQuery that does not use the “.debug.js” naming convention?
To be fair, #4 was already possible by explicitly setting the ScriptMode property on the script reference – but we’ll see a better way now.
ScriptResourceMapping to the rescue. Think of ScriptResourceMapping as a static/global location into which you can describe all the details about a particular script resource: a logical name for the script, it’s debug and release locations, what assembly it lives in (if any), and what it’s debug and release CDN paths are. With all that information, ScriptManager can make smarter choices about how to handle a script reference and makes your life easier.
For example, let’s say you want to include jQuery via a ScriptReference.
<asp:ScriptManager runat="server">
<Scripts>
<asp:ScriptReference Path="~/scripts/jQuery-1.3.2.js" />
</Scripts>
</asp:ScriptManager>
But wait – what about jQuery-1.3.2.min.js? You should definitely be using the minimized version in production. But in a debugging environment, it’s better to use the non-minimized version so if you ever need to step into jQuery code, you can actually understand what is going on.
And what about EnableCdn? That’s not going to work with this since ScriptManager doesn’t know about this script. It’s just some script in your project. Yes, you could just put the full ‘http://...’ path in there. But having to remember that path every time you need it is not very nice.
No problem – we’ll just create a mapping that fully describes the resource. Mappings are static, so let’s create them from the Application_Start event in Global.asax:
void Application_Start(object sender, EventArgs e) {
// map a simple name to a path
ScriptManager.ScriptResourceMapping.AddDefinition("jQuery", new ScriptResourceDefinition {
Path = "~/scripts/jquery-1.3.2.min.js",
DebugPath = "~/scripts/jquery-1.3.2.js",
CdnPath = "http://ajax.microsoft.com/ajax/jQuery/jquery-1.3.2.min.js",
CdnDebugPath = "http://ajax.microsoft.com/ajax/jQuery/jquery-1.3.2.js"
});
}
Three big wins here.
- Now I can simply reference jQuery by name, just like MicrosoftAjax.js (only I don’t even need the ‘.js’ part).
- Because ScriptManager knows the path to jQuery’s release and debug versions, it switches automatically just like it does for MicrosoftAjax.js.
- Because ScriptManager knows jQuery’s CdnPath, the EnableCdn feature will toggle using it on/off just like it does for MicrosoftAjax.js.
Using this mapping is simple – just reference the script by name:
<asp:ScriptManager runat="server">
<Scripts>
<asp:ScriptReference Name="jQuery" />
</Scripts>
</asp:ScriptManager>
Beautiful. You can use this to redefine the CdnPath for an existing script, too. For example, say you don’t like the fact that MicrosoftAjax.js loads from the ajax.microsoft.com Cdn. Or, say you are using some 3rd party controls, and they don’t define a CdnPath, but you happen to know of one that uses it (or even, the 3rd party published a Cdn after they shipped the product). Just create a mapping and set the CdnPath. Done. For the entire application.
But wait, there’s even more. The jQuery example shows how you can map a simple name to a static script. But a script resource mapping can also be used to map a name to an assembly resource.
// map a simple name to an assembly
ScriptManager.ScriptResourceMapping.AddDefinition("Foo", new ScriptResourceDefinition {
ResourceName = "FooResource.js",
ResourceAssembly = MyAssembly
});
Or, map an existing assembly resource to a static path.
// map assembly resource to a path
ScriptManager.ScriptResourceMapping.AddDefinition("SomeScript.js", SomeAssembly,
new ScriptResourceDefinition {
ResourceName = "SomeScript.js",
ResourceAssembly = SomeAssembly,
DebugPath = "~/scripts/somescript.debug.js",
Path = "~/scripts/somescript.js"
});
Or, map an existing assembly resource to a different assembly resource.
// map an assembly resource to another assembly resource
ScriptManager.ScriptResourceMapping.AddDefinition("SomeScript.js", SomeAssembly,
new ScriptResourceDefinition {
ResourceName = "SomeOtherScript.js",
ResourceAssembly = MyAssembly
});
Don’t forget you can also use this to change where the MicrosoftAjax*.js scripts come from.
This gives you a lot of control over where scripts come from. Now your pages can utilize ScriptManager’s advanced features, without necessarily depending on MicrosoftAjax.js, and without having to hard code paths to scripts or to assembly resources. Now your pages can be a semantic definition of the scripts your page requires – and the details are all in one location. And when the need arises, you can change where those scripts come from, no matter where they came from originally.
Bringing it all together: De-AXD’ifying
Not really a feature of it’s own – but a consequence of the others worth noting. Because ScriptManager can now deal with System.Web scripts or any scripts regardless of which API is used to register them, it is now possible – for the first time – to get rid of all WebResource.axd and ScriptResource.axd urls from the HTML rendering. And you can do it without touching your pages due to the ScriptResourceMapping feature. And what’s more – because of the CompositeScript feature, you can also get them all combined into a single, static script file, with a simple, beautiful url. Or better yet – let Microsoft handle the bandwidth, and use the CDN!