|
Tuesday, July 22
|
The UpdatePanel is one of the coolest features of Microsoft's ASP.NET AJAX. It magically lets you enhance your web sites with AJAX goodness, with very little cost to you as a developer. If you are like me though, you'll be feeling just a little bit uncomfortable simply dragging the UpdatePanel onto your web form, and then letting it do its stuff. We want to understand what it is doing on our behalf.
Introduction In this article I'll walk through what happens in the browser when an UpdatePanel updates. You'll see what events are fired, what intervention points there are where you can intercept the action, and generally get a feel for the sequence of events.
I'll take the simplest case possible. I've created an UpdatePanel with two children: a Button and a Label:
 When the button is clicked the code-behind sets the label to the current time:
using System; public partial class _Default : System.Web.UI.Page { protected void Button1_Click(object sender, EventArgs e) { Label1.Text = DateTime.Now.ToString(); } }
There are three major events that are triggered in the Web Browser, and all three hit the PageRequestManager object, which is central to the ASP.NET AJAX Architecture. The first event is fired because the button is clicked. The second event is fired because the form is submitted. The final event occurs when the response is received from the web server.
Before the PageRequestManager's events are fired, there are of course lower level infrastructure events that in turn invoke the PageRequestManager. These low level handlers do things like wrapping the browser's event object in a Sys.UI.DomEvent. I'll not be going into detail on these.
The PageRequestManager _onFormElementClick event handler This gets invoked when you click on the button in the UpdatePanel, and it does two things.
First it sets up the PageRequestManager's _postBackSettings member:
this._postBackSettings = this._getPostBackSettings(element, element.name);
The postBackSettings has three properties:
A boolean indicating whether the postback should be asynchronous or not The ID of the panel and element that is causing the postback A reference to the element that triggered the postback The _getPostBackSettings method walks up the control tree starting with the element that fired the event (in this case a button) and looking for an UpdatePanel. If it finds an UpdatePanel, which in our case it will, it sets the postBackSettings to indicate that an asynchronous postback is required:
var indexOfPanel = Array.indexOf(this._updatePanelClientIDs, element.id); if (indexOfPanel !== -1) { if (this._updatePanelHasChildrenAsTriggers[indexOfPanel]) { return this._createPostBackSettings(true, this._updatePanelIDs[indexOfPanel] + '|' + elementUniqueID, originalElement); }
The second thing that the onFormElementClick handler does is set up an additionalInput member with the name and value of the button that is clicked. This is because the process that collects up all the form values when the form is submitted does not handle buttons, since only the button that is clicked should have its value sent to the server:
else if ((element.tagName === 'BUTTON') && (element.name.length !== 0) && (element.type === 'submit')) { this._additionalInput = element.name + '=' + encodeURIComponent(element.value); }
The PageRequestManager _onFormSubmit event handler This method gets invoked after the _onFormElementClick handler finishes and is responsible for packaging up the form values and submitting the asynchronous request to the server. It only does that if an asynchronous request is what is required, which it determines by looking at corresponding boolean flag in the _postBackSettings member that was set up in the onFormElementClick method.
In our case an asynchronous request is required. The method walks through the children of the form, packaging up their values into a formBody StringBuilder if they are INPUT (text, password, hidden, checkbox and radio types), SELECT or TEXTAREA elements.
As an aside, a small optimization could possibly be made when processing SELECTs, the current routine looks at every single OPTION child to see if it is selected, whereas if the multiple property on the SELECT is false it could use its selectedIndex property. If there are 1000 options, there is no need to look at all 1000 if the SELECT is not multiple:
else if (tagName === 'SELECT') { var optionCount = element.options.length; for (var j = 0; j < optionCount; j++) { var option = element.options[j]; if (option.selected) { formBody.append(name); formBody.append('='); formBody.append(encodeURIComponent(option.value)); formBody.append('&'); } } }
Note that hidden fields are submitted, which means that the hidden __VIEWSTATE element's contents will be sent up the server and back again. Just because you are using the UpdatePanel doesn't mean that you'll escape the cost of shifting the viewstate up to the server and back again when an asynchronous update occurs.
In order to submit the request the method creates a Sys.Net.WebRequest and sets it's URL to the form's action URL, sets its x-microsoft-ajax header to delta=true to indicate that this is a request for deltas (changes) to the document. It also sets the request to have no caching, and the timeout to be whatever was specified on the server-side ScriptManager.
var request = new Sys.Net.WebRequest(); request.set_url(form.action); request.get_headers()['X-MicrosoftAjax'] = 'Delta=true'; request.get_headers()['Cache-Control'] = 'no-cache'; request.set_timeout(this._asyncPostBackTimeout);
Obviously the PageRequestManager needs to be told when the response from the server has arrived, and it does this by creating a delegate to it's _onFormSubmitCompleted method and adding the delegate to the list of delegates that are notified when the request has completed:
request.add_completed( Function.createDelegate(this, this._onFormSubmitCompleted));
You might think that the PageRequestManager is pretty much ready to send its request at this point, but there are a couple more things it has to do.
One of the features I like the most about the whole ASP.NET and ASP.NET AJAX design is that its been built with numerous hooks, which allow developers to intercept the standard processing and add their own functionality. This is great because it lets us add features that Microsoft could not even dream of, and to me it shows openness that wasn't exactly Microsoft's trademark in the early years.
So before the request gets submitted, the PageRequestManager fires off an InitializeRequest event to anyone that has subscribed to it, at this stage the form submission can be canceled by subscribers to the event by setting the cancel flag on the event arguments:
var handler = this._get_eventHandlerList().getHandler("initializeRequest"); if (handler) { var eventArgs = new Sys.WebForms.InitializeRequestEventArgs( request, this._postBackSettings.sourceElement); handler(this, eventArgs); continueSubmit = !eventArgs.get_cancel(); }
This is an example of how user code could subscribe to this event:
var prm = Sys.WebForms.PageRequestManager.getInstance(); prm.add_initializeRequest(InitializeRequestHandler);
function InitializeRequestHandler(sender, initializeRequestEventArgs) { var cancel = !confirm('Initialize request occurring.'); initializeRequestEventArgs.set_cancel(cancel); }
Having fired the InitializeRequest event if the submission wasn't canceled, the PageRequestManager fires a BeginRequest event to anyone that has signed up for it, although at this stage the submission cannot be canceled.
Finally, having noted the scroll position of the document in the _scrollPosition member, it asks the Sys.Net.WebRequest to send the request, and cancels the default button processing, so that nothing else happens until the WebRequest comes back:
request.invoke();
if (evt) { evt.preventDefault(); }
The PageRequestManager _onFormSubmitCompleted event handler This method gets called when the response from the server has been received, or if something went wrong.
The first thing it does is check for all the things that might have gone wrong, such as timing out, being aborted, the response being the response to a different request, an error occurring etc. you get the idea. In most of these cases it calls the internal _endPostBack method which fires off an EndRequest event to anyone that has signed up to receive it.
Having done those checks it walks through the response buffer received from the server, extracting delta nodes each of which has a type, an id and content.
Once it has this array of deltas, it looks at each one in turn. There are currently around twenty different types of delta nodes, some of which can be processed immediately, such as the document title:
for (var i = 0; i < delta.length; i++) { var deltaNode = delta[i]; switch (deltaNode.type) { ... case "pageTitle": document.title = deltaNode.content; break; ...
Others are saved for later processing:
case "updatePanel": Array.add(updatePanelNodes, deltaNode); break; case "hiddenField": Array.add(hiddenFieldNodes, deltaNode); break;
Having sorted the delta nodes into their respective types or acted immediately in a couple of cases, each type of delta node is processed, but not before firing the PageLoadingEvent to anyone that has signed up for it.
The updatePanelNodes are processed by destroying the existing panel contents, and then replacing the contents with the delta node's content.
Finally any scripts that have to be loaded are loaded and then in the _scriptsLoadComplete method the PageLoadedEvent and EndRequestEvents are fired, the window is scrolled to the appropriate position, the focus is set to the appropriate control, and the story ends.
You might be inclined to think that this is all pretty straight-forward, and for this simple case it is. But if you think about the complexity of what you can do on the server, then you'll realize that there has to be a lot more than this. For example what if you register new script blocks on the server, or set focus to another control? This is why there are twenty or so delta node types, each to handle a specific change scenario.
A small note on performance You've often heard that you shouldn't run with the debug flag set to true in your Web.Config on a production machine --ASP.NET AJAX makes this suggestion even more true. Here's a statistic to back this up. When you click on the button in debug mode, there are 10,126 JavaScript statements executed in the web browser. When you run without debug mode, there are 1,448 statements executed. The reason for the difference is almost entirely due to the parameter validation that is taking place in the debug build, indeed if you break into JavaScript in debug mode then you are almost guaranteed to break into one of the parameter validation routines, they eat up an enormous amount of resources.
Summary In this article you've had a walk through what happens in the web browser when an updatepanel updates, there is of course no magic, but there is a lot of code handling special cases, and lots of places where you can intercept the standard processing to add your own magic to the mix.
|
|
Post your own comment
|
|
0
comment(s):-
|
|