Cutting Edge

Implications of Script Callbacks in ASP.NET

Dino Esposito

Code download available at:CuttingEdge0412.exe(241 KB)

Contents

Portability of Script Callbacks
Building the Sample
A Quick Client Callback FAQ

Script callbacks in ASP.NET 2.0 is a feature whose time has come. Script callbacks can significantly speed up an application by limiting server postbacks. They also allow you to execute small portions of server-side code without having to manage the view state for reading or writing.

In the August installment of Cutting Edge I began my discussion of ASP.NET 2.0 script callbacks, and this month I'll go into it further (see Cutting Edge: Script Callbacks in ASP.NET). I'll clarify some cross-platform implications of the feature and provide a realistic example that mimics the behavior of the MapPoint® Web site. As you read, remember that much of this has been based on Beta 1 of ASP.NET 2.0 and might change in future builds.

Portability of Script Callbacks

When it comes to portability, there is good and bad news. The bad news is that ASP.NET script callbacks are designed to work with browsers that can deal with dynamic page changes. Any browser that doesn't support Dynamic HTML (or similar models) can't execute callbacks since they need to refresh information without refreshing the whole page.

The good news is that there are quite a few browsers, in addition to Microsoft® Internet Explorer, that support callbacks. The list includes Netscape 7.0 and up and Safari 1.2 for Apple clients, to name just a couple. In fact, DHTML-capable browsers account for almost all of today's Web traffic.

For an ASP.NET page to run script callbacks, it needs to download some JavaScript code. Looking at the page source, you can spot the line responsible for pulling down the script:

<script src="WebResource.axd?a=s&r=WebForms.js&t={guid}" type="text/JavaScript"> </script>

As I explained in the August 2004 column, a JavaScript function triggers the out-of-band call. The name and prototype of this function are provided by a new method on the Page class—the GetCallbackEventReference method:

Dim callbackRef As String = _ GetCallbackEventReference(Me, input, callback, _ "null", "null")

The GetCallbackEventReference method has two key purposes. First, it returns a string that represents the JavaScript function call that starts the remote method invocation. You bind this str ing to a client-side event of choice—for example, a mouse click. An example of the JavaScript call is shown here:

onclick="WebForm_DoCallback('__Page', 'North', UpdateMap, null, null)"

Of particular importance are the second and third parameters. The second indicates the input parameter to the remote method; the third indicates a local JavaScript function to consume results and refresh the page upon completion of the call.

The second key action that GetCallbackEventReference performs is injecting into the page the <script> tag you saw earlier. The tag references a server-side script file that contains the definition of WebForm_DoCallback.

Take a look at the content of the WebForms.js file to understand how ASP.NET script callbacks really work. Open the temporary Internet files folder, locate a resource with the matching URL, and view the resource (see Figure 1). Alternatively, you can simply request the WebForms.js file from the Web server by copying and pasting the file's URL from the script tag in the rendered page's source into the browser's address bar.

Figure 1 Locating WebForms.js

Figure 1** Locating WebForms.js **

Figure 2 shows the source code of the WebForm_DoCallback function. Note that the code in the figure is based on Beta 1 and might change in future builds. As a page author, you'll never be exposed to that code, but it helps you appreciate how smart ASP.NET 2.0 can be.

Figure 2 Callback Manager Implementation

function WebForm_DoCallback(eventTarget, eventArgument, eventCallback, context, errorCallback) { re = new RegExp("\\x2B", "g"); if (__nonMSDOMBrowser) { var xmlRequest = new XMLHttpRequest(); postData = __theFormPostData + "__CALLBACKID=" + eventTarget + "&__CALLBACKPARAM=" + escape(eventArgument).replace(re, "%2B"); if (pageUrl.indexOf("?") != -1) { xmlRequest.open("GET", pageUrl + "&" + postData, false); } else { xmlRequest.open("GET", pageUrl + "?" + postData, false); } xmlRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xmlRequest.send(null); response = xmlRequest.responseText; ... } else { var xmlRequest = new ActiveXObject("Microsoft.XMLHTTP"); xmlRequest.onreadystatechange = WebForm_CallbackComplete; __callbackObject.xmlRequest = xmlRequest; __callbackObject.eventCallback = eventCallback; __callbackObject.context = context; __callbackObject.errorCallback = errorCallback; postData = __theFormPostData + "__CALLBACKID=" + eventTarget + "&__CALLBACKPARAM=" + escape(eventArgument).replace(re, "%2B"); usePost = false; if (pageUrl.length + postData.length + 1 > 2067) { usePost = true; } if (usePost) { xmlRequest.open("POST", pageUrl, true); xmlRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xmlRequest.send(postData); } else { if (pageUrl.indexOf("?") != -1) { xmlRequest.open("GET", pageUrl + "&" + postData, true); } else { xmlRequest.open("GET", pageUrl + "?" + postData, true); } xmlRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xmlRequest.send(); } } }

As you can see, the behavior of WebForm_DoCallback changes for two categories of browsers: Internet Explorer and everything else. The value of the __nonMSDOMBrowser variable is set to the following expression upon page loading:

var __nonMSDOMBrowser = (window.navigator.appName.toLowerCase().indexOf('explorer') == -1);

When the browser is Internet Explorer (version 5.0 or above) the script callback mechanism is implemented through the well-known Microsoft.XmlHttp COM object. It requires that the browser be set to run ActiveX® controls. If the browser is not Internet Explorer, a totally different approach is taken; a new component in the browser's local document object model (DOM), XMLHttpRequest, will be used.

XMLHttpRequest is a little-known object that a surprising number of Web browsers support. This object is quite similar to Microsoft.XmlHttp. Microsoft introduced the Microsoft.XmlHttp object with Internet Explorer 5.0 for Windows® back in 1999. Shortly afterwards, some architects on the Mozilla project implemented a compatible native version of it for Mozilla and Netscape 7.x. Later on, Apple did the same starting with Safari 1.2. The programming interface of XMLHttpRequest looks nearly identical to that of XmlHttp. This is not a coincidence—the XMLHttpRequest DOM object was created to provide other browsers with the same set of functionalities as the XmlHttp COM object.

Obviously, script callbacks won't work in older browsers that do not support the DOM; likewise, the feature has a limited range of applicability (but might work) on browsers that don't support dynamic changes to content and layout. For example, it doesn't work on Netscape 4.x (no XMLHttpRequest object is defined), but works like a champ on the newest Netscape 7.2.

Technically speaking, script callbacks provide the plumbing to make a remote call to the current URL without redrawing the page and without disrupting the application flow. This is possible on a variety of browsers—basically all the fifth-generation browsers such as Internet Explorer 5.x and above, Mozilla/Netscape 6.x and above, Safari 1.2x, and Opera 7.x. Browser compatibility guarantees end once the script callback has been executed and the server-generated data has been retrieved. At this point, the JavaScript code you write to handle the results from the server determines how compatible your application is with the various browsers.

To ensure cross-platform compatibility, you should strive for a DOM syntax that provides equivalent functionality and is accepted by all browsers. I'll return to this later. For now, suffice it to say that the sample code I'm going to illustrate works with both Internet Explorer 6.0 and Netscape 7.2. To let you determine dynamically if the current browser supports ASP.NET script callbacks, the HttpBrowserCapabilities class has been extended with two new properties: SupportsCallbacks and SupportsXmlHttp.

SupportsCallbacks indicates whether the browser supports advanced script scenarios like those in which scripts are invoked in callback mode upon completion of asynchronous operations. SupportsXmlHttp indicates whether the browser supports sending and receiving XML data over HTTP. Figure 3 shows a sample page you can use to check these browser capabilities.

Figure 3 Check Browser Capabilities

<%@ Page language="VB" %> <%@ Import namespace="System.Web.Configuration" %> <script runat="server"> Private Sub Page_Load(ByVal Sender As Object, ByVal e As EventArgs) CheckBrowserCaps() End Sub Private Sub CheckBrowserCaps() Dim labelText As String = String.Empty Dim caps As HttpBrowserCapabilities = Request.Browser ' Browser name labelText += caps.Browser & "<hr>" ' Supports Callback If caps.SupportsCallback Then labelText += "Browser supports callback scripts.<br>" Else labelText += "Browser does not support callback scripts.<br>" End If ' Supports XmlHttp If caps.SupportsXmlHttp Then labelText += "Browser supports XML HTTP scripts.<br>" Else labelText += "Browser does not support XML HTTP scripts.<br>" End If Label1.Text = labelText End Sub </script> <html> <head> <title>Browser Capabilities Sample</title> </head> <body> <form> <div> Browser Capabilities... <p/><asp:Label ID="Label1" Runat="server" /> </div> </form> </body> </html>

Are there alternatives to ASP.NET callbacks that can run on even more browsers? Remote Scripting works on a wider range of browsers and platforms. It requires an IIS Web server and does its work using ASP. Also worth a look is the JavaScript Remote Scripting (JSRS) library, which you'll find at www.ashleyit.com/rs/main.htm. JSRS is a client-side library that lets you make hidden remote procedure calls to the server. There are server-side implementations for server environments like ASP, Cold Fusion, PerlCGI, PHP, Python, and JSP. JSRS is free and available with source code.

However, a huge advantage to using ASP.NET script callbacks is that they're part of the ASP.NET Page model. Yes, other callback mechanisms are available that can deal with a variety of server environments, but ASP.NET callbacks integrate with the page lifecycle, and as a result, a developer can leverage the context of a lightweight page environment created to run the request.

Building the Sample

Imagine you have a Web site that supplies reporting services. Chances are good that at some point you'll have to present data in a chart. Later, someone looking at the report might want to drill down into the data. You could display another image, and refresh the whole page each time this happens, but that's not very efficient. Fortunately, script callbacks can help. Before I explain how, let's take a look at Microsoft MapPoint as an example.

To use MapPoint, you navigate to the site, enter a city, and get your map (see Figure 4). Now choose a direction button and click to pan the image accordingly. You'll notice that the page remains static and only the image is refreshed. In a minute, you'll see how to implement the same feature using script callbacks.

Figure 4 The UI of the MapPoint Site

Figure 4** The UI of the MapPoint Site **

While this functionality could be reproduced using only client-side JavaScript, script callbacks provide a good mix of server- and client-side execution, performance, and cross-platform support. Figure 5 shows the sample page in action in Netscape 7.2.

Figure 5 The Sample Page in Netscape 7.2

Figure 5** The Sample Page in Netscape 7.2 **

I built the sample page using some of the hot new technologies available in ASP.NET 2.0 like Master Pages and codebehind files. The Master Page determines the layout of derived pages by defining replaceable regions that content pages fill. The master I use here incorporates a couple of images, a page description, and a body. A content page must have a very special structure, as shown in Figure 6. At least in the Beta 1 build of ASP.NET 2.0, content pages are not allowed to contain anything but <asp:Content> tags. You can't even insert comments or client-side script blocks outside replaceable regions. To add some script code to the content page, you can define an additional placeholder and fill it with script and anything else you may need. This approach is acceptable to the ASP.NET page parser.

Figure 6 The Sample Implemented as a Content Page

<%@ Page Language="VB" MasterPageFile="~/MsdnMag.master" CompileWith="Default.aspx.vb" ClassName="Default_aspx" Title="Cutting Edge (Dec04)" %> <%@ Implements interface="System.Web.UI.ICallbackEventHandler" %> <asp:Content Runat="server" ContentPlaceHolderID="PageScript"> <script language="javascript"> function UpdateMap(result, context) { // Unpack the result and extract image URL and // ID of the map control var o = result.split(","); // Set the new image var id = o[1]; var img = document.getElementById(id); var url = o[0]; img.src = url; } </script> </asp:Content> <asp:Content Runat="server" ContentPlaceHolderID="PageDescription"> <asp:Label Runat="Server" ID="Desc"> Using ASP.NET 2.0 script callbacks to scroll an image without refreshing the whole page. </asp:Label> </asp:Content> <asp:Content Runat="server" ContentPlaceHolderID="PageBody"> <h2>Santa Claus's hometown</h2> <table bgcolor="#004472"><tr> <td colspan="3" align="center"> <asp:Image Runat="Server" ID="GoNorth" ToolTip="Move north" ImageUrl="images\gonorth.gif" /> </td> </tr> <tr> <td> <asp:Image Runat="Server" ID="GoWest" ToolTip="Move west" ImageUrl="images\gowest.gif" /> </td> <td> <asp:Image Runat="server" ID="Map" ImageUrl="images\SantaHomeTown.gif" /> </td> <td align="right"> <asp:Image Runat="Server" ID="GoEast" ToolTip="Move east" ImageUrl="images\goeast.gif" /> </td> </tr> <tr> <td colspan="3" align="center"> <asp:Image Runat="Server" ID="GoSouth" ToolTip="Move south" ImageUrl="images\gosouth.gif" /> </td> </tr></table> </asp:Content>

The body of the page is a 3×3 table containing a central image surrounded with navigational buttons used to change which portion of the map is displayed. Each move button is associated with a client-side event to trigger the callback mechanism. To implement graphic buttons, like the North button in Figure 5, ASP.NET supplies the excellent ImageButton control:

<asp:ImageButton Runat="Server" ID="GoNorth" ToolTip="Move north" ImageUrl="images\gonorth.gif" />

ImageButton derives from Image but implements the new IButtonControl interface along with IPostBackEventHandler. As a result, the control behaves like a button and, more importantly, always posts back when the user clicks on it. The control works like the Button control of ASP.NET 1.x, which always renders as a submit button. In ASP.NET 2.0, the Button control features a new Boolean property—UseSubmitBehavior—that renders it as a client-side button so that the click doesn't cause the page to post. You can build a pseudo image button using the following code:

<asp:Image Runat="Server" ID="GoNorth" Style="cursor:hand;" ToolTip="Move north" ImageUrl="images\gonorth.gif" />

The Image control must be associated with a client click event handler, as shown here:

Dim callbackRef As String = GetCallbackEventReference( _ Me, "'North'", "UpdateMap", "null", "null"); GoNorth.Attributes("onclick") = callbackRef

The code runs during the page loading phase. Once the user clicks, the underlying ASP.NET callback manager sends the new request in the background. Figure 7 shows the RaiseCallbackEvent method that each ASP.NET page must provide as part of the ICallbackEventHandler interface. This is the server-side method that is invoked on each out-of-band call. The method is passed a string (here, "North") that represents the direction of the newly requested image. This information is passed along to the method that retrieves the new image. All the images are organized in a 3¥3 matrix, at the center of which is the main image. The main image is the map centered on the city of Rovaniemi. Two indexes (map row and column) are defined to track the current position and subsequently the URL of the image displayed. When the user pans the image, row and column indexes are updated depending on the direction required, and a new URL is retrieved from the matrix and returned to the callback manager.

Figure 7 Raised Callback Event

Public Overridable Function RaiseCallbackEvent( _ ByVal mapDir As String) As String _ Implements ICallbackEventHandler.RaiseCallbackEvent ' Get the direction of the panning Dim dir As MapDirections = CType([Enum].Parse( _ GetType(MapDirections), mapDir), MapDirections) ' Obtain the new image Dim NewImage As String = DetermineNewImage(dir) ' The output string contains two pieces of information: ' image, direction to block Dim buf() As String = New String(2) {} buf(0) = NewImage buf(1) = Map.ClientID Return String.Join(",", buf) End Function

UpdateMap is the JavaScript callback function that executes when the remote call has completed. UpdateMap receives the URL of the new image and is called to update the Src attribute of the client-side counterpart of the Image control. You access this client-side component like so:

GoNorth.src = url;

When hosted in content pages, though, the previous code won't work because no HTML element is found in the client page that is named GoNorth. This apparently contradicts the ASP.NET page source, where you clearly have an Image control with an ID of GoNorth. What's up?

The IDs of controls defined in a Master Page placeholder have their names mangled and prefixed with the ID of the region and other information. The actual ID of the GoNorth image control is something different. In the example, it is ctl00_PageBody_GoNorth. This is why ASP.NET has provided the ClientID property since 1.0. A control's UniqueID property is used to find a control in the context of server execution, while the ClientID is used for writing client-side script to access the element rendered to the client. So, I modified the server method to make it return the client ID of the control to be refreshed. In summary, the string returned to the JavaScript callback function is a comma-separated string in which the first token contains the URL of the image and the second contains the client ID of the control to refresh.

As mentioned, the sample page needs to track row and column indexes to quickly locate the URL of the image to show as the user clicks on the displayed image. Normally, this information is placed in the view state to avoid taxing the server memory. However, to handle view state updates completely on the server would require that the entire page lifecycle be executed. This is contrary to the design of the script callback feature. While the feature does allow for avoiding a complete redraw of the client, it also allows for lightweight page execution, running only what is necessary to process the callback. The remote call posts all the input fields of the current page along with two additional values, CALLBACKID and CALLBACKPARAM, as Figure 2 shows. In the collection of input fields you'll find the view state information as well. So the view state is sent back during the remote invocation and gets correctly restored. However, any change you make on the server that alters the view state of the page is lost because the view state is not persisted upon exit. You might disagree with this design choice, but then the view state should be attached to the response you serve to the JavaScript callback and some script would be necessary to replace it on the fly. It may be possible, but is it worth the cost?

To work around the issue, avoid using the view state in the implementation of page or control properties that need be updated during script callback operations. If the properties are to survive page requests, you can use session state, as you can see in Figure 8.

Figure 8 Use Session Instead of View State

Private Property MapRow() As Integer Get Return CType(Session("MapRow"), Integer) End Get Set (ByVal Value As Integer) Session("MapRow") = value End Set End Property Private Property MapColumn() As Integer Get Return CType(Session("MapColumn"), Integer) End Get Set (ByVal Value As Integer) Session("MapColumn") = value End Set End Property

A Quick Client Callback FAQ

To finish the second leg of the ASP.NET script callback coverage, let's address a couple of side points. The first involves values you can send and receive through client callbacks. The only clear restriction you have here is that the server method RaiseCallbackEvent must receive and return a string. You can devise and implement just about any functionality around that requirement. For example, you could exchange strings that evaluate to objects. The main issue I see with this, though, is that you need to manage two different types of objects—managed .NET objects on the server and JavaScript objects on the client. The client can build and send a SOAP representation of the .NET object and the server can return the string description of a JavaScript object.

The beauty of callbacks is that they work asynchronously; therefore there should be no problem if multiple callback operations are simultaneously started. Unfortunately, this feature doesn't work in Beta 1 due to a known bug. If multiple callback events are fired in succession, only one is really fired; the others are lost.

ASP.NET script callbacks provide a good mix of power, performance, portability, and even security. Both pages and individual controls can take advantage of this feature. Now you know what's needed for a page to invoke a remote method. In a future column I'll show how to implement a custom control that takes advantage of callbacks.

Send your questions and comments for Dino to  cutting@microsoft.com.

Dino Esposito is a Wintellect instructor and consultant based in Italy. Author of Programming ASP.NET and the newest Introducing ASP.NET 2.0 (both from Microsoft Press), he spends most of his time teaching classes on ASP.NET and ADO.NET and speaking at conferences. Get in touch with Dino at cutting@microsoft.com or join the blog at weblogs.asp.net/despos.