Introduction
When attempting to create web applications that are based off Windows applications, there tends to be a thin line between behavioral differences that is acceptable to end users or the management. There are more than a few examples out on the web right now that will show you how to queue async requests and/or persist focus between requests, but I want to encapsulate all this logic into a nice JavaScript object to hide the functionality from my own business objects, for extensibility reasons. I have to admit that some of the ideas here and there came from separate posts, but I no longer have the links. If anybody happens to find a link that shows similar code, I will quote it here.
Objective
To create a JavaScript object that will encapsulate the pageRequestManager
of Microsoft AJAX in order to extend its behaviors when handling async postbacks for an UpdatePanel
.
The Code
Object Creation
The first step is to create the JavaScript object and initialize it by placing its constructor call at the top of the .js file (this ensures I don’t have to worry about constructing it outside of the file itself).
var _AsyncPostbackUtility = new AsyncPostbackUtility();
function AsyncPostbackUtility(){}
Constructor
Afterwards, I need some logic in the constructor that will wire to the events of the pageRequestManager
so that I can interact with it during postbacks.
Notes: I check to see if Sys
is defined (i.e., if there is a scriptManager
on the page and has the scriptManager
had a chance to be defined). If you have included this script in the scripts directory of the script manager, you are fine, but things can go south if you created your own include
tag within your HTML page directly.
function AsyncPostbackUtility(){
var currentFocusedControlId = "";
var requestQueue = new Array();
if (Sys)
Sys.Application.add_init(appInit);
else
throw 'Sys is not defined, please verify you ' +
'have a script manager on the page ' +
'and that this object js file is included ' +
'in its script collection.';
function appInit(){
Sys.WebForms.PageRequestManager.getInstance().
add_initializeRequest(initializeRequestHandler);
Sys.WebForms.PageRequestManager.getInstance().
add_pageLoading(pageLoadingHandler);
Sys.WebForms.PageRequestManager.getInstance().
add_endRequest(endRequestHandler);
}
…
}
Event Handlers
Now that we have object creation and event wiring going, it's time to place some logic into the event handlers themselves in order to achieve the functionality we are looking for.
Initialize Request Handler
This is the first event that gets called whenever a postback request is made by the client. Within this event, we have the ability to cancel the request, which is what we will utilize if we are already in an async postback. You might be asking yourself why we need a queue, and the answer is simple. If a request is attempted while another request is pending, the first request on the page is canceled by the client in favor of the second request, which, as you can imagine, might leave your UI a bit out of sync. Take notice that we maintain a reference to the element that caused the last postback, and that we only queue requests that are not generated by that element. This is done to prevent click happy users when an async postback is taking longer than expected, and disabling the UI or using a progress indicator isn’t useful. By queuing the requests, we can give the application a more fluid feel without disabling the form on every postback, and ensure we don’t miss updates from the UI in case the user is moving quickly and triggering postbacks.
function initializeRequestHandler(sender, args){
var postBackElement = args.get_postBackElement();
if (Sys.WebForms.PageRequestManager.getInstance().
get_isInAsyncPostBack() == false){
executingElement = postBackElement;
}
else{
if (executingElement != postBackElement){
var evArg = $get("__EVENTARGUMENT").value;
Array.enqueue(requestQueue, new Array(postBackElement, evArg));
}
args.set_cancel(true);
}
}
Page Loading Request Handler
This event is fired after the page request manager gets the result of the async postback, but before it has rebuilt the UpdatePanel
, and is used exclusively by this object to get a reference to the ActiveElement
object to persist focus once the UpdatePanel
is rebuilt.
Note: Firefox 3/IE6 and above will support the activeElement
property. If you must support different or earlier versions of the browsers, I would recommend a different method at this point.
function pageLoadingHandler(sender, args){
currentFocusedControlId = typeof(document.activeElement) ==
"undefined" ? "" : document.activeElement.id;
}
End Request Handler
This event is fired after the UpdatePanel
has been loaded and the async postback has completed. This is where we will persist focus and check our queue to see if we need to handle additional postbacks.
function endRequestHandler(sender, args){
focusControl();
if (requestQueue.length > 0){
var elemEntry = Array.dequeue(requestQueue);
var _element = elemEntry[0];
var _eventArg = elemEntry[1];
Sys.WebForms.PageRequestManager.getInstance()._doPostBack(
_element.id, _eventArg);
}
}
Focus Control Method
The focus control method is straightforward except for the trick at the end to reset the cursor to the end of a text box or a text area element.
function focusControl(){
if (typeof(currentFocusedControlId) !=
"undefined" && currentFocusedControlId != ""){
var targetControl =
document.getElementById(currentFocusedControlId);
if (targetControl.contentEditable != "undefined"){
oldContentEditableSetting = targetControl.contentEditable;
targetControl.contentEditable = false;
}
targetControl.focus();
if (oldContentEditableSetting != "undefined")
targetControl.contentEditable = oldContentEditableSetting;
targetControl.value = targetControl.value;
}
}
How to Use the Code
As it comes out of the box, it is very simple. Just add a script reference via the script manager on your page to the .js file of this article, or you could remove the constructor from the .js file and call it somewhere after the body load event of your web page.