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:

  1. 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.
  2. 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?
  3. What if I want to change which CDN is used for a particular resource (remember, we hard coded the path earlier)?
  4. How can I get the benefits of automatically switching between debug and release scripts for a non assembly-based resource?
  5. 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.

  1. Now I can simply reference jQuery by name, just like MicrosoftAjax.js (only I don’t even need the ‘.js’ part).
  2. Because ScriptManager knows the path to jQuery’s release and debug versions, it switches automatically just like it does for MicrosoftAjax.js.
  3. 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!

22 Comments

  • Great post!

  • This is really great, finally the script manager can now become the hub of all things javascript.

    I have two questions though:

    1. Can we put it in the head of the page, instead of in the form?
    2. Are you planning a similar control for CSS? Or support for stylesheets in the scriptmanager?

    Point 2 is really important for visual components.

    Thanks!

  • Looks great, but have they fixed the ScriptManager to work without a server form? Otherwise, it's useless for MVC, or for any other page that doesn't require a server form.

    Currently, you can have a ScriptManager/ScriptManagerProxy on a page without a server form by setting the SupportsPartialRendering property to false, but none of the registered scripts will be included in the HTML output. This is because it uses the ClientScriptManager class, which has its scripts rendered from the internal BeginFormRender/EndFormRender method on the Page class, and these methods are only called from the RenderChildren method of a server form.

    We need some sort of script placeholder control which would render the registered scripts when a server form is not present. If there's no script placeholder, the scripts could be rendered before the end of the HTML body tag, or, as a last resort, inside the page header.

    Until this issue is fixed, I find it difficult to get excited about improvements to the ScriptManager control!

  • Looks great, but can you use it without a server form?

    In v3.5, the ScriptManager uses the ClientScriptManager class, which only renders the scripts when a server form is rendered. If you set the SupportsPartialRendering property to false you won't get an error, but you won't get any scripts either!

  • With regard to "you can also get them all combined", there used to be a restriction that limited the combind url to 1024 characters (or something). This meant that if you specified more than say 10 files for combining, you'd receive an exception. Has any fix or work-around been provided for that?

    -Mark

  • Very nice..

    How well will this play with asp.net mvc 2?

    Erik

  • Great Post! I dont know about you but ur code blocks are kinda difficult to read might want to change the styling a bit.

  • @Mike -- answer to both is no, but I appreciate the feedback.

    @RichardD -- no, it is still really only for WebForms, it was not a goal to make it MVC friendly. One problem with that of course is it would be strictly for webform views. I suggest you take a look at the script loader recently released in ASP.NET Ajax 4 Beta. You can use it to load your own scripts, and this makes it very useful in MVC. See: http://weblogs.asp.net/bleroy/archive/2009/11/23/enabling-the-asp-net-ajax-script-loader-for-your-own-scripts.aspx

    @Mark -- yes, the limit was increased to 2048, which I believe was as high as it could go for IE. That should help a lot. But regardless, you know, if you hit the limit the workaround is to use the 'path' property on the CompositeScript to point to a static combined script. That's better for performance anyway and sure looks cleaner in the HTML.

  • Ahh fantastic. Just the improvements I was waiting for.

    Many thanks for summarizing all the goodness of the scriptmanager to anticipate.

  • This is going to be very helpful!

    I hope there will be a way to declare the ScriptResourceDefinition stuff in the web.config file, and not just in code!!

  • Can you point me to the legacy method for replacing system scripts with static copies? Thanks!

  • Can you add WebServices to the composite Script somehow?

  • Mapping WebUIValidation.js in System.Web works fine with full page loads, but with partial post backs, the ScriptManager sends out the System.Web script. Any ideas why?

  • any ideas why some files (the webform stuff etc) seems to work fine with enablecdn, but a couple others (appears to be the ajax files?) still are served via scriptresource.axd? you see the results in this website: http://www.deadlywind.com .

    Any input appreciated.

  • setiri -- Looks like it's using the ACT, correct? Not sure but probably ACT isn't setting the cdn paths. If you're able to create a simple repro page using just the script manager, and open a bug on ajaxcontroltoolkit.codeplex.com, perhaps it can be fixed :)

  • Why does the name only method not work when I add it programatically? E.g. ScriptManager.GetCurrent(Page).Scripts.Add(new ScriptReference("jquery", GetType().Assembly.FullName))

  • @Drew -- the name and assembly are together the key to identify the script. You are giving it an assembly, whereas your mapping is not. Or, they aren't the same. If you don't specify an assembly in the mapping, don't specify one in the script reference either. Also, a null/missing assembly is treated semantically identical to the assembly being System.Web.Extensions (as in typeof(ScriptManager).Assembly), so they are interchangeable, should you have to specify an assembly for some reason.

  • I figured it out. I ran in debug and stopped at the point where I was adding the script references but with the <asp:ScriptReference.. line in place and the Assembly was null. So removing the <asp:ScriptReference.. line and adding this worked:

    sm.Scripts.Add(new ScriptReference("WebForms", null));

  • Can somebody tell me specifically (preferably with a URL that downloads the actual file(s)) what version of the Ajax Control Toolkit I would need to download to get the "EnableCdn" attribute to work?

    I'm using Visual Studio 2010 SP1 and .Net 4, but nothing I've tried will prevent "ScriptResource.axd" and "WebResource.axd" from loading off my local system instead of using the CDN. I've also tried adding "asp:ScriptReference" tags and pointing them to various releases of the libraries on the Ajax CDN, and that results in version mismatches (and lots of JS errors) between the Microsoft Ajax scripts and the Ajax Control Toolkit scripts.

    It seems to me (and possibly I'm making the wrong assumptions after reading this article) that setting "EnableCdn" to TRUE would fetch both sets of JS files from the Ajax CDN. But nothing I'm trying seems to work.

    Any assistance would be appreciated!

    Jerry H.

  • Check out the answer linked in the StackOverflow question. Also linked below via Stackunderflow.js.

    http://stackoverflow.com/questions/5035573/loading-ajaxcontroltoolkit-scripts-from-microsofts-cdn-using-scriptmanager-toolk/5944102#5944102

  • Tried it, and variations of it, and the versions just don't seem to align correctly between the Microsoft Ajax scripts and the Ajax Control Toolkit scripts (signature issues of some sort).

    Then again, not an expert by any means when it comes to ASP pages. Maybe I'm just doing something completely wrong...

    Jerry H.

  • You do have to point to the correct version. But like I said I don't even know if they are on the CDN or not. The directory that the files are in is the version number. Just make sure it matches the version of the toolkit you have. All files must be in there, even the regular MS ajax scripts, since the ACT has its own copies of them. It may very well be that those are missing on the CDN. I'll find out...

Comments have been disabled for this content.