AJAX development centers on using more JavaScript. With increased use of JavaScript comes the need for better ways to manage, reference, localize (that is, provide different script versions for specific language and culture combinations), and transmit script code to the client browser. The ASP.NET ScriptManager is at the center of ASP.NET AJAX functionality. The ScriptManager is the key component that coordinates the use of JavaScript for the Microsoft AJAX Library. Custom controls also use it to take advantage of script compression and reliable loading, as well as for automatic access to localized versions of scripts.
In this chapter, you will see what the ScriptManager does and how you can control it. You will see how to use the ScriptManager to include your scripts, as well as how to use the AJAX Library scripts. You will also learn how to take advantage of the ScriptManager for accessing scripts embedded in a dll and show how it functions to retrieve localized script resources.
A ScriptManager is required on every page that wants to use the AJAX Library. When the ScriptManager is included in the page, the AJAX Library scripts are rendered to the browser. This enables support for partial page rendering and use of the ASP.NET AJAX Client Library. Listing 5-1 (Bare.aspx) is a page with a barebones ScriptManager that does nothing more than render the Microsoft AJAX Library files to the browser.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html
xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>ASP.NET AJAX ScriptManager</title>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server" />
<div>
</div>
</form>
</body>
</html>
Notice how the rendering changes when the ScriptManager is added to the page. You get five new script elements in the HTML sent to the browser.
The following HTML is part of what is sent to the browser when Bare.aspx is requested. You can see that querystring parameters in the script references are long. They include time stamp elements and unique hash identifiers for script that is registered dynamically. If you copy the path from the src attribute of the script element and paste it into your browser’s address bar, you will get the actual script resources returned from the server.
<script type="text/javascript">
<!-
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="/chapter05/WebResource.axd?d=6kIHBZsykATBSq3fbEmsYQ2&t=
632968784944906146" type="text/javascript"></script>
<script src="/chapter05/ScriptResource.axd?d=9x_HPpGK-eN7tqV0Ff_J6PdW6
RyjdfhhTabpkRyiakKJ_a_q_nueOi1SMgVHCnyemAE_Wi1zmQc6dppn1_ShJ3RT853s6OS8dnx
NpQibyXs1&t=633054056223442000" type="text/javascript"></script>
<script src="/chapter05/ScriptResource.axd?d=9x_HPpGK-eN7tqV0Ff_J6PdW6
RyjdfhhTabpkRyiakKJ_a_q_nueOi1SMgVHCnyemAE_Wi1zmQc6dppn1_ShJ_clCUeTRolYBVlv
TyhBCrs1&t=633054056223442000" type="text/javascript"></script>
The first script is rendered inline in the HTML without a callback to the server. There is no src attribute on the script element. It is the method for initiating a postback. The second is a script reference for the approximately 500 lines of JavaScript included in most ASP.NET pages. It is the fundamental script providing some of the essential functionality of ASP.NET 2.0, including callbacks, performing validation, and managing focus.
The next script reference is for the MicrosoftAjax Library. It is the same as the MicrosoftAjax.js file copied to the \Program Files\Microsoft ASP.NET\ASP.NET 2.0 AJAX Extensions\v1.0.61025\ MicrosoftAjaxLibrary\System.Web.Extensions\1.0.61025 directory when installing ASP.NET AJAX. The code can be used with other server technologies, as it is focused on enriched client-side development. There are debug and release versions of the script files, discussed later in this chapter. The resources are also embedded in the System.Web.Extensions.dll and then extracted by the HTTP request to the ScriptResource.axd handler. This is the JavaScript that supports the client-side type system and Base Class Libraries you saw in Chapter 4.
The third script reference is for the MicrosoftWebForms Library. This code is retrieved as a resource from the dll by default, but is also copied onto disk. These scripts provide support for the UpdatePanel and the lifecycle of events associated with using partial page rendering.
The other two script entries are rendered inline in the page. You can see this in the following code, which is again part of the page output from requesting Bare.aspx from Listing 5-1. The first is for the PageRequestManager that handles partial page updates. The second piece of script performs the primary startup of the client page lifecycle by initializing the application object.
<script type="text/javascript"> //<![CDATA[ Sys.WebForms.PageRequestManager._initialize('ctl02', document.getElementById('form1')); Sys.WebForms.PageRequestManager.getInstance()._updateControls([], [], [], 90); //]]> </script> <script type="text/javascript"> <!-- Sys.Application.initialize(); // --> </script>
You have seen that including the ScriptManager in the page results in rendering the scripts necessary to use the client-side Microsoft AJAX Library and to take advantage of partial page rendering as well. It also renders the scripts necessary for initiating the lifecycle of JavaScript events.
The ScriptManager element can contain a scripts collection for adding scripts to the page. The way you would typically include a separate file of JavaScript in a page would be to use the HTML script element. You set the type attribute to "text/javascript" and the src attribute to the path of the JavaScript file, as shown in the following code.
<script type="text/javascript" src="script.js"></script>
Instead of writing the script element directly, the script can be added to the set of scripts that the ScriptManager controls using a ScriptReference element. By including it this way, I am assured that it will be loaded at a point when the ASP.NET AJAX Library is available. This is shown in Listing 5-2 (ScriptReference.aspx).
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>ScriptManager</title>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager runat="server" ID="ScriptManager1">
<Scripts>
<asp:ScriptReference Path="sample.js" />
</Scripts>
</asp:ScriptManager>
<div>
</div>
</form>
</body>
</html>
You would expect that requesting this page would result in the same output as running Bare.aspx from Listing 5-1 (with one additional script element), but this is not exactly the case. After the initial request for the page, everything appears to be the same, but during partial page updates when asynchronous updates are happening and scripts are being loaded, a JavaScript error is encountered, as shown in Figure 5-1.
A call to Sys.Application.notifyScriptLoaded is required so that the ScriptManager can provide the client lifecycle reliably on various browsers. Not all browsers provide an event in their DOM for detecting this automatically. The script would actually run fine without it, but to ensure cross-browser support, the ScriptManager requires the call. Listing 5-3 (Sample.js) shows the recommended way of modifying scripts so that they can provide the required call to the Microsoft AJAX Library when it is present, and still function reliably when used separately.
// filesystem based script resource
function identityFunction(arg) {
return arg;
}
if(typeof('Sys') !=='undefined')Sys.Application.notifyScriptLoaded();
Rather than directly making the call, I first need to check that the Sys namespace is defined. The script can then be included in a page where the ASP.NET AJAX Library is not being used without causing an error.
The ScriptManager and ScriptReferences both allow you to set the ScriptMode. The default value of this enum is Auto. The other possible values are Release, Debug, and Inherit. The ScriptMode determines whether release or debug versions of the scripts are used. When set to Auto, the determination is primarily the result of server settings. Debug scripts are used when debug is set to true in the compilation section of the web.config file, or when the debug page directive is set to true. The deployment setting in the configuration file trumps all of the other settings. When retail is set to true, all other debug settings are treated as false and you won't get debug versions of the scripts.
ASP.NET AJAX includes release and debug versions of the scripts, but for ScriptReferences that refer to files on disk, the ScriptManager does not assume that you have provided debug versions of the script, so the path given is used regardless of the server’s debug setting. The Auto setting for ScriptMode is almost identical to the Inherits value, except for this behavior. Inherits will take the value directly from the server configuration. The ScriptManager doesn’t look to configuration or page settings when the ScriptMode is set directly to Release or Debug.
The pattern you use to provide debug versions of scripts is to add .debug to the filename before the .js suffix. The name the ScriptManager would assume for the debug version of Sample.js would be Sample.debug.js. The filenames are not validated, so specifying the debug version of a script file that doesn’t exist will result in an error like the one shown in Figure 5-2. Again, this error is only produced when script loading is validated during asynchronous postbacks, not during the initial loading of the page.
The choice of using .debug in the script name is arbitrary, but it does allow automation in finding the right version of scripts on disk, as well as when they are embedded as resources in a dll. You won’t have to keep swapping files during development and deployment. You can set the debug mode you want and get the right version for the scripts you want to debug. For file-based script resources, you toggle between the release and debug versions by setting the ScriptMode attribute to Debug directly on the ScriptReference.
There are some advantages to embedding JavaScript files as resources in a dll and deploying them that way. You don’t have to wonder whether someone has modified the scripts on the server. You can maintain a set of release and debug scripts in a single location and use the ScriptManager to dynamically switch between versions. And once deployed, you have version information on the scripts, allowing for easier maintenance and servicing.
One key difference exists in how the ScriptManager treats scripts retrieved from the filesystem compared with those embedded as a resource in a dll. When the path to a script is used, the ScriptManager provides a callback to the ScriptResource handler, which retrieves the contents. The script itself is not modified (no extra calls are injected). When a script is retrieved as an embedded resource, however, the ScriptManager injects the call to Sys.Application.notifyScriptLoaded for you automatically. This allows you to start using scripts that you already have with ASP.NET AJAX without having to rebuild the dlls.
In Visual Studio, you first create a class library project. You can do this from your existing web application by right-clicking the Solution name in the Solution Explorer window. From the Add New Project dialog, you can add a new project by selecting the Class Library template (see Figure 5-3). Alternatively, you can choose to create a new Project from the File menu. In the dialog where you select the Class Library type you can also choose to add this project to the current Visual Studio Solution.
You create the script files as you normally would, by adding JScript files to the project. Remember that using the naming convention of adding .debug into the filename for the debug version of a script enables automatic switching between release and debug versions at runtime. To embed the scripts into the resulting dll, you set the Build Action in the Properties pane for the script file to Embedded Resource, as shown in Figure 5-4.
In this example, I have added embeddedSample.js with a function called doubleArg that just returns double what it is passed. Listing 5-4 (embeddedSample.debug.js) includes some error checking. For some functions, you want to do parameter validation in release scripts, but for many situations, you just want the extra checks during development and testing.
function doubleArg(arg) {
if(typeof(arg) === 'undefined'){
throw Error.argumentUndefined('arg');
}
if(arg === null) {
throw Error.argumentNull('arg');
}
if((arg % 0) === 0) {
throw Error.argumentOutOfRange('arg',arg);
}
return 2*arg;
}
To be able to add a ScriptReference for the embeddedSample script, you need to define the WebResource for the project. The WebResource attribute is in the System.Web.UI namespace. The attribute is used in the code of your project to include the script into the compiled dll. The project needs a compile-time reference to the System.Web assembly to compile. Right-click the References entry of the Solution Explorer in Visual Studio and select Add Reference. Figure 5-5 shows the dialog where you can select System.Web.
The WebResource is added to the AssemblyInfo.cs file, which is in the Properties directory of the class library project. The WebResource attribute tells ASP.NET the names and types of resources available from assemblies in your web project. In this case, there are two resources, and both of them are JavaScript files to be embedded in the assembly.
[assembly: WebResource("MyScripts.embeddedSample.js", "text/javascript")]
[assembly: WebResource("MyScripts.embeddedSample.debug.js", "text/javascript")]
If you create the class library as a project within your web project in Visual Studio, you can modify the properties of the project to have the resource assembly copied into the bin directory of your web application. If it is a separate project, you will need to explicitly set the output location or copy the dll into your application manually.
Tip |
To declare the dependency on the separate project, right-click on the Web Project in the Solution Explorer and select the Add Reference option. From there, you can select the project with the embedded resources. Also, you have to select the script file in the Solution Explorer of Visual Studio, and in the Property Grid set the Build Action to the Embedded Resource value. This is the same place where you can choose to copy the output to a specific location when the dll is compiled. |
Now you have a dll with script resources embedded, and can include them in your page with a ScriptReference. Listing 5-5 (EmbeddedReference.aspx) is a page that includes the script and calls to the doubleArg function with valid and invalid parameters.
<%@ Page Language="C#" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title>ScriptManager</title> </head> <body> <form id="form1" runat="server"> <asp:ScriptManager runat="server" ID="ScriptManager1" > <Scripts> <asp:ScriptReference Name="MyScripts.embeddedSample.js" Assembly="MyScripts" /> </Scripts> </asp:ScriptManager> </div> </form> </body> </html> <script type="text/javascript"> function pageLoad() { alert(doubleArg(3)); alert(doubleArg()); } </script>
When debugging is not enabled for the page, the browser displays the number 6 and NaN. The second call to the function does not include an argument, and the release script just tries to double it and returns the “Not A Number” JavaScript value. When using the debug version of the script, however, you get the benefit of running the debug version of the script, and a better error message is displayed, explaining that the argument cannot be undefined.
You saw when using the path attribute of a ScriptReference to load a script from a file that the ScriptManager does not switch automatically between debug and release versions of the script. And when using the name and assembly attributes to specify loading of an embedded script, it will pick up the right version based on the server setting. If you do not provide a debug version of the script, the ScriptManager will notice this and fall back to the release version. It is not an error to skip inclusion of debug script resources.
You can include all three attributes - name, assembly, and path - in a single ScriptReference ele-ment and get a slightly different behavior still. The path will be used to retrieve the script from disk, but the determination of whether or not a debug version of the script is available is made by looking at the embedded resources. If you have a debug embedded resource and choose to get the script from disk, it is an error to try and retrieve the debug version if it does not exist on disk. The ScriptManager will not fall-back from the filesystem to the embedded resource.
The .NET Framework has good support for providing and using localized resources in .NET applications. This is a feature that has been lacking in JavaScript. ASP.NET AJAX makes it possible to provide localized string resources and have the correct language used automatically at runtime.
It follows the same principle as the previous example of embedded script resources, but extends it to automatically generate a string resource class. The following JavaScript function uses the shortText and longText properties of the Res class.
function LocalizedMessage() { alert(Wrox.ASPAJAX.Samples.LocalizedMessage.Res.shortText); alert(Wrox.ASPAJAX.Samples.LocalizedMessage.Res.longText); }
But you haven’t defined the Res class. Instead, the ScriptResource attribute is used along with the WebResource attribute to instruct the ScriptManager to create the class. The WebResource attribute is still required to specify the resource name and its type. Then the ScriptResource attribute is added with the name of the script resource, along with the name of the string resources to use and the name of the class to create in JavaScript for use in the browser. This is done in the AssemblyInfo.cs class of the project as shown in the following.
[assembly: WebResource("MyScripts.LocalizedMessage.js", "text/javascript")] [assembly: ScriptResource("MyScripts.LocalizedMessage.js", "MyScripts.LocalizedMessage", "Wrox.ASPAJAX.Samples.LocalizedMessage.Res")]
The name of the .resx files you create is key, as it is used to determine the culture of the resources contained in that file. You can define shortText and longText keys in the LocalizedMessage.resx file and then create a LocalizedMessage.fr-FR.resx file to house French translations of the messages. In this case, fr means the French language, and FR means the French culture. In some cases, a given language can have several culture variations, for example, American English versus British English. Figure 5-7 shows the French resource file. As you can see, it really is just a set of name-value pairs being established for cultures that will be accessed in the application.
To use localized resources, you do not have to provide versions for every possible culture known to the .NET Framework. A request for a culture that does not have localized versions of the strings will fall back to the default strings instead. To enable localization on the ScriptManager, you must set the EnableLocalization property to true. It is false by default.
Another default setting of ASP.NET needs to be changed to make effective use of the localization feature. The ScriptManager will provide the localized resources in the culture setting under which the page is running. By default, this does not reflect the user’s language setting in the browser. The browser will send with each web request an ordered list of language preferences. By setting the UICulture to Auto, ASP.NET will take the top entry from the list of languages and use that culture to execute the request. Note that it does not try subsequent entries in the list if the first one is not a valid culture on the server. Listing 5-6 (LocalizedResource.aspx) is a page that sets the culture, enables localization, includes the ScriptReference for the localized file, and calls the localizedMessage function, which uses the resources.
<%@ Page Language="C#" UICulture="Auto" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title>ScriptManager</title> </head> <body> <form id="form1" runat="server"> <asp:ScriptManager runat="server" ID="ScriptManager1" EnableScriptLocalization="true"> <Scripts> <asp:ScriptReference Assembly="MyScripts" Name="MyScripts.LocalizedMessage.js" /> </Scripts> </asp:ScriptManager> <div> </div> </form> </body> <script type="text/javascript"> function pageLoad() { LocalizedMessage(); } </script> </html>
Now, if the language preference in the browser is English, the page will display Attention messages in English. The options dialog of your browser allows you to add, remove, and reorder your language preference. Figure 5-8 shows adding French to the list of preferred languages. Remember that ASP.NET will only try the topmost item from the preferred languages list. When French is the most preferred language, the UICulture for the thread processing the server request is switched to French, and the ScriptManager selects the French resource strings.
Script localization allows you to provide for specific translations of text for use in JavaScript in the browser. Script globalization is the ability to format and parse data using a specific culture. For example, it is customary in U.S. English for dates to be formatted as month, followed by the day and then the year (8/21/1993), but in many other cultures, the day of the month is presented first (21/8/1993). The ScriptManager will populate a CultureInfo object, with the data from the culture being used to process the page on the server when EnableGlobalization is set to true. It is false by default. For script localization, you change the Page UICulture property to Auto, but for globalization you use the Culture property. UICulture is for language choice, while Culture is for date and number parsing and formatting.
In Listing 5-7 (Globalization.aspx), I show the string formatting functions using String.localeFormat. This version of the formatting API will respect the current culture setting in the browser. With globalization, you can get language-specific formatting in the browser without needing to provide localized resource files in your application.
<%@Page Language="C#" Culture="Auto" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title>EnableGlobalization</title> </head> <body> <form id="form1" runat="server"> <asp:ScriptManager runat="server" ID="ScriptManager1" EnableScriptGlobalization="true" > </asp:ScriptManager> <div> <input type="text" id="textbox" value="something"/> </div> </form> </body> <script type="text/javascript"> function pageLoad(sender, args) { var d = new Date(); var message = String.format("With {0} culture date formats are:\n{1}\n{2}\n{3}\n{4}\n{5}\n{6}\n{7}\n{8}", Sys.CultureInfo.CurrentCulture.name, d.localeFormat("d"), d.localeFormat("D"), d.localeFormat("t"), d.localeFormat("T"), d.localeFormat("F"), d.localeFormat("M"), d.localeFormat("s"), d.localeFormat("Y") ); alert(message); } </script> </html>
Much of the emphasis of richer application development centers on leveraging web services through asynchronous communications with the server. ASP.NET Ajax makes this easy. You can declare references to web services within the ScriptManager in much the same way that the previous examples do for scripts. Suppose you want to mark the time that some event occurred on the client. You might provide a web service like the one in Listing 5-8 (Servertime.asmx) so that you are not relying on the local time of the user’s machine.
<%@ WebService Language="C#" Class="Wrox.ASPAJAX.Samples.TimeOfDay" %> using System; using System.Web; using System.Web.Script.Services; using System.Web.Services; using System.Web.Services.Protocols; namespace Wrox.ASPAJAX.Samples { [ScriptService] [WebService(Namespace = "http://tempuri.org/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class TimeOfDay : System.Web.Services.WebService { [WebMethod] public string TheTime() { return DateTime.Now.ToString(); } } }
By including the new ScriptService attribute on the class declaration, you declare to ASP.NET that it should provide a JavaScript proxy class for use in calling the web service. If you navigate directly to the .asmx file that contains the web service, ASP.NET provides a page that allows you to invoke the methods directly from the browser without writing any code. In a similar vein, if you navigate to the .asmx file in the browser and append /js to the filepath, ASP.NET will return the proxy code necessary to invoke the method directly from script.
The ScriptManager will generate a dynamic script element for the proxy code associated with each ScriptReference it contains. You can optionally set the InlineScript property to true to have the proxy code rendered directly in the page. Listing 5-9 (ServiceReference.aspx) includes a ScriptReference to the TimeOfDay web service. The pageLoad function first sets the timeout to 1000 milliseconds from the default value, where there is no client timeout. The code then invokes the web service providing the names of callback functions to invoke for success or failure.
Tip |
Web browsers limit the number of simultaneous connections they make back to the server, typically to two. So, if you have a lot of ScriptReference elements in a page, you may want to consider having some select ones rendered inline to avoid contention for multiple concurrent requests to download the proxies. However, the tradeoff is that inline scripts are not cached by the browser and bloat the size of the page. Large, frequently used scripts should be external references to take advantage of browser caching. |
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title>ASP.NET AJAX Service Reference</title> </head> <body> <form id="form1" runat="server"> <asp:ScriptManager runat="server" ID="ScriptManager1"> <Services> <asp:ServiceReference Path="ServerTime.asmx" /> </Services> </asp:ScriptManager> <div> </div> </form> </body> <script type="text/javascript"> function pageLoad() { Wrox.ASPAJAX.Samples.TimeOfDay.set_timeout(1000); Wrox.ASPAJAX.Samples.TimeOfDay.TheTime(success, failed, "arbitrary info for the callback") } function success(result, userContext) { alert(result + " " + userContext); } function failed(result) { var message = String.format("statusCode={0}\r\nexceptionType ={1}\r\ntimedOut={2}\r\nmessage={3}\r\nstackTrace={4}", result.get_statusCode(), result.get_exceptionType(), result.get_timedOut(), result.get_message(), result.get_stackTrace()); alert(message); } </script> </html>
The third argument passed to the proxy for invoking the web service is called the userContext. It is not sent to the server but is passed to the completion callback function. This allows you to carry extra data to your callback that is not involved in the web service call without consuming resources to roundtrip it to the server.
Even code that runs correctly almost all of the time can encounter something unexpected. Listing 5-10 (from ThrowError.asmx) demonstrates this by throwing an exception to the web service code from Listing 5-8. If an error is encountered and an exception thrown, or if the timeout is exceeded, the error callback is called instead of the success completion function.
<%@ WebService Language="C#" Class="ThrowError" %>
using System;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class ThrowError : System.Web.Services.WebService {
[WebMethod]
public string TheTime(){
throw new Exception("failed miserably");
return DateTime.Now.ToString();
}
}
The object passed to the error handler will provide a JavaScript call stack when an exception is thrown. It also carries any exception message, what the HTTP status code was, and whether or not the error was caused by a timeout. The result of throwing this exception is shown in Figure 5-9.