Managing the User Experience in AJAX
Dino Esposito
Code download available at: CuttingEdge2007_11.exe (183 KB)
Browse the Code Online
We are all used to the stop-and-go nature of the Web. For every request you make by clicking a button or a link, the browser places a request and waits for the response. You work a bit with that page, then click and wait again.
As I've mentioned in recent columns, I am very excited about the possibilities offered by the AJAX paradigm and its related set of technologies. Developers and architects can build a new generation of Web sites in which the stop-and-go pattern is replaced by a more interactive structure. I always enjoy seeing the sincere enthusiasm users have when they first experience the continuous feel of an AJAX application.
In recent columns I have extolled the power of AJAX, listing its many direct and indirect benefits. This month, I want to focus on some issues directly related to implementing the AJAX paradigm by using ASP.NET AJAX Extensions.
Initially built on top of ASP.NET 2.0, ASP.NET AJAX Extensions has been fully integrated into the ASP.NET platform with the release of ASP.NET 3.5. In fact, ASP.NET AJAX sports two programming models for you to choose from—partial rendering and script services. For a deeper discussion of the two, you might want to check out my columns from the September 2007 and October 2007 issues of MSDN® Magazine.
An AJAX application has the power to place Web requests directly from script code, bypassing the browser. When you use the browser to request a page—for example, through a standard Submit button or hyperlink—just one request is processed at a time for each active browser window. Requesting a page via script doesn't have this limitation and multiple requests can take place concurrently. In the July 2007 installment of Cutting Edge, I took advantage of this feature to implement a live progress bar that reports to the client the real progress made by a server-side running task. The possibility of placing concurrent calls from a single browser window opens up a whole new world of possibilities for addressing various issues—including many related to the UI.
In my column this month, I'll review problems, solutions, and necessary tools to implement an effective user interface for Web applications based on ASP.NET AJAX. In particular, I'll focus on applications that use the partial rendering model.
AJAX and the User Interface
A Web page remains active even after the user has placed an AJAX request. This means that the user can click on active elements and start new operations that could interfere with the current request. As with desktop applications, therefore, developers sometimes need to consider temporarily disabling portions of the UI.
How can you disable client page elements for the duration of a server operation? There are a few ways to do this. You can retrieve the ID of the corresponding Document Object Model (DOM) element and enable or disable its state—this is a client-side operation that you accomplish via script using the DOM or perhaps CSS styles. In addition, ASP.NET controls feature the Visible property. When set to false, the Visible property prevents the server control from adding its own markup to the client response. As a result, the expected markup for that control doesn't show up in the client page.
To disable and enable a portion of the user interface in ASP.NET AJAX, you first need to identify the IDs of all the HTML elements that form that portion, as shown in the following code:
// Get the reference to the DOM element var button1 = $get("Button1"); // Disable the status of the element button1.disabled = true;
To disable elements, you retrieve the ID and set the disabled property to the appropriate value. Ideally, you should turn off the specific portion of the user interface immediately before placing the request and re-enable that portion immediately after the request has been completed. As for the implementation, this will vary depending on the programming model you have chosen.
If you are using partial rendering, use the eventing model of the PageRequestManager object—this class is the client-side brains behind partial rendering. The full class name is Sys.WebForms.PageRequestManager and its source code is defined in MicrosoftAjaxWebForms.js. PageRequestManager contains all the logic required to set up and control AJAX postbacks. The class is a singleton—which means that it guarantees that exactly one request is pending at a time. You obtain the reference to the unique instance of the page request manager through the following code:
var manager = Sys.WebForms.PageRequestManager.getInstance();
PageRequestManager exposes a few client-side events that let you intercept key steps in the processing of an AJAX postback, as detailed in Figure 1. The occurrence of the BeginRequest event is usually a good time to disable UI elements, and the EndRequest event is the right time to re-enable HTML elements.
Figure 1 Events Exposed by the PageRequestManager
Event | Argument | Description |
---|---|---|
initializeRequest | InitializeRequestEventArgs | Fired immediately after detecting that a new AJAX request has been issued. |
beginRequest | BeginRequestEventArgs | Fired just before the request is sent out. |
pageLoading | PageLoadingEventArgs | Fired once the response has been fully downloaded and processed, but before any content on the page is updated. |
pageLoaded | PageLoadedEventArgs | Fired once all contents on the page are refreshed. |
endRequest | EndRequestEventArgs | Fired once all work is done. |
If you opted for a script services approach, then you probably have a JavaScript-powered front end that talks to a server-side façade made of ASP.NET AJAX script Web services. Each call to services is conducted via an explicit asynchronous script call. The developer is in complete control over the start of the remote call and can safely disable any pieces of the UI. Because each call is going to be asynchronous, a callback can be specified for both success and failure of the call. From the callback, then, you can reset the UI back to proper status.
All that said, there are some issues relative to disabling and re-enabling UI elements. I'll delve into them with some scenarios.
Disabling UI via Element IDs
Figure 2 shows the code used to disable some elements of a sample AJAX page (using partial rendering) during postbacks. When the page first loads, handlers for BeginRequest and EndRequest events are registered. The pageLoad function is a conventionally named handler for the application load event fired by the AJAX client library. The pageLoad function receives two arguments—one is of type Sys.Application and the other one is an ApplicationLoadEventArgs object.
Figure 2 Page that Disables UI during Postbacks
<html> <body> <script type="text/javascript"> function pageLoad() { var manager = Sys.WebForms.PageRequestManager.getInstance(); manager.add_beginRequest(OnBeginRequest); manager.add_endRequest(OnEndRequest); } var lcPostbackElementID; functionw OnBeginRequest(sender, args) { lcPostbackElementID = args.get_postBackElement().id.toLowerCase(); if (lcPostbackElementID === "button1") { $get("Button1").disabled = true; } } function OnEndRequest(sender, args) { if (lcPostbackElementID === "button1") { $get("Button1").disabled = false; } } </script> <form id="form1" runat="server"> <asp:ScriptManager ID="ScriptManager1" runat="server" /> <div id="pageContent"> <b>Enter some data:</b><br /> <asp:TextBox ID="TextBox1" runat="server" /> <asp:Button ID="Button1" runat="server" Text="Post ..." onclick="Button1_Click" /> <br /><br /> <asp:UpdateProgress runat="server" ID="UpdateProgress1"> <ProgressTemplate> <img src="images/loading.gif" /> </ProgressTemplate> </asp:UpdateProgress> <asp:UpdatePanel ID="UpdatePanel2" runat="server" UpdateMode="Conditional"> <ContentTemplate> <asp:Label runat="server" ID="Label1" /> </ContentTemplate> <Triggers> <asp:AsyncPostBackTrigger ControlID="Button1" /> </Triggers> </asp:UpdatePanel> </div> </form> </body> </html>
BeginRequest (and all other events listed in Figure 1) occurs for each AJAX postback raised from within the page. Such events merely notify developers that a request is being placed or has just completed. These events are not like, for instance, the click event of a client or server button. You know that a postback is going to happen, but you don't immediately know why or what UI element is responsible for it. But in order to disable the right portion of the user interface, you need to know exactly which element was clicked.
The data structure that accompanies the BeginRequest event contains a reference to the DOM element that caused the postback. The member is named postBackElement and is accessed like so:
var lcPostbackElement = args.get_postBackElement();
You can then use this information to check the element the user clicked and decide which elements need to be disabled:
if (lcPostbackElement.id.toLowerCase() === "button1") { $get("Button1").disabled = true; ... }
Note that the same information is not associated with the EndRequest event when you restore the original UI. Therefore, you need to cache the reference, or just the ID of the element, to check it again from within the EndRequest event handler.
To disable and enable a DOM element, you need to retrieve a DOM reference to the element and then act on the Boolean disabled property. In ASP.NET AJAX, the $get function is equivalent to the DOM document.getElementById call. More precisely, $get is equivalent to a parameterized call of getElementById that also accepts an optional reference to indicate the root of the DOM subtree to be searched.
It is very simple to temporarily disable UI elements in a script service call. The only thing that changes is the location from which you use the $get function. You disable before making the call and re-enable in the success and failure callbacks, as shown in Figure 3.
Figure 3 Disabling UI Elements in a Script Service Call
function startCall() { $get("Button1").disabled = true; ... Samples.RemoteService.GetTime(param1, param2, onCompleted, onFailed); } function onCompleted(results) { // Process response ... $get("Button1").disabled = false; ... } function onFailed(error) { $get("Button1").disabled = false; ... }
Disabling UI Elements in Naming Containers
The code discussed so far works just fine, but it is far from perfect. What if you need to disable UI elements embedded in a Master Page or a user control?
Many ASP.NET server controls use a naming container to create a unique namespace for the ID property values of their child controls. This is especially important in templated and iterative data-bound controls, such as GridView and Repeater. For each control, the naming container is the nearest parent control that implements the INamingContainer interface. For example, the UserControl class acts as a naming container for all of its contained controls. The same happens to any control on a page that is associated with a Master Page.
For any ASP.NET control under the jurisdiction of a naming container, its client-side ID is changed to be prefixed with the ID of the parent. Consider the following code snippet:
<form runat="server"> <asp:Button runat="server" ID="Button1" /> ... </form>
The client ID of the button matches the server ID and is always Button1. However, this programming aspect changes suddenly if you move the button in a user control or derive its content page from a Master Page. Consider the following user control:
<%@ Control Language="C#" CodeFile="test.ascx.cs" Inherits="test" %> <asp:Button runat="server" ID="Button2" Text="Click" />
After embedding the user control in an ASP.NET page, the emitted HTML refers to Button2:
<input type="submit" name="UserControl1$Button2" value="Click" id="UserControl1_Button2" />
The ID of each contained control is now prefixed with the ID of the parent. This behavior is essentially transparent to the developer as long as you limit yourself to server-side programming. If, however, you need to script Button2 from the client, you need to know about naming containers and how they change the client ID of HTML elements in the page DOM.
In Figure 4, you see the same page as that shown in Figure 2, but embedded as a content page in a Master Page. The real client ID of Button1 is now:
Figure 4 Content Page that Disables UI during Postbacks
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="DisableUIEmb.aspx.cs" MasterPageFile="~/Sample.master" Title="Disable UI Elements (Embedded)" Inherits="Samples_DisableUIEmb" Theme="ProgAjax1" %> <asp:Content runat="server" ContentPlaceHolderID="ScriptPlaceHolder"> <script type="text/javascript"> function pageLoad() { var manager = Sys.WebForms.PageRequestManager.getInstance(); manager.add_beginRequest(OnBeginRequest); manager.add_endRequest(OnEndRequest); } var lcPostbackElementID; function OnBeginRequest(sender, args) { lcPostbackElementID = args.get_postBackElement().id.toLowerCase(); if (lcPostbackElementID === "<%= Button1.ClientID.ToLower() %>") { $get("<%= Button1.ClientID %>").disabled = true; } } function OnEndRequest(sender, args) { if (lcPostbackElementID === "<%= Button1.ClientID.ToLower() %>") { $get("<%= Button1.ClientID %>").disabled = false; } } </script> </asp:Content> <asp:Content runat="server" ContentPlaceHolderID="ContentPlaceHolder1"> <div id="pageContent"> <b>Enter some data:</b><br /> <asp:TextBox ID="TextBox1" runat="server" /> <asp:Button ID="Button1" runat="server" Text="Post ..." onclick="Button1_Click" /> <br /><br /> <asp:UpdateProgress runat="server" ID="UpdateProgress1"> <ProgressTemplate> <img src="images/loading.gif" /> </ProgressTemplate> </asp:UpdateProgress> <asp:UpdatePanel ID="UpdatePanel2" runat="server" UpdateMode="Conditional"> <ContentTemplate> <asp:Label runat="server" ID="Label1" /> </ContentTemplate> <Triggers> <asp:AsyncPostBackTrigger ControlID="Button1" /> </Triggers> </asp:UpdatePanel> </div> </asp:Content>
ctl00_ContentPlaceHolder1_Button1
To properly disable Button1 (or, in general, to script it), you must know this new ID. The exact ID is known only at run time, even though the name can be figured out and hardcoded in an external JavaScript file.Figure 4 demonstrates how you can use ASP-style code blocks and the server-side ClientID property to emit in the browser parametric script code that works around naming containers. Figure 5 shows the sample page in action.
Figure 5 Content Page Using Script to Adjust UI (Click the image for a larger view)
Using the UpdatePanelAnimation Extender
The portion of the UI being disabled is postback-specific and you usually define that using plain script. In most cases, all you need to do is gray out elements; however, you could do even more. For example, you could fade out and then fade back in portions of the interface.
Implementing this kind of advanced user interaction is entirely your responsibility if you are using the script services approach. If, however, you use the partial rendering approach and install the AJAX Control Toolkit, you can rely on an extremely cool extender control: the UpdatePanelAnimation extender.
An extender control is an ASP.NET server control that provides additional behavior to a variety of existing server controls. When you need a special behavior from a control, you code the behavior in an extender and then bind together the extender and the original control in the same page. Suppose you want to filter out non-digit characters in a textbox. You can develop an ad hoc TextBox control or you can just use the plain old TextBox control extended with the proper behavior. The advantage is that the same behavior can be reused for multiple types of controls and, further, that multiple behaviors can be freely combined to enrich a particular instance of a server control.
The UpdatePanelAnimation extender was designed to add a bit of animation to any updatable regions in an ASP.NET AJAX partial rendering page. You declaratively associate the animation you want the page to use while the postback proceeds. The extender works in conjunction with the script-based animation framework available through the AJAX Control Toolkit. Figure 6 shows sample code you can use to animate an updatable region.
Figure 6 Using the UpdatePanelAnimation Extender
<asp:UpdatePanel ID="UpdatePanel1" runat="server" UpdateMode="Conditional"> <ContentTemplate> <asp:Button ID="Button1" runat="server" Text="Load" ... /> <div id="Panel1"> ... </div> </ContentTemplate> <Triggers> ... </Triggers> </asp:UpdatePanel> <act:UpdatePanelAnimationExtender ID="UpdatePanelAnimation1" runat="server" TargetControlID="UpdatePanel1"> <Animations> <OnUpdating> <Sequence> <EnableAction AnimationTarget="Button1" Enabled="false" /> <FadeOut AnimationTarget="Panel1" minimumOpacity=".3" /> </Sequence> </OnUpdating> <OnUpdated> <Sequence> <FadeIn AnimationTarget="Panel1" minimumOpacity=".3" /> <EnableAction AnimationTarget="Button1" Enabled="true" /> </Sequence> </OnUpdated> </Animations> </act:UpdatePanelAnimationExtender>
You first bind the extender to a particular UpdatePanel control in the page and then configure the animation. The code in Figure 6 sets up a sequence of two actions before the postback occurs (the OnUpdating tag) and another sequence of two actions when the postback has completed (the OnUpdated tag). EnableAction, FadeOut, and FadeIn are all predefined animations supported by the AJAX Control Toolkit. FadeOut and FadeIn use CSS styles to fade out and in the DOM subtree they apply to—Panel1 in this example. The EnableAction animation simply disables or enables the specified target DOM element.
Compared to direct scripting, the UpdatePanel animation extender supports declarative programming and allows you to do more than just graying out elements. The extender, though, is limited to pages that use partial rendering. For more information, take a look at the AJAX Control Toolkit home page atcodeplex.com/AtlasControlToolkit.
Disabling the Whole Page
As I mentioned, the portion of the UI to be disabled depends on the control the user clicked and, more generally, the required action. In a partial rendering context, though, only one request can be executed at a time. Identifying and disabling only the portion of the interface affected by the pending operation would be too much work and, more importantly, it wouldn't guard against undesired clicking. I'll discuss this in more detail in a moment. For now, suffice it to say that disabling the whole page without the burden of figuring out which controls should be grayed out is a simpler and more effective solution.
The idea is to create a DIV tag as large as the client area of the browser and use it to cover everything in the page. In this way, any clicking will be captured by this DIV and won't reach the underlying HTML elements. You can even associate the DIV with some styles to gray out the page while the operation is processing. Figure 7 shows the JavaScript code used to accomplish this task.
Figure 7 Covering the UI with a DIV
<script type="text/javascript"> var _backgroundElement = document.createElement("div"); function pageLoad() { var manager = Sys.WebForms.PageRequestManager.getInstance(); manager.add_beginRequest(OnBeginRequest); manager.add_endRequest(OnEndRequest); // pageContent is the parent of the new DIV $get("pageContent").appendChild(_backgroundElement); } function OnBeginRequest(sender, args) { EnableUI(false); } function OnEndRequest(sender, args) { EnableUI(true); } function EnableUI(state) { if (!state) { _backgroundElement.style.display = ''; _backgroundElement.style.position = 'fixed'; _backgroundElement.style.left = '0px'; _backgroundElement.style.top = '0px'; var clientBounds = this._getClientBounds(); var clientWidth = clientBounds.width; var clientHeight = clientBounds.height; _backgroundElement.style.width = Math.max(Math.max(document.documentElement.scrollWidth, document.body.scrollWidth), clientWidth)+'px'; _backgroundElement.style.height = Math.max(Math.max(document.documentElement.scrollHeight, document.body.scrollHeight), clientHeight)+'px'; _backgroundElement.style.zIndex = 10000; _backgroundElement.className = "modalBackground"; } else { _backgroundElement.style.display = 'none'; } } function _getClientBounds() { var clientWidth; var clientHeight; switch(Sys.Browser.agent) { case Sys.Browser.InternetExplorer: clientWidth = document.documentElement.clientWidth; clientHeight = document.documentElement.clientHeight; break; case Sys.Browser.Safari: clientWidth = window.innerWidth; clientHeight = window.innerHeight; break; case Sys.Browser.Opera: clientWidth = Math.min(window.innerWidth, document.body.clientWidth); clientHeight = Math.min(window.innerHeight, document.body.clientHeight); break; default: // Sys.Browser.Firefox, etc. clientWidth = Math.min(window.innerWidth, document.documentElement.clientWidth); clientHeight = Math.min(window.innerHeight, document.documentElement.clientHeight); break; } return new Sys.UI.Bounds(0, 0, clientWidth, clientHeight); } </script>
A global JavaScript variable is declared to represent the DIV tag. Once initialized, the DIV is added to the DOM tree at the specified position. In this case, it is defined as the child of the DIV tag that wraps the whole visible interface. The BeginRequest handler configures and displays the DIV, thus disabling the visible interface. The EndRequest handler just hides the DIV, thus restoring the UI:
function OnBeginRequest(sender, args) { EnableUI(false); } function OnEndRequest(sender, args) { EnableUI(true); }
The DIV tag is fixed at 0,0 position and its width and height are dynamically calculated in a browser-specific manner. The size of the DIV reflects the current size of the browser's client area. The getClientBounds function shows an example of script programming that works around the differences in the object model of various browsers.
As you can see, the code uses some classes built in the Microsoft® AJAX client library. I borrowed the code snippet from the source code of the ModalPopup extender—one of the coolest extenders in the AJAX Control Toolkit. In particular, the ModalPopup extender allows a page to display content to the user with modality—that is, preventing the user from interacting with the rest of the page. The net effect is the same as window.alert, but the interface—the hierarchy of controls being displayed—is entirely up to you.
Figure 8 shows a page that uses a modal DIV to disable the entire UI during a partial rendering operation.
Figure 8 Disabling the Entire Page during Partial Rendering (Click the image for a larger view)
One AJAX Postback at a Time
Since partial rendering lets you add AJAX functionality to classic ASP.NET 2.0 pages without modifying the programming model, the server lifecycle of the page is fully preserved. This holds true for postback events, loading, rendering, and view state. The amount of traffic for a typical partial rendering call is smaller than that for a classic ASP.NET call, but the size of the view state is a constant and can only be reduced through page-specific programming techniques.
With partial rendering, an ASP.NET page gains the continuous feel of an AJAX page, but maintains the traditional user interface. Seen from the client-side, a partial rendering page behaves as if it is part of a single, sequential UI. Only one operation can be pending at a time, even when two concurrent operations are possible from a business viewpoint.
Imagine you have a page that displays any sort of information about a customer. This page contains two buttons—one for downloading order information and one for displaying customer details. From a pure business perspective, there's nothing preventing the user from clicking the two buttons in sequence to generate two successive requests, with the second request starting before the first has completed. The two requests need the same input—the customer ID—and both are read-only, meaning there is no danger of interfering with the status of the back-end database. Nevertheless, these two calls can't run concurrently in a partial-rendering page because partial rendering is based on the classic ASP.NET programming model—each request returns markup that includes an updated view state for the whole page. If you let two requests run concurrently, you put the consistency of the page's view state at risk. Two requests sent in quick sequence send the same view state to the server, but each request returns a different view state, updated for the controls involved with the processing of the request. This means the last request to return overwrites the changes made when the first request returned. Thus, partial-rendering AJAX pages still work according to the stop-and-go pattern.
To avoid these types of problems, the page request manager object automatically kills any request that is pending when a new request is placed. In light of this behavior, it is extremely important that you disable the entire UI in partial rendering pages. This is to ensure that users can't interact with the whole page for the duration of the current request. As you can see, this statement directly contradicts one of the key benefits of AJAX—asynchronous interaction.
High-Priority Calls
Certain questions come up when examining partial rendering, such as if you can't run two requests concurrently due to the need to preserve the consistency of the view state, then why hasn't the team implemented an algorithm to merge two partial view states on the client? This seems like a reasonable question. However, it is not an option due at least in part to the view state's internal structure, which includes encoded content, a hash value, and a server-only security value.
Combining two view states on the client is technically possible, but it would require significant changes to the current structure of the view state and the ASP.NET runtime. Moreover, it would raise security issues by making script code available to virtually everybody, allowing them to prepare ad hoc view state content for replay and cross-site scripting attacks.
Another common question has to do with the last-win policy that the page request manager implements when it kills the pending request whenever a new request is received. I find that developers tend to feel constrained when someone else makes a decision about a programming point. So it seems that many developers—myself, included—don't like the imposed last-win policy for AJAX postbacks. What about a first-win policy where basically the current request is given precedence and successive requests are canceled or queued until the first request finishes?
The ASP.NET AJAX framework provides the tools needed to implement this pattern, as you'll see in a moment. But are you sure you really need this option? The fact is that if you use partial rendering, you should be ready to deliver your pages one request at a time. The easiest and safest way to accomplish this is to disable the whole UI to prevent users from clicking.
If, for some reason, you don't think you can disable the entire user interface, then knowing how to implement a first-win approach might deliver the flexibility you need. The idea is to write some script code that intercepts the beginning of each partial rendering request—the initializeRequest event of the page request manager. In the handler, you check run-time conditions to determine whether you really want to cancel the request and then instruct the manager accordingly:
function OnInitializeRequest(sender, args) { var manager = sender; if (manager.get_isInAsyncPostBack() && args.get_postBackElement().id === "Button1") { args.set_cancel(true); } }
The sample code checks whether another request is being served and whether this is a request with a lower priority. If so, you can cancel the request by setting the cancel property to true on the event data structure. According to the previous code, the latest request is gone. However, you might write code to implement an internal queue for pending requests. For more information on high-priority calls, check outajax.asp.net/docs/tutorials/ExclusiveAsyncPostback.aspx.
No comments:
Post a Comment