Wednesday, August 1, 2007

Restoring Lost Focus in the Update Panel with Auto Post-Back Controls

UpdatePanel control is used for different purposes like reducing flickering of the page and reducing network traffic generated by a web site. Developers often wrap input boxes into an UpdatePanel to implement cascading drop-downs and update other related controls.  Cascading drop-downs can be implemented with a help of Ajax Control Toolkit control extenders, but in general case you will either need to write script code or place controls in an update panel.  The latest approach is very easy to implement, but it also has a lot of drawbacks.  If you wrap input boxes (and other input controls) in the update panel, you must be aware about the following consequences:

  1. If a user changes value of an auto post-back control and "tabs" to the next control, he or she starts asynchronous (AJAX) post-back.  However, as the post-back is asynchronous the user can continue typing (selecting value) in the next selected control. If the server responds to asynchronous post-back after the user has started typing in the next control, everything typed in this control is lost once AJAX post-back completes.

    If the server responds quite fast, this situation is unlikely to happen, but if the server is busy or web site is browsed from the dial-up connection, it is likely to happen.  It is still less probable than with full-post back, but possible.
  2. The second annoying behavior you may experience with input controls inside an UpdatePanel control is lost focus.  Input (keyboard) focus moves to nowhere once AJAX post-back completes.  Web site users who prefer to use keyboard need to use mouse to activate appropriate input box or press TAB multiple times.  (The exact behavior is a little different for Internet Explorer and Mozilla FireFox).

You can place this UpdatePanel to an ASPX page to see the second problem by yourself:

<asp:UpdatePanel runat="server" ID="up">
<ContentTemplate
>
<div
>
<asp:DropDownList ID="ddl1" runat="server" AutoPostBack
="True">
<asp:ListItem>1</asp:ListItem
>
<asp:ListItem>2</asp:ListItem
>
<asp:ListItem>3</asp:ListItem
>
</asp:DropDownList></div
>
<div
>
<asp:DropDownList ID="ddl2" runat="server" AutoPostBack
="True">
<asp:ListItem>1</asp:ListItem
>
<asp:ListItem>2</asp:ListItem
>
<asp:ListItem>3</asp:ListItem
>
</asp:DropDownList></div
>
<div
>
<asp:TextBox runat="server" ID="tb"></asp:TextBox><br
/>
</div
>
</ContentTemplate
>
</
asp:UpdatePanel>

Why Does It Happen?

I need to remind some details of how UpdatePanel works to answer this question.


If ScriptManager EnablePartialRendering property is set to true, controls on the web page can initiate asynchronous post-back.  When it happens web page send asynchronous request to the server.  The web page instance at the server goes through all the phases in its normal lifecycle, but instead of full rendering only partial rendering happens.  The html content for updated panels is sent back to the client in the format that client script manager control can recognize.  The client part of the ScriptManager control parses the response and set innerHTML properties of all DIV elements generated by UpdatePanels.


So, what happens to the UpdatePanel with input controls?  When client script receives a response from the server with new Html content for the UpdatePanel it assigns this content to the DIV element generated by the UpdatePanel.  Input focus at this time is in one of the controls inside the DIV element (UpdatePanel).  The browser destroys the old content of the UpdatePanel including input controls and creates new elements by parsing new content assigned to the innerHTML property.  Input focus moves out of the update panel when focused input controls is being destroyed and it is not restored later when the new controls are created from the assigned html.


Solution of the Problem with the Lost Input Focus

The basic idea behind the solution is to save the ID of the control with input focus before the update panel is updated and set input focus back to that control after the update panel is updated.


I come with the following JavaScript which restores the lost focus in the update panel. 

var lastFocusedControlId = "";

function focusHandler(e) {
document.activeElement = e.originalTarget;
}

function appInit() {
if (typeof(window.addEventListener) !== "undefined") {
window.addEventListener("focus", focusHandler, true);
}
Sys.WebForms.PageRequestManager.getInstance().add_pageLoading(pageLoadingHandler);
Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(pageLoadedHandler);
}

function pageLoadingHandler(sender, args) {
lastFocusedControlId = typeof(document.activeElement) === "undefined"
? "" : document.activeElement.id;
}

function focusControl(targetControl) {
if (Sys.Browser.agent === Sys.Browser.InternetExplorer) {
var focusTarget = targetControl;
if (focusTarget && (typeof(focusTarget.contentEditable) !== "undefined")) {
oldContentEditableSetting = focusTarget.contentEditable;
focusTarget.contentEditable = false;
}
else {
focusTarget = null;
}
targetControl.focus();
if (focusTarget) {
focusTarget.contentEditable = oldContentEditableSetting;
}
}
else {
targetControl.focus();
}
}

function pageLoadedHandler(sender, args) {
if (typeof(lastFocusedControlId) !== "undefined" && lastFocusedControlId != "") {
var newFocused = $get(lastFocusedControlId);
if (newFocused) {
focusControl(newFocused);
}
}
}

Sys.Application.add_init(appInit);

If you save this code to FixFocus.js file, it can be used as:

    <asp:ScriptManager ID="c_scriptManager" runat="server">
<Scripts
>
<asp:ScriptReference Path="~/FixFocus.js"
/>
</Scripts
>
</asp:ScriptManager>

Unfortunately, different browsers handle input focus a little differently.  Mozilla FireFox browser does not provide an easy way to get currently focus element at all.  The script, therefore, handles these differences between browsers. 


You may find the focusControl function a little strange.  I cannot explain the magic it does, but this is really required to set focus to the control in the Internet Explorer.  ASP.NET AJAX extensions use this code itself when setting focus set by ScriptManager.SetFocus method.


I tested this code with Internet Explorer 7 and the most recent build of Mozilla FireFox


Sample Web Site

You can download the sample web site here.


UPDATE

You can often solve the problem by placing each input control into its own update panel.  However, this approach does not solve the problem if user press TAB to go to the next control, which is in its own UpdatePanel. 


The code above is a workaround.  If you need to implement cascading drop-downs it is better to go with Ajax Control Toolkit Cascading Drop-Down Control Extenders.  If you need to use UpdatePanel or several UpdatePanels use this code, but be aware of problem #1 which is not solved by this script.

27 comments:

jptrahan said...

Nice Article, This script was very useful. However I found 1 issue. If the control that caused the postback becomes invisible, javascript raises and exception about not being able to focus on the control. To get around it I modified the script to work around this problem.

try
{
targetControl.focus();
if (focusTarget)
{
focusTarget.contentEditable = oldContentEditableSetting;
}
}
catch(err) { }

Yuriy Solodkyy said...

Hi jptrahan,
you are right about posisble exception there. Thank you.

harikesh said...

It is good article. it will raise exception when any button is disbled after clicked. so after miner changes it will work fine.
if(newFocused.disabled == false)
{ focusControl(newFocused);
}

Yuriy Solodkyy said...

Hi, once again thank you for noting about possible exceptions. I would like to use WebForm_CanFocus function to find out if it is possible to set focus to the given control. However, I am not yet sure how I can make ASP.NET include required scripts.

Anonymous said...

Nice Article. Solved my problem

Rico said...

Great article saved my butt ;)
THanks
Rico

Anonymous said...

My issues is it adds the curser before any of the copy that already resides in the textbox, is there a way to get it select the textbox?

Yuriy Solodkyy said...

unfortunately I don't know a solution for this, and i can just hope that user will notice wrong text typed in.

However, in general I prefer sending data items to client and updating controls without recreating them with UpdatePanel.

Anonymous said...

How could I use this code to set focus on a differnt control?

is there a way to send a contol's id?

thanks

BG

Anonymous said...

Thanks for this article, resolved issues with my app. There is one issue though, which is, when you type something on the first input box and tab out it doesn't set focus to the next input box until I press tab key one more time. i.e. for the first input box I have to use tab key twice,
Let me know what could be the problem.
Thanks,
Waj

Johnny said...

This article's gonna save my butt too! Thanks for publishing :D

Lasker said...

Hey Yuriy,

Just want to help out here as I was faced with a problem and your post helped me.

Firefox 3!

I had this software that lives in an IFRAME. Apparently scriptmanager.setfocus(control) will cause Firefox3 to JUMP to the textbox by scrolling it upwards until the textbox (that you want to set focus to) is right at the bottom of the page. This obviously sucks for an IFRAME apps(firefox2 and IE7 did not have this issue).

So it took a while until I found your blog post and used your JS file .. it worked. And I thought I should share this with you.

Thanks again :)

Miguel said...

This scripts was awesome!

But what if i want to do the same thing, but I click on a < a href="xpto" >xpto< /a> ?

I tryed the following

function focusControl(targetControl) {
if (Sys.Browser.agent === Sys.Browser.InternetExplorer) {
var focusTarget = targetControl;
if (focusTarget && (typeof(focusTarget.contentEditable) !== "undefined")) {
oldContentEditableSetting = focusTarget.contentEditable;
focusTarget.contentEditable = false;
}
else {
focusTarget = null;
}
decideAction(targetControl);

if (focusTarget) {
focusTarget.contentEditable = oldContentEditableSetting;
}
}
else {
decideAction(targetControl);
}
}

function decideAction(targetControl){
if(targetControl.tagName === "A") {
// targetControl.click(); - This was commented because when used the button was clicked twice
}
else {
targetControl.focus();
}
}

but as you see on my commented code there is the reason why it didn't work!

Yuriy Solodkyy said...

but how does click on <a href="..." cause async postback that refresh updatepanel?

Felipe said...

Great article! It appears to work well in the example, but in my project I copy and pasted your script and it couldn't find the 'Sys' object. Could you tell me what is it???

Miguel said...

Yuriy Solodkyy the full link is there below:

< a href="javascript:__doPostBack('ctl00$SIGContentPlaceHolder$mainControl$ppCorrectStock$lbtnPartitionateNumbering','')" cclass="linkFerramentas" title="Partir Numeração" iid="ctl00_SIGContentPlaceHolder_mainControl_ppCorrectStock_lbtnPartitionateNumbering">Partir Numeração< / a>

Yuriy Solodkyy said...

Felipe, sample uses MS AJAX library scripts and you must load script via asp:ScriptManager. If you don't want to use MS AJAX libary you need to replace everything withpure JavaScript.

Harish said...

Good article! But, if performance is not an issue, how about using setTimeout to cause a brief delay, and then set focus to desired the control on the popup? See for an example:

http://www.aspdotnetcodes.com/ModalPopup_Postback.aspx

Craig M Oliver said...

I found that if you if you change the script in the function 'focusHandler' from:

document.activeElement = e.originalTarget

to:

e.originalTarget.focus = true;

The Firefox compatibility issues go away for me.

Elise said...

It is great! It helps me in FireFox 3. But it doesn't work in Google Chrome. Any idea? Please advise.

Thanks in advance.

Rich said...

Thanks Yuriy, potentially tricky problem solved very easily thanks to your code!

bhushan said...

Thank you all...it really helped me

Anonymous said...

Here is a link to help you set the starting position in the textbox to the end.

good luck

Marc said...

hello,

the script worked perfectly for me, except for one thing. After gaining the focus back on my last textbox, i can type in it but it doesn't seem to be active. I have to click inside it for the Enter key to work, for example.

any idea how to solve this?
thanks

Galiyashka said...

Thanks a lot!

Anonymous said...

Thanks - I had a situation where I added a dropdown field to a page using AJAX, then set focus to that field using a setTimeout. But as soon as I moved the mouse or pressed a key, focus was lost. Adding this part of your code right before the field.focus() command fixed it for me:

if (typeof(field.contentEditable) !== "undefined") {
field.contentEditable = false;
}

However, I still don't understand why that would change the behavior.

Jing said...

I encountered the same exception as mentioned in the first comment. I commented out the line
...
}
// targetControl.focus();
if(focusTarget){
...

It worked!