Managing Page Requests

Managing Page Requests

Every .aspx page that uses ASP.NET AJAX must contain a ScriptManager control that manages scripts for rendering to the client. And it oversees the processing of partial page updates to get the various UpdatePanel controls refreshed appropriately. The ScriptManager and UpdatePanel have a counterpart in JavaScript to manage this behavior. The PageRequestManager is the central point of interaction for the client side.

There is a single instance of the PageRequestManager for each page, which can be retrieved through the getInstance static function. Listing 2-8 (ClientLifecycle.aspx) demonstrates working with it. The PageRequestManager is retrieved and then used to add handlers for the pageLoaded and initializeRequest events. The event handlers in this example just use the alert function to show that they have been invoked.

Listing 2-8
Image from book
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
 "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>PageRequestManager</title>
<script language="C#" runat="server">
    protected override void OnLoad(EventArgs e) {
        theTime.Text = DateTime.Now.ToLongTimeString();
    }
</script>    

</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server" />
<script type="text/javascript">
var prm = Sys.WebForms.PageRequestManager.getInstance();
prm.add_pageLoaded(pageLoaded);
prm.add_initializeRequest(initializeRequest);

function initializeRequest(sender, eventArgs) {
    alert("initializeRequest is only called for asynchronous requests");
}

function pageLoaded(sender, eventArgs) {
    alert("pageLoaded is called for synchronous and asynchronous requests");
}
</script>
<div style="border-style:solid">
<asp:UpdatePanel runat="server">
<ContentTemplate>
<br />&nbsp;<asp:Label runat="server" ID="theTime"></asp:Label><br />
<asp:Button runat="server" Text="Partial Update" /><br /><br />
</ContentTemplate>
</asp:UpdatePanel>
</div><br />

<asp:Button runat="server" Text="Regular PostBack" />
</form>
</body>
</html>
Image from book

The Request Lifecycle

When the page is first retrieved, the pageLoaded event is fired. As the message in Figure 2-7 shows, the pageLoaded event occurs for all requests, synchronous and asynchronous. When the request for the partial page request is started by clicking the button in the UpdatePanel, the initializeRequest alert will be shown. This event does not occur for the full postback triggered by the button outside the UpdatePanel. It is part of the asynchronous lifecycle implemented by the PageRequestManager.

Image from book
Figure 2-7

The lifecycle provided by the PageRequestManager for asynchronous requests includes five events:

  1. initializeRequest

  2. beginRequest

  3. pageLoading

  4. pageLoaded

  5. endRequest

The potentially confusing thing about the series of events is that they don’t fire all of the time. Requests can be canceled or preempted by another request. Listing 2-9 (LifecycleCounts.aspx) sets up counters that are incremented for each time the event handlers are invoked. It also increments a counter on the server for every time the page executes. The count is displayed inside an UpdatePanel. The page sleeps briefly so that the behavior of initiating a request while another is pending can be seen. There is also a button for displaying the current values.

Listing 2-9
Image from book
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" 
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>PageRequestManager</title>
<script language="C#" runat="server">
    protected override void OnLoad(EventArgs e) {
        if (Session["count"] == null) {
            Session["count"] = 1;
        }
        else {
            Session["count"] = ((int)Session["count"]) + 1;
        }
        System.Threading.Thread.Sleep(1000);
        theCount.Text = "count = " + Session["count"].ToString();
    }
</script>    

</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server" />
<script type="text/javascript">
var initializeCount = 0;
var beginCount = 0;
var loadingCount = 0;
var loadedCount = 0;
var endCount = 0;

var prm = Sys.WebForms.PageRequestManager.getInstance();
prm.add_initializeRequest(initializeRequest);
prm.add_beginRequest(beginRequest);
prm.add_pageLoading(pageLoading);
prm.add_pageLoaded(pageLoaded);
prm.add_endRequest(endRequest);

function initializeRequest(sender, initializeRequestEventArgs) {
    initializeCount = initializeCount + 1;
}

function beginRequest(sender, beginRequestEventArgs) {
    beginCount = beginCount + 1;
}

function pageLoading(sender, pageLoadingEventArgs) {
    loadingCount = loadingCount + 1;
}

function pageLoaded(sender, pageLoadedEventArgs) {
    loadedCount = loadedCount + 1;
}

function endRequest(sender, endRequestEventArgs) {
    endCount = endCount + 1;
}

function showCounts() {
    var message = "initialized = " + initializeCount + 
            "\r\nbegin = " + beginCount + 
            "\r\nloading = " + loadingCount + 
            "\r\nloaded = " + loadedCount + 
            "\r\nend = " + endCount;
    alert(message);
}
</script>
<div style="border-style:solid">
<asp:UpdatePanel runat="server">
<ContentTemplate>
<br />&nbsp;<asp:Label runat="server" ID="theCount"></asp:Label><br />
<asp:Button runat="server" Text="Refresh" ID="RefreshButton" /><br /><br />
</ContentTemplate>
</asp:UpdatePanel>
</div><br />

<input type="button"  onclick="showCounts()" value="Show Counts" />
</form>
</body>
</html>
Image from book

After the page is first run, it shows one page execution and one pageLoaded. The other counts are zero. If you click the Refresh button and wait, all of the counts are incremented by one. But if you click the button several times quickly, the counts begin to change out of synch with each other. Notice in Figure 2-7 that the page execution count of 12 corresponds to the count of the pageLoaded counter. This count is one ahead of the beginRequest, loadingPage, and endRequest counters. However, the initializedRequest counter is now much higher. As I clicked the button faster than the request could be initiated, the previous start would be canceled in favor of the newer request. At that point, the multiple starts are essentially seen as simultaneous, and only one is advanced to the beginRequest event.

Canceling a Request

The initializeRequest event alerts you to the fact that a request is about to begin and gives you access to the element responsible for initiating the request. You can examine the state of things and explicitly cancel the request here before it even begins based on other factors. For example, you can query the PageRequestManager and see that an asynchronous postback is already underway:

function initializeRequest(sender, initializeRequestEventArgs) {
    var prm = Sys.WebForms.PageRequestManager.getInstance();
    if(prm.get_isInAsyncPostBack() === true) {
        initializeRequestEventArgs.set_cancel(true);
        alert("canceling event from " + 
initializeRequestEventArgs.get_postBackElement().id);
    }
    initializeCount = initializeCount + 1;
}

That would prevent the new request from starting, but you also might want to abort a request after it has started. Suppose you included a Cancel button in your page:

<input type="button" onclick="cancelRequest()" value="Cancel Request" />

In the onClick handler, you could then abort a request that is already underway but not yet completed:

function cancelRequest() {
    var prm = Sys.WebForms.PageRequestManager.getInstance();
    if(prm.get_isInAsyncPostBack() === true) {
        prm.abortPostBack();
    }
}

You can see in Figure 2-8 that the count of calls to the different event handlers can get out of synch. The number of times the page executed is no longer the same as the number of times the page was loaded. Requests were canceled after the page execution was already started. The endRequest event is still called for every beginRequest. And once the pageLoading event started, the load finished and the pageLoaded event was called.

Image from book
Figure 2-8

You can count on getting an endRequest event if the beginRequest event occurs, even if the request is canceled. But, as you have seen, you can’t expect each event for every request initialized. Other factors will come into play that will also skew the lifecycle.

Detecting Errors

Not to sound pessimistic, but just when you think everything is going great, inevitably something goes wrong. The EndRequestEventArgs includes an error property to allow you to detect and react to errors that have happened during asynchronous requests. The default behavior is to display an error, but you may want to change this to save some user state and navigate to your own error page or to provide a recovery path unique to your application.

To demonstrate, I simply throw a NotImplementedException for postbacks on the server:

<script language="C#" runat="server">
    protected override void OnLoad(EventArgs e) {
        if(IsPostBack) {
            throw new NotImplementedException("something terrible has happened");
        }
    }
</script>

The endRequest handler then displays the error description and sets the errorHandled property so the PageRequestManager won’t react to it any further:

function endRequest(sender, endRequestEventArgs) {
    var theError = endRequestEventArgs.get_error();
    if(theError !== null) {
        alert(theError.description);
        endRequestEventArgs.set_errorHandled(true);
    }
}

The EndRequestEventHandler also exposes a property to indicate whether or not the request was aborted before while it was being processed:

function endRequest(sender, endRequestEventArgs) {
    if(endRequestEventArgs.get_aborted()) {
        alert("request was aborted");
    }
}

Working with Updates

Part of the complexity of partial page updates comes in establishing and maintaining relationships between elements on the page as the page evolves and as parts are updated independently. Of course, in real applications, you will have event handlers attached to DHTML DOM (Document Object Model) elements within an UpdatePanel that affect other parts of the page. When an asynchronous postback occurs, the event hookup will be lost and must be reestablished.

I extended the idea of displaying the server time in an UpdatePanel to have it in two separate UpdatePanels: panel1 and panel2.

<asp:UpdatePanel runat="server" ID="panel1" UpdateMode="Conditional">
<ContentTemplate>
Panel One
<asp:Label runat="server" ID="theTime"></asp:Label><br />
<asp:Button runat="server" Text="Refresh" ID="RefreshButton" /><br /><br />
</ContentTemplate>
</asp:UpdatePanel>
</div><br />

<div style="border-style:solid">
<asp:UpdatePanel runat="server" ID="panel2" UpdateMode="Conditional">
<ContentTemplate>
Panel Two
<asp:Label runat="server" ID="theTime2"></asp:Label><br />
<asp:Button runat="server" Text="Refresh" ID="RefreshButton2" /><br /><br />
</ContentTemplate>
</asp:UpdatePanel>

The time1 and time2 labels are updated each time the page is executed on the server:

<script language="C#" runat="server">
    protected override void OnLoad(EventArgs e) {
        string text = DateTime.Now.ToLongTimeString();
        theTime.Text = text;
        theTime2.Text = text;
    }
</script>

Then I added a span elsewhere on the page that should reflect the time as it is in panel1:

The Time from Panel One is: <asp:Label runat="server" ID="theDuplicate" /><br />

If the dependency were really as simple as getting one string and copying it over as I am doing here, it wouldn’t merit any extra work. I would just copy it over in a pageLoaded event handler and be done with it. In a more realistic example, the dependencies would probably be more labor intensive. Instead of blindly doing the work each time any part of the page is updated, you would want to do the work only when necessary. In this case, that would be just when panel1 was changing. Instead of putting the code directly in the pageLoaded event handler, it goes in a separate function.

function hookupPanelOne() {
 $get('<%= theDuplicate.ClientID %>').innerHTML =
$get('<%= theTime.ClientID %>').innerHTML;
}

The $get syntax is a shortcut for locating the item in the browser’s DOM. (More details on the Microsoft AJAX Library are in Chapter 4.) To tell when the hookupPanelOne function needs to be called, I examine which UpdatePanels are being created and which are being updated in the pageLoaded event han-dler. The PageLoadedEventArgs provides property accessors that return arrays of the newly created and updated UpdatePanels.

function pageLoaded(sender, pageLoadedEventArgs) {
    var panelsCreated = pageLoadedEventArgs.get_panelsCreated();
    for(var i = 0; i < panelsCreated.length; i++) {
        if(panelsCreated[i].id === "panel1") {
            hookupPanelOne();
        }
    }

    var panelsUpdated = pageLoadedEventArgs.get_panelsUpdated();
    for(var i = 0; i < panelsUpdated.length; i++) {
        if(panelsUpdated[i].id === "panel1") {
            hookupPanelOne();
        }
    }
}

Yes, this looks like a lot of overhead for a simple example, but if you had cascading dependencies or were making additional Web Service calls as the result of an UpdatePanel being refreshed, you could save extra work for the browser and the server by updating the dependency only when it’s necessary. Putting it all together results in the page shown in Figure 2-9. The panels can update independently, and the Duplicate span is changed as a function of updates to the first panel only.

Image from book
Figure 2-9