Partial Page Rendering

The key concept behind AJAX development is that much more work than usual should occur on the client. For the time being, working on the client side also means using a script language, which is not something that would make most developers jump for joy. A large share of Web developers have a love-hate relationship with JavaScript and with script languages in general. Either they love it and can achieve virtually any results by leveraging the flexibility of the syntax, or they hate it and feel ill just at the appearance of a client

As you saw in Chapter 2, “The Microsoft AJAX Client Library,” PageRequestManager is one of the classes that form the client library of ASP.NET AJAX. In particular, the class is in charge of running asynchronous calls through the XMLHttpRequest object.

  Note 

The _initialize and _updateControls methods are conventionally considered private members of the PageRequestManager class; yet they are externally callable. In Chapter 2, we learned that classes in the Microsoft Client Library (MCL) can’t really have private members. By convention, private members have their names prefixed with the underscore (_). However, this doesn’t prevent code from calling into the underscored methods when necessary.

The _initialize method wires up new event handlers for the form’s submit and click events and for the window’s unload event. The _updateControls method takes four parameters: the list of updatable panels in the form; two arrays with the ID of form controls capable of firing AJAX and classic postbacks, respectively; and the time, in seconds, allowed for the update to complete before timing out. (The default is 90 seconds.) You can take a look at the real implementation of the method by scanning the source code contained in the file MicrosoftAjaxWebForms.js.

Format of the AJAX Request

So the _initialize method prepares a new AJAX request for each classic postback that is going to take place. Manipulated by PageRequestManager’s script, the body of an example GridView update request sent to the server looks much like this:

ScriptManager1=UpdatePanel1|GridView1& __EVENTTARGET=GridView1& __EVENTARGUMENT=Page%243& __VIEWSTATE=%2Fw ... 3D& __EVENTVALIDATION=%2Fw ... 3D

The original request is the one sent over by a GridView control named GridView1 when the user clicks to view page #3. Note that the %24 character is the HTML-encoded format for the dollar symbol ($).

In addition to the view-state and event-validation data (which are specific features of ASP.NET 2.0 postbacks), the request contains the ID of the updatable panel that will serve the call (UpdatePanel1 in the snippet just shown) and the server control target of the postback (in this case, GridView1). The actual format of the request might change a bit depending on the characteristics of the posting control. For example, GridView uses the __EVENTTARGET field to pass its name to the server. A Button control, on the other hand, uses a new parameter, as shown here:

ScriptManager1=UpdatePanel1|Button1& __EVENTTARGET=& __EVENTARGUMENT=& __VIEWSTATE=%2Fw ... 3D& __EVENTVALIDATION=%2Fw ... 3D& Button1=Refresh

You can look at the bits of the request either by using an ad hoc network monitor tool or by simply adding some debug code to the Page_Load event handler:

protected void Page_Load(object sender, EventArgs e) { // Need permissions to write Request.SaveAs(@"c: eq_details.txt", true); }

In case of anomalies and malfunctions, it’s crucial to analyze the response of an AJAX call. For this reason, you need to provide yourself with a tool that monitors HTTP traffic. Fiddler is certainly a good one. You can read all about its features at http://www.fiddlertool.com/fiddler. It works as a proxy and logs all HTTP traffic so that you can later examine the contents of each request and response. Fiddler supports Internet Explorer as well as other browsers.

Developed by a member of the ASP.NET AJAX team, the Web Development Helper utility is a free tool specifically for Internet Explorer. You can download it from http://www.nikhilk.net/ Project.WebDevHelper.aspx. The utility allows viewing information about the current ASP.NET page, such as view state and trace output. In addition, it can also perform some operations on the server, such as restarting the application or managing the Cache object. Finally, the utility provides developers with the ability to view the live HTML DOM and allows for monitoring requests and responses for diagnostic scenarios. Most features of the tool work only when the application is hosted by the local host. Installing the tool requires a bit of manual work, as it is implemented as an Internet Explorer browser helper object. Figure 4-7 shows the tool in action. Note that after installing the tool, you display it by using the View|Explorer Bar menu in Internet Explorer.

Figure 4-7: The Web Development Helper tool in action

Detecting AJAX Requests at Runtime

On the server, the postback is processed as usual, and indeed there’s no way for the ASP.NET runtime to detect it’s an AJAX call to refresh only a portion of the original page.

More precisely, each AJAX request contains an extra header that is missing in traditional postback requests. However, only ad hoc additional runtime components (for example, HTTP modules)-not the default set of runtime components-can take advantage of this. Here are the details of the new request header:

x-microsoftajax: Delta=true

Figure 4-8 shows the extra header using the user interface of the Web Development Helper tool.

Figure 4-8: The extra request header that decorates ASP.NET AJAX requests sent through the UpdatePanel control

The header is added only for requests managed through the UpdatePanel control. Other AJAX requests-for example, requests for Web service methods-don’t include the header. These requests, though, have a special content type: application/json.

To detect an AJAX request from the page life cycle, you just use the IsInAsyncPostBack property on the ScriptManager control.

Format of the AJAX Response

From within the page life cycle, the script manager hooks up the standard rendering process by registering a render callback method. The modified rendering process loops through all the registered updatable panels and asks each to output its fresh markup.

At the end of the request, the client is returned only contents of updatable regions, and a JavaScript callback function buried in the client library takes care of replacing the current content of regions with any received markup. Here’s the typical response of an AJAX UpdatePanel response:

2735|updatePanel|UpdatePanel1| ... -- markup for all updated panels goes here -- ... |0|hiddenField|__EVENTTARGET| |0|hiddenField|__EVENTARGUMENT| |684|hiddenField|__VIEWSTATE|5A ... /0= |64|hiddenField|__EVENTVALIDATION|H2 ... /0=

The output is made of a sequence of records. Each record contains the overall size, a keyword that identifies the client container of the following information, optionally the name of the container (control, variable), and the actual content. Two consecutive records (and fields inside a single record) are separated with pipe characters ( | ). In the preceding code snippet, more than 2 KB of information are being returned as the replacement for the contents of UpdatePanel control named UpdatePanel1. In addition, the response includes empty __EVENTTARGET and __EVENTARGUMENT hidden fields, plus updated view-state and event-validation data. The response also includes other helper records that cache parameters for the partial update (basically, the input data for the _initialize method), the action URL, and the new page title.

The updated markup constitutes the lion’s share of the response, followed by the updated view-state and event-validation data. All remaining data rarely adds up to an extra KB of information.

One Partial Update at a Time

From the user perspective, a partial page update is an asynchronous operation, meaning that the user can continue working with the controls page and animation can be displayed to entertain the user during a lengthy task and to provide feedback. Two partial updates, though, can’t run together and concurrently.

If you trigger a second partial update when another one is taking place, the first call is aborted to execute the new one. As a result, just one partial update can be executed at a time. This limitation is by design because each partial update updates the view state and event-validation data. To ensure consistency, each AJAX postback must find and return a consistent view state for the next postback to start.

As we’ll see later in the chapter, an updatable region can be bound to events fired by the PageRequestManager client object that signal key steps in the client life cycle of an update such as begin/end request, page loaded, and the like. By handling these events, you can cancel additional postback requests while another one-with a higher priority-is going on. I’ll say more about this later.

Error Handling in Partial Updates

As discussed in Chapter 3, any errors that occur during a partial update are captured by the script manager control and handled in a standard way. The script manager swallows the real exception and sends back to the client a response record similar to the following:

53|error|500|Object reference not set to an instance of an object.|

The first piece of information is the size of the record, followed by a keyword that qualifies the information. Next, you get the error code and the message for the user. From a pure HTTP perspective, the packet received denotes success-HTTP status code 200. The client-side script, though, knows how to handle an error response and by default pops up a message box with the message received.

As in Chapter 3, you can customize the error handling both on the server and the client. On the server, you can hook up the AsyncPostBackError event on the ScriptManager control and set a custom error message through the AsyncPostBackErrorMessage property:

void AsyncPostBackError(object sender, AsyncPostBackErrorEventArgs e) { ScriptManager1.AsyncPostBackErrorMessage = "An error occurred: " + e.Exception.Message; }

You don’t need to write such an event handler if you just want to return a generic and static message whatever the exception raised. In this case, you simply set the property to the desired string in Page_Load.

On the client, you can register a handler for the endRequest event of the PageRequestManager object, do your custom client handling, and then state that you’re done with the error:

The endRequest client event fires at the end of a partial page refresh operation.

Taking Control of Updatable Regions

So far, I’ve considered only pages with a single UpdatePanel control and silently assumed that any updatable panel had to refresh on every postback. It’s more realistic, however, to expect that you’ll have pages with multiple panels. In addition, the updating of panels will likely be triggered by controls that are scattered in the page and not necessarily placed inside the panel. Finally, it’s common to have each panel logically bound to a well-known set of triggers-button clicks, value changes, or HTML events-and refresh only if any of the assigned triggers are active. Let’s see how to take full control of the page update process and assign triggers, conditionally update panels, and use other, more advanced features.

Triggering the Panel Update

In all the examples I’ve illustrated thus far, the control that causes the asynchronous AJAX postback has always been part of the updatable panel and defined within the Template> section. This arrangement is not what you will see in all cases. In general, a panel update can be triggered by any page controls that have the ability to post back. Furthermore, an updatable panel also can be refreshed programmatically when proper runtime conditions are detected during a partial page update caused by another panel.

By default, an UpdatePanel control refreshes its contents whenever a postback occurs within the page. If the control that posts back is placed outside the updatable panel, a full refresh occurs. Otherwise, only the panel that contains the control refreshes. You can programmatically control under which conditions the contents of the panel are refreshed. You have three tools to leverage: the UpdateMode, ChildrenAsTriggers, and Triggers properties.

Updating Modes

Through the UpdateMode property, the UpdatePanel control supports two update modes: Always (the default) and Conditional. The values are grouped in the UpdatePanelUpdateMode enumeration, listed in Table 4-3.

Table 4-3: The UpdatePanelUpdateMode Enumeration

Open table as spreadsheet

Value

Description

Always

The panel is updated on each postback raised by the page.

Conditional

The panel is updated only when the update is triggered by one of the assigned panel triggers or the update is requested programmatically.

When the UpdateMode property is set to Always, each postback that originates around the page-from controls inside and out of the panel-triggers the update on the panel. This means that if you have multiple updatable panels in a page, all of them will update even though the event concerns only one of them or even none of them.

When the UpdateMode property is set to Conditional, the panel is updated only when you call the Update method on the control during a postback or when a trigger associated with the UpdatePanel control is verified. The ChildrenAsTriggers Boolean property (which is true by default) defines whether children of an updatable panel also trigger a refresh.

We’ll also refer to children controls as “implicit triggers” and triggers defined to through the Triggers property as “explicit triggers.”

Conditional Refresh of an UpdatePanel Control

An UpdatePanel trigger defines a runtime condition-mostly a control event-that causes an UpdatePanel control to be refreshed when the page is working in partial rendering mode. Triggers make sense mostly when the panel is being updated conditionally or when children are not meant to be implicit triggers.

ASP.NET AJAX Extensions supports two types of triggers, both derived from the abstract class UpdatePanelControlTrigger. They are AsyncPostBackTrigger and PostBackTrigger. You associate triggers with an UpdatePanel control declaratively through the section or programmatically via the Triggers collection property. Here’s an example of asynchronous triggers in a page with two updatable panels:

Query string: Grid contents generated at: <%=DateTime.Now %> ...

This panel has been generated at: <%=DateTime.Now %>

The user types a query string in the text box that will be used to filter the customers in the grid. (See Figure 4-9.)

Figure 4-9: The contents of the grid are refreshed only when the user sets a filter

More precisely, the contents of the grid are refreshed when the user pages through the record and when the user clicks the Load button. Paging refreshes the grid as long as ChildrenAsTriggers is set to true-the default-because the link buttons used to page are child controls of the UpdatePanel. The Click event of the Load button, conversely, is registered as an asynchronous postback trigger because it is external to the panel.

As you can see in Figure 4-9, the time at which the grid was last refreshed doesn’t coincide with the time rendered in the second panel. According to the preceding code, to refresh the bottom panel you have to click the Refresh time button-a trigger for the UpdatePanel2 control.

Programmatic Updates of Panels

What if while refreshing the first panel you realize you need to update a second one? You can programmatically command a panel update by using the following code:

protected void Button1_Click(object sender, EventArgs e) { // Do as usual assuming we're refreshing UpdatePanel1 ... // Command an update on an external UpdatePanel control UpdatePanel2.Update(); }

You should resort to this approach only if some sort of implicit dependency exists between two panels. In this case, when you are in the process of updating one, there might be conditions that require you to update the second one also. Because at this point your code is executing on the server, there’s no way for you to do this other than by explicitly invoking the Update method on the panel.

Triggers of an UpdatePanel Control

As mentioned, there are two types of triggers for an UpdatePanel control-AsyncPostBackTrigger and PostBackTrigger. They have nearly the same syntax; both, when raised, cause the contents of the UpdatePanel control to be updated during a postback. Where’s the difference between the two? It’s indicated by the name, actually.

The event associated with the AsyncPostBackTrigger component triggers an asynchronous AJAX postback on the UpdatePanel control. As a result, the host page remains intact except for the contents of the referenced panel and its dependencies, if any. Usually, the AsyncPostBackTrigger component points to controls placed outside the UpdatePanel. However, if the panel has the ChildrenAsTriggers property set to false, it could make sense that you define an embedded control as the trigger. In both cases, when a control that is a naming container is used as a trigger, all of its child controls that cause postbacks behave as triggers.

You add an event trigger declaratively using the section of the UpdatePanel control:

...

You need to specify two pieces of information: the ID of the control to monitor, and the name of the event to catch. Note that the AsyncPostBackTrigger component can catch only server-side events fired by server controls. Both ControlID and EventName are string properties. For example, the panel described in the previous code snippet is refreshed when any of the controls in the page post back or when the selection changes on the drop-down list control named DropDownList1.

It should be noted that in no way does the panel refresh when a client event (i.e., onblur, onchange, click) fires. If you need to refresh when the selection on a list changes, either you set the AutoPostBack property on the control to true so that a key client event fires the postback or you wait for something else around the page to trigger the postback. For example, imagine you have UpdatePanel1 like in the snippet above and UpdatePanel2 bound to a button. When the user simply changes the selection on the drop-down list nothing happens. However, when another panel is refreshed-say, UpdatePanel2, even completely unrelated to the other-then the page places an AJAX postback and the page lifecycle is started for all controls in the page. At this point, the change in the drop-down list is detected and UpdatePanel1 is refreshed too.

Full Postbacks from Inside Updatable Panels

By default, all child controls of an UpdatePanel that post back operate as implicit asynchronous postback triggers. You can prevent all of them from posting by setting ChildrenAsTriggers to false. Note that when ChildrenAsTriggers is false, postbacks coming from child controls are just ignored. In no way are such postback events processed as regular form submissions. You can then re-enable only a few child controls to post back by adding them to the section of the UpdatePanel. These postbacks, though, will only refresh the panel.

There might be situations in which you need to perform full, regular postbacks from inside an UpdatePanel control in response to a control event. In this case, you use the PostBackTrigger component, as shown here:

...

The preceding panel features both synchronous and asynchronous postback triggers. The panel is updated when the user changes the selection on the drop-down list; the whole host page is refreshed when the user clicks the button.

  Note 

When should you use a PostBackTrigger component to fire a full postback from inside an updatable panel? Especially when complex and templated controls are involved, it might not be easy to separate blocks of user interface in distinct panels and single controls. So the easiest, and often the only, solution is wrapping a whole block of user interface in an updatable panel. If a single control in this panel needs to fire a full postback, you need the PostBackTrigger component.

A PostBackTrigger component causes referenced controls inside an UpdatePanel control to perform regular postbacks. These triggers must be children of the affected UpdatePanel.

The PostBackTrigger object doesn’t support the EventName property. If a control with that name is causing the form submission, the ASP.NET AJAX client script simply lets the request go as usual. The ASP.NET runtime will then figure out which server postback event has to be raised for the postback control by looking at its implementation of IPostBackEventHandler.

Triggering Periodic Partial Updates

ASP.NET pages that require frequent updates can be built to expose clickable elements so that users can order a refresh when they feel it is convenient. What if the update occurs frequently and periodically-that is, every n milliseconds? In this case, you can’t ask users to stay there and click all the time. Timers exist to help with such situations. And timers have been incorporated in virtually every browser’s DOM since the beginning of the Web.

The setTimeout method of the DOM window object allows you to create a client timer. Once installed, the timer periodically executes a piece of JavaScript code. In turn, this script code can do whatever is needed-for example, it can start an asynchronous call to the server and update the page automatically.

You can use script-based timers with any version of ASP.NET. To do so, though, you need to have some JavaScript skills and be aware of the characteristics of the browser’s DOM. ASP.NET AJAX shields you from most of the snags with its Timer control.

Generalities of the Timer Control

The Timer control derives from the Control class and implements the IPostBackEventHandler and IScriptControl interface:

public class TimerControl : Control, IPostBackEventHandler, IScriptControl

Implemented as a server-side control, it actually creates a client-side timer that performs a postback at regular intervals. When the postback occurs, the Timer server control raises a server-side event named Tick:

public event EventHandler Tick

The control features only two properties, as described in Table 4-4.

Table 4-4: Properties of the Timer Control

Open table as spreadsheet

Property

Description

Enabled

True by default, the property indicates whether the client-side script needed for the timer is generated when the page is first rendered.

Interval

This integer property indicates the interval at which a client timer raises its Tick event. The interval is expressed in milliseconds and is set by default to 60,000 milliseconds (one minute).

Basically, the Timer control is a server-side interface built around a client timer. It saves you from the burden of creating the timer via script and making it post back when the interval expires.

Using the Timer

The most common use of the Timer control is in conjunction with an UpdatePanel trigger to refresh the panel at regular intervals. The following script defines a timer that posts back every second:

It is extremely important that you carefully consider the impact of a too frequent interval on the overall performance and scalability of your application. Setting the interval to a low value (such as one or two seconds) might cause too many postbacks and traffic on the way to the server. As a result, even asynchronous postbacks performed in partial-rendering mode might incur some undesired overhead.

The following page mixes an UpdatePanel with a Timer control. The timer ticks every second and makes a postback. The UpdatePanel control is bound to the Tick event of the timer using a trigger:

As a result, the panel is updated every second. Put another way, the following code is run every second on the server resulting in a sort of Web clock:

protected void Timer1_Tick(object sender, EventArgs e) { Label1.Text = DateTime.Now.ToLongTimeString(); }

Figure 4-10 shows the sample page in action.

Figure 4-10: An ASP.NET AJAX clock in action in a sample page

Providing User Feedback During Partial Updates

Updating a panel might not be seamless from a user perspective, however. Having the computer engaged in a potentially long task, in fact, might be problematic. Will the user resist the temptation of reclicking that button over and over again? Will the user patiently wait for the results to show up? Finally, will the user be frustrated and annoyed by waiting without any clue of what’s going on? After all, if the page is sustaining a full postback, the browser itself normally provides some user feedback that this is happening. Using ASP.NET AJAX, the callback doesn’t force a regular full postback and the browser’s visual feedback system is not called upon to inform the user things are happening.

Keep in mind that ASP.NET AJAX Extensions is the ASP.NET incarnation of the AJAX paradigm. And in AJAX, the “A” stands for “asynchronous.” This implies that ASP.NET AJAX developers should take into careful account ways to explain latency to users and, wherever possible, provide ways for users to cancel pending requests.

Because of the need to keep users informed, ASP.NET AJAX supplies the UpdateProgress control to display a templated content while any of the panels in the page are being refreshed.

Generalities of the UpdateProgress Control

The UpdateProgress control is designed to provide any sort of feedback on the browser while one or more UpdatePanel controls are being updated. The UpdateProgress control derives from the Control class and implements the IScriptControl interface-an ASP.NET AJAX-specific interface that qualifies a custom server control as an AJAX control.

public class UpdateProgress : Control, IScriptControl

If you have multiple panels in the page, you might want to find a convenient location in the page for the progress control or, if possible, move it programmatically to the right place with respect to the panel being updated. You can use cascading style sheets (CSSs) to style and position the control at your leisure.

The UpdateProgress control features the properties listed in Table 4-5.

Table 4-5: Properties of the UpdateProgress Control

Open table as spreadsheet

Property

Description

AssociatedUpdatePanelID

Gets and sets the ID of the UpdatePanel control that this control is associated with.

DisplayAfter

Gets and sets the time in milliseconds after which the progress template is displayed. This property is set to 500 by default.

DynamicLayout

Indicates whether the progress template is dynamically rendered in the page. This property is set to true by default.

ProgressTemplate

Indicates the template displayed during an AJAX postback that is taking longer than the time specified through the DisplayAfter property.

An UpdateProgress control can be bound to a particular UpdatePanel control. You set the binding through the AssociatedUpdatePanelID string property. If no updatable panel is specified, the progress control is displayed for any panels in the page. The user interface of the progress bar is inserted in the host page when the page is rendered. However, it is initially hidden from view using the CSS display attribute.

When set to none, the CSS display attribute doesn’t display a given HTML element and it reuses its space in the page so that other elements can be shifted up properly. When the value of the display attribute is toggled on, existing elements are moved to make room for the new element. If you want to reserve the space for the progress control and leave it blank when no update operation is taking place, you just set the DynamicLayout property to false.

Composing the Progress Screen

ASP.NET AJAX displays the contents of the ProgressTemplate property while waiting for a panel to update. You can specify the template either declaratively or programmatically. In the latter case, you assign the property any object that implements the ITemplate interface. For the former situation, you can easily specify the progress control’s markup declaratively, as shown in the following code:

...

You can place any combination of controls in the progress template. However, most of the time you’ll probably just put some text there and an animated GIF. The lefthand page view shown in Figure 4-11 shows the ProgressPanel in action (at the top of the page). Notice the content begins with the letter ‘A’ while the letter ‘F’ is to be queried. The righthand view shows the outcome of the postback, which is to say values beginning with the letter ‘F’ are displayed after the data is asynchronously returned from the server.

Figure 4-11: A progress template informing users that some work is occurring on the server

Note that the UpdateProgress control is not designed to be a gauge component, but rather a user-defined panel that the ScriptManager control shows before the panel refresh begins and that it hides immediately after its completion.

  Note 

You can dynamically customize the look and feel of the progress control to some extent. All you have to do is write a handler for the Load event of the UpdateProgress control and retrieve the controls in the template using the FindControl method. In this way, you can change some standard text and make it a bit more context sensitive. Likewise, you can change animated images to make the displayed one better reflect the current context. This is often easier than replacing the template with an entirely new ITemplate-based value.

Aborting a Pending Update

A really user-friendly system will always let its users cancel a pending operation. How can you obtain this functionality with an UpdateProgress control? The progress template is allowed to contain an abort button. The script code injected into the page will monitor the button, and it will stop the ongoing asynchronous call if it’s clicked. To specify an abort button, you add the following to the progress template:

In the first place, the button has to be a client-side button. So you can express it either through the element or theelement for browsers that support this element. If you opt for the element, the type attribute must be set to button. The script code you wire up to the onclick event is up to you, but it will contain at least the following instructions:

You retrieve the instance of the client PageRequestManager object active in the client page and check whether an AJAX postback is ongoing. If a postback is in progress, you call the abortPostBack method to stop it.

  Important 

Canceling an ongoing update in this way is equivalent to closing the connection with the server. No results will ever be received and no updates will ever occur on the page. However, canceling the update is a pure client operation and has no effect on what’s happening on the server. If the user started a destructive operation, simply clicking the clientside Cancel button won’t undo the destructive operation on the server.

Associating Progress Screens with Panels

The AssociatedUpdatePanelID property allows you to associate a progress screen with a particular UpdatePanel control so that when the panel is refreshed the screen is displayed to the user. However, the implementation of this property in the context of the UpdateProgress control is not free of issues in spite of the strong sense of simplicity and clearness that name and description suggest.

The property works seamlessly as long as the refresh of an UpdatePanel control is caused by a child control. Should the refresh be ordered by an external trigger, no progress screen would ever be displayed for any panels in the page. This weird behavior is due to the code in the Sys.UI._UpdateProgress JavaScript class-the client object model of the control. The class is defined in the MicrosoftAjaxWebForms.js script file.

Before delving deeper into the reasons for the behavior, let me first address some workarounds. If the page can contain just one UpdateProgress to serve any number of updatable panels, then you’re just fine. You avoid setting the AssociatedUpdatePanelID property for the control and all panels automatically share the same progress screen.

If distinct UpdatePanel controls require distinct progress screens, and these panels are bound to external triggers, the only way for you to display the correct progress screen passes through the addition of a bit of JavaScript code that manually displays the right screen. In other words, you bypass the automatic display mechanism of the UpdateProgress control that fails if an external trigger fires. I’ll show this in a moment after introducing client-side events.

What does cause the script of the UpdateProgress control to fail when an external trigger fires an update? The progress screen is displayed just before the request is sent out and only if the request regards the updatable panel referenced by the AssociatedUpdatePanelID property. The point is, the ID of the panel being updated is not known to the UpdateProgress script. The script attempts to find it out by walking up the hierarchy of the element that caused the postback. Clearly, if the posting element is outside of the UpdatePanel’s tree (i.e., an external trigger) the search is unsuccessful and no progress screen is ever displayed.

Client Side Events for a Partial Update

Each ASP.NET AJAX postback involves the PageRequestManager client object, which is responsible for invoking, under the hood, the XMLHttpRequest object. What kind of control do developers have on the underlying operation? If you manage XMLHttpRequest directly, you have full control over the request and response. But when these key steps are managed for you, there’s not much you can do unless the request manager supports an eventing model.

The PageRequestManager object provides a few events so that you can customize handling of the request and response.

Events of the Client PageRequestManager Object

The client events listed in Table 4-6 are available on the client PageRequestManager object. As you can see, these events signal the main steps taken when an AJAX postback partially updates a page. The events are listed in the order in which they fire to the client page.

Table 4-6: Events of PageRequestManager Object

Open table as spreadsheet

Event

Event Argument

Description

initializeRequest

InitializeRequestEventArgs

Occurs before the AJAX request is prepared for sending.

beginRequest

BeginRequestEventArgs

Occurs before the request is sent.

pageLoading

PageLoadingEventArgs

Occurs when the response has been acquired but before any content on the page is updated.

pageLoaded

PageLoadedEventArgs

Occurs after all content on the page is refreshed as a result of an asynchronous postback.

endRequest

EndRequestEventArgs

Occurs after an asynchronous postback is finished and control has been returned to the browser.

To register an event handler, you use the following JavaScript code:

var manager = Sys.WebForms.PageRequestManager.getInstance(); manager.add_beginRequest(OnBeginRequest);

The prototype of the event handler method-OnBeginRequest in this case-is shown here:

function beginRequest(sender, args)

The real type of the args object, though, depends on the event data structure. The other events have similar function prototypes.

Kick In before the Request Starts

The initializeRequest event is the first event in the client life cycle of an AJAX request. The life cycle begins at the moment in which a postback is made that is captured by the UpdatePanel’s client infrastructure. You can use the initializeRequest event to evaluate the postback source and do any additional required work. The event data structure is the InitializeRequestEventArgs class. The class features three properties: postBackElement, request, and cancel.

The postBackElement property is read-only and evaluates to a DomElement object. It indicates the DOM element that is responsible for the postback. The request property (read-only) is an object of type Sys.Net.WebRequest and represents the ongoing request. Finally, cancel is a readwrite Boolean property that can be used to abort the request before it is sent.

Immediately after calling the initializeRequest handler, if any, the PageRequestManager object aborts any pending asynchronous requests. Next, it proceeds with the beginRequest event and then sends the packet.

A typical scenario for many Web applications is that the user clicks to start a potentially lengthy operation, no (or not enough) feedback is displayed, and the user keeps on clicking over and over again. Given the implementation of PageRequestManager, any new request kills the current request that is still active. Note that the abort has no effect on the server-side operation; rather, it simply closes the connection and returns an empty response. This potentially results in multiple actions on the server, one for each button click, yet the user will only see the results of the final button click (if they’re patient enough).

By handling the initializeRequest event, though, you can assign a higher priority to the current event and abort any successive requests until the other has terminated. Let’s see how to implement this sort of click-only-once functionality:

The preceding script aborts any requests originated by the Button1 element if another request from the same element is still being processed. In this way, when the user clicks several times on the same button, no other requests will be accepted as long as there’s one being processed. (See Figure 4-12.)

Figure 4-12: Users are allowed to start only one high-priority task at a time

Disabling Visual Elements during Updates

If you want to prevent users from generating more input while a partial page update is being processed, you can also consider disabling the user interface-all or in part. To do so, you write handlers for beginRequest and endRequest events:

The beginRequest event is raised before the processing of an asynchronous postback starts and the postback is sent to the server. You typically use this event to call custom script to animate the user interface and notify the user that the postback is being processed. You can also use the beginRequest event to set a custom request header that identifies your request uniquely.

// Globals var currentPostBackElem; var oldStyleString = ""; function OnBeginRequest(sender, args) { currentPostBackElem = args.get_postBackElement(); if (typeof(currentPostBackElem) === "undefined") return; if (currentPostBackElem.id.toLowerCase() == "button1") { // Disable the Load button $get("Button1").disabled = true; // Optionally, highlight the grid oldStyleString = $get("GridView1").style.border; $get("GridView1").style.border = "solid red 5px"; } }

The beginRequest handler receives event data through the BeginRequestEventArgs data structure-the args formal parameter. The class features only two properties: request and postBackElement. The properties have the same characteristics as the analogous properties on the aforementioned InitializeRequestEventArgs class.

In the preceding code snippet, I disable the clicked button to prevent users from repeatedly clicking the same button. In addition, I draw a thick border around the grid to call the user’s attention to the portion of the user interface being updated. (See Figure 4-13.)

Figure 4-13: The Load button, disabled and grid-framed during the server processing

At the end of the request, any temporary modification to the user interface must be removed. So animations must be stopped, altered styles must be restored, and disabled controls re-enabled. The ideal place for all these operations is the endRequest event. The event passes an EndRequestEventArgs object to handlers. The class has a few properties, as described in Table 4-7.

Table 4-7: Properties of the EndRequestEventArgs Class

Open table as spreadsheet

Property

Description

dataItems

Returns the client-side dictionary packed with server-defined data items for the page or the control that handles this event. (I’ll say more about registering data items later.)

error

Returns an object of type Error that describes the error (if any) that occurred on the server during the request.

errorHandled

Gets and sets a Boolean value that indicates whether the error has been completely handled by user code. If this property is set to true in the event handler, no default error handling will be executed by the Microsoft AJAX client library. We saw an example of this property in Chapter 3.

response

Returns an object of type Sys.Net.WebRequestExecutor that represents the executor of the current request. Most of the time, this object will be an instance of Sys.Net.XMLHttpExecutor. For more information, refer to Chapter 2.

As you can see, when the endRequest event occurs there’s no information available about the client element that fired the postback. If you need to restore some user-interface settings from inside the endRequest event handler, you might need a global variable to track which element caused the postback. In the following code, you need to track the postback trigger as well as the original style of the grid that is shown in Figure 4-13:

function OnEndRequest(sender, args) { if (typeof(currentPostBackElem) === "undefined") return; if (currentPostBackElem.id.toLowerCase() == "button1") { $get("Button1").disabled = false; $get("GridView1").style.border = oldStyleString; } }

Managing Progress Screens

To display the progress screen, you wait for the beginRequest event, then apply your own logic to decide which screen is appropriate and go. Here’s a quick example:

You first check the ID of the postback element. Next, based on that information you figure out the update progress block to turn on. You display or hide a block of markup by acting on its display CSS attribute. Additional progress screens would be handled in the same way (i.e., by adding code to display them in OnBeginRequest).

Page Loading Events

In an asynchronous postback, there are two distinct and nested phases: begin/end of the request, and begin/end of the partial page update. After the request has been sent to the server, the client waits for any response to become available.

  Note 

If you’re curious about the real sequence of steps accomplished to execute an asynchronous AJAX request, take a look at the _onFormSubmit method on the Sys.WebForms. PageRequestManager class. The class is defined in the MicrosoftAjaxWebForms.js script file.

When the response arrives, the PageRequestManager object first processes any returned data and separates hidden fields, updatable panels, data items, and whatever pieces of information are returned from the server. Once the response data is ready for processing, the object fires the pageLoading client event. The event is raised after the server response is received but before any content on the page is updated. You can use this event to provide a custom transition effect for updated content or to run any clean-up code that prepares the panels for the next update.

The event data is packed in an instance of the class PageLoadingEventArgs. The class has three properties: panelsUpdating, panelsDeleting, and dataItems. The first two are arrays and list the updatable panels to be updated and deleted, respectively. The dataItems property is the same as described in Table 4-7. From within a pageLoading event handler, you can’t cancel the page update. Immediately after the pageLoading event, the page request manager starts its rendering engine and updates all involved panels.

The pageLoaded event is raised after all content on the page is refreshed. You can use this event to provide a custom transition effect for updated content, such as flashing or highlighting updated contents. The event data is packed in the class PageLoadedEventArgs, which has three properties: panelsUpdated, panelsDeleted, and dataItems. The first two are arrays and list the updatable panels that were just updated and deleted, respectively. The dataItems property is the same as described in Table 4-7.

You can use this event as well as endRequest to provide notification to users or to log errors.

Passing Data Items during Partial Updates

The UpdatePanel control allows you to wrap groups of controls that need to be updated over a postback. There might be pages, though, in which grouping all controls involved in an operation inside a single panel is too challenging or impractical. What if, therefore, you need to update controls outside the UpdatePanel that fired the call? If the control lives inside another UpdatePanel, you can programmatically order a refresh of the whole panel. What if, instead, just one control needs update? And what if the update on the client must be done with data generated during the asynchronous postback? The RegisterDataItem method of the ScriptManager control addresses exactly this issue.

The RegisterDataItem method specifies a server-generated string that will be added to the response of the asynchronous postback along with the updated markup of the panel or panels. This string is then passed to the client infrastructure of ASP.NET AJAX through the dataItems property of the event data for pageLoading, pageLoaded, and endRequest events.

The RegisterDataItem is used when the page, or a server control, needs to pass additional data to the client that requires explicit processing on the client that is beyond the capabilities of updatable panels. Let’s arrange an example that illustrates the usefulness of data items.

Motivation for Using Data Items

Imagine a page that incorporates a kind of clock. It is made by a timer control that updates a label every second. The panel also contains a couple of buttons to increase and decrease the clock interval.


As you can see, the timer is not part of the updatable panel. As such, no markup for the Timer control will be sent over during an AJAX postback. In light of this, what about the following code?

protected void Button1_Click(object sender, EventArgs e) { Timer1.Interval += 1000; }

This code is certainly executed during the postback. There’s no visible mechanism, though, that ensures that the new interval is passed to the client. Without this key information, how can the client timer update its interval and tick the server properly? However, such code works just fine. You click the button and the clock is updated every two seconds. What’s up with that?

The Timer control internally registers a data item and, through it, passes its server state to the client also over an AJAX postback. Here’s a brief excerpt from the source code of the Timer control:

protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); if (ScriptManager.IsInAsyncPostBack) { ScriptManager.RegisterDataItem(this, GetJsonState(), true); } ... } protected string GetJsonState() { ... }

The internal GetJsonState function returns a JSON string that renders out as a key/value dictionary object. In particular, the Timer control saves the value of the Enabled and Interval properties-the only two properties that can affect the behavior of the timer on the client.

Preparing Data Items

Let’s see how to proceed to make an ASP.NET AJAX page pass a message for a client label that can’t be included directly in an updatable panel. The data to pass is related to events that occur on the server. For example, imagine you want to display the current interval of the timer and the increment/decrement it underwent on the server. Here’s the code-behind class for page DataItems.aspx:

public partial class Samples_Ch04_Advanced_DataItems : System.Web.UI.Page { private const int OneSecond = 1000; private int oldInterval; private static bool isDirty = false; protected void Page_Load(object sender, EventArgs e) { oldInterval = Timer1.Interval; } protected void Timer1_Tick(object sender, EventArgs e) { Label1.Text = DateTime.Now.ToLongTimeString(); } protected void Button1_Click(object sender, EventArgs e) { Timer1.Interval += OneSecond; isDirty = true; } protected void Button2_Click(object sender, EventArgs e) { if (Timer1.Interval > OneSecond) { Timer1.Interval -= OneSecond; isDirty = true; } } private string GetJsonState() { return ("[" + Timer1.Interval.ToString() + "," + (Timer1.Interval - oldInterval).ToString() + "]"); } private void Page_PreRender(object sender, EventArgs e) { if (isDirty && ScriptManager1.IsInAsyncPostBack) { isDirty = false; ScriptManager1.RegisterDataItem(this, GetJsonState(), true); } } }

The AJAX-sensitive state of the page is tracked down and, if dirty, saved as a JSON string during the PreRender event through a call to RegisterDataItem. The state to send to the client is a commaseparated JSON value string that includes the Interval of the timer as modified by Increase and Decrease buttons in the page. RegisterDataItem takes up to three parameters: the object for which the data is registered (page or control), the string to pass, and true if the string is a JSON string.

  Note 

You get an exception if you call RegisterDataItem in a non-AJAX postback. Tracking the dirtiness of the object’s state is not mandatory but helps in two ways: it reduces the bandwidth and makes it easier for you to manage data items on the client. In fact, the client will receive data only if there’s some data to consume and some user interface to update.

Data Items as JSON Strings

You use the RegisterDataItem method to send data from the server to the client during asynchronous postbacks, regardless of whether the control receiving the data is inside an UpdatePanel control. The data you send is a string and is associated with a particular server control or the page itself. The internal format of the string is up to you. It can be a single value string or a string that represents multiple values-for example, a comma-separated list of values. Needless to say, if you opt for a custom format inside the string, any deserialization is up to you and must be accomplished in the event handler where you get to process sent data items.

Data items are available with the pageLoading event and the following pageLoaded and endRequest events. All event data structures for these events, in fact, feature a dataItems property. The dataItems property is a key/value dictionary where the key is the ID of the control that references the data or __Page if the referrer is the ASP.NET page. The value is the string you passed to RegisterDataItem on the server for that control or page.

When you need to pass multiple values to the client, you can use a JSON-serialized array of values. In JavaScript, an array can be expressed as a comma-separated list of values wrapped by square brackets, as shown in the following code:

var x = [1,2,3];

Returned as a string, the expression must be pre-processed by the eval function to become a valid array:

var x = eval("[1,2,3]");

This is exactly what happens in the code-behind class described earlier, where the GetJsonState method returns an array of strings:

private string GetJsonState() { return ("[" + Timer1.Interval.ToString() + "," + (Timer1.Interval-oldInterval).ToString() + "]"); }

On the client, the dataItems object for the page points to an array with interval and last change information.

  Note 

When data items are expressed in a format that requires a call to eval for them to be transformed into usable client objects, you must turn on the JSON flag on the RegisterDataItem method. When the data item is JSON-enabled, the eval JavaScript function is called to evaluate the returned string. You should only use JSON serialization if you’re passing multiple data values to the client. To learn more about JSON, visit http://json.org/.

Retrieving Data Items on the Client

The dictionary with data items is sent to the client and, as mentioned earlier, is available in the pageLoading, pageLoaded, and endRequest events through the dataItems property on the event argument data object. For a sample page that contains a timer and is bound to the codebehind class considered earlier, the following is an excerpt from the postback response:

11|dataItemJson|__Page|[3000,1000]| 11|dataItemJson|Timer1|[true,3000]|

This text is appended to the response along with the updated markup and new values for hidden fields. The text should be read as follows. It contains two JSON-serialized data items, which is to say they both require eval to evaluate the returned 11 bytes of data each. The first data item is bound to the page and contains an array of two values: 3000 and 1000. The second data item refers to the Timer1 control in the page and contains an array of two values: true and 3000. Let’s see how to retrieve and consume this information programmatically:

function pageLoad() { var manager = Sys.WebForms.PageRequestManager.getInstance(); manager.add_endRequest(OnEndRequest); } function OnEndRequest(sender, args) { var dataItem = args.get_dataItems()["__Page"]; if (dataItem) { var text = String.format("Interval set to {0}ms; last change {1}ms.", dataItem[0], dataItem[1]); $get("Msg").innerHTML = text; } }

The endRequest handler retrieves the data item record for the page (or any control you’re interested in) and then uses the returned object as an array. In particular, it refreshes the innermost HTML of a label in the page. (See Figure 4-14.)

Figure 4-14: Refreshing some text in the page that is outside any UpdatePanel controls

The __Page ID used in the preceding example indicates that data items are related to the page. The string is emitted as a generic ID for the page and depends on the object you specify as the first argument of RegisterDataItem. You can also associate data items to a specific control in the page; in that case, the ID to use in the endRequest client-side event handler is the ID of the referenced server control.

Animating Panels during Partial Updates

Most users of the first ASP.NET AJAX applications reported that for them it was a problem not being able to find a visual clue of the changes in the various portions of the page. An UpdatePanel control allows you to partially refresh a page; the operation, though, might pass unnoticed to users, especially when it turns out to be particularly quick or when it serves only a slightly different markup. Animating panels to call a user’s attention to the changes has therefore become the next challenge for ASP.NET AJAX developers.

Visual Feedback during the Partial Update

A primary and quite effective form of animation consists of wiring up the beginRequest and endRequest events of the page request manager and changing the style of controls in the page for the time it takes to complete the server operation. In Figure 4-13 (shown earlier), you see a button that is disabled during the partial update. At the same time, the grid that will receive the new data is framed. Of course, you can choose any combination of style settings to reflect your idea of visual feedback.

For example, in the beginRequest event handler you can disable all controls involved with a postback, change their background color, modify borders, collapse tables, and so on. In the endRequest or pageLoaded handler, you then restore the original settings. As a result, the style of the user interface is altered for the duration of the server request and then restored when the new markup is available.

Visual Feedback after the Page Refresh

Another option is to call the user’s attention only when the modified page is up and running. In this case, nothing happens while the request is processed on the server; as soon as the pageLoaded event is fired, though, the animation starts to notify the user of changes. As a first step, let’s arrange a JavaScript class that implements the animation:

Type.registerNamespace('IntroAjax'); IntroAjax.BorderAnimation = function IntroAjax$BorderAnimation( color, thickness, duration) { this._color = color; this._thickness = thickness; this._duration = duration; } // Method to start the animation on the specified panel function IntroAjax$BorderAnimation$animatePanel(panelElement) { if (arguments.length !== 1) throw Error.parameterCount(); var style = panelElement.style; style.borderWidth = this._thickness; style.borderColor = this._color; style.borderStyle = 'solid'; window.setTimeout( function() , this._duration); } IntroAjax.BorderAnimation.prototype = { animatePanel: IntroAjax$BorderAnimation$animatePanel } IntroAjax.BorderAnimation.registerClass('IntroAjax.BorderAnimation');

The BorderAnimation class features an animatePanel method that renders out a solid border of the specified thickness, color, and duration around all the panels updated during the postback.

For the script to be executed, you must first register the .js file with script manager:

<asp:ScriptReference Path="animation.js" />

Then you need to add some script to the page to trigger it:

As the page loads up, you register handlers for the beginRequest and pageLoaded events. The beginRequest handler doesn’t do anything related to the user interface; it simply tracks the DOM element that caused the asynchronous postback. The handler for the pageLoaded event creates an instance of the animation class and uses it to animate all panels that have been updated. It does that only if the ID of the postback element matches the ID stored in the postbackElement element variable, indicating it is the same element that triggered the postback (in this case “button1”). In this way, you can animate based on the specific action commanded. (See Figure 4-15.)

Figure 4-15: Updated panels are framed for a few seconds to get the user’s attention

  Note 

The definitive solution for animating updatable panels is not in the ASP.NET AJAX Extensions platform but just outside of it, in the AJAX Control Toolkit. (See http://ajax.asp.net/ ajaxtoolkit.)

As we’ll see in Chapter 5, “The AJAX Control Toolkit”, the AJAX Control Toolkit is a joint project between the developer community and Microsoft. It extends the ASP.NET AJAX Extensions platform with a collection of Web client components, including controls and control extenders. One of coolest extenders is the UpdatePanelAnimation component. Attached to an UpdatePanel, the component automatically injects client script to animate the panel during the postback operation.

Conclusion

The UpdatePanel control and other similar server controls provide an excellent compromise between the need to implement asynchronous and out-of-band functionality and the desire to use the same familiar ASP.NET application model. As you’ve seen in this chapter, any ASP.NET page can be easily transformed into an ASP.NET AJAX page. You divide the original page into regions and assign each markup region to a distinct UpdatePanel control. From that point on, each updatable region can be refreshed individually through independent and asynchronous calls that do not affect the rest of the page. The current page remains up and active while regions are updated. This feature is known as partial rendering.

If required, you can define one or more triggers for each updatable region so that the update of each region occurs under specified and well-known conditions. The UpdatePanel and other server controls covered in this chapter represent the first, and certainly the easiest, path to using ASP.NET AJAX. With minimal effort, you can transform an ordinary ASP.NET page into an AJAX page and start practicing with the new framework until you feel the need to use braveheart JavaScript code.

In the next chapter, we’ll look at another key category of ASP.NET AJAX server controls-the extenders. A control extender is a server-side component that adds a new behavior and extra capabilities to a bunch of existing ASP.NET controls.

Chapter 5 The AJAX Control Toolkit

Категории