A Quick Fix for the Validator SetFocusOnError Bug
The ASP.NET validators have this nice property called
"SetFocusOnError" that is supposed to set the focus to the
first control that failed validation. This all works great
until your validator control is inside a naming container. I
ran into this recently when using validators in a
DetailsView. Take this simple example:
<%@ Page Language="C#" %> <script runat="server"> protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) DataBind(); } </script> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> </head> <body> <form id="_frm" runat="server"> <asp:DetailsView ID="dv1" DefaultMode="Edit" DataSource='<%# new object[1] %>' runat="server" > <Fields> <asp:TemplateField HeaderText="First Name:"> <EditItemTemplate> <asp:TextBox ID="FirstNameTextBox" runat="server" /> <asp:RequiredFieldValidator ID="FirstNameValidator1" ControlToValidate="FirstNameTextBox" ErrorMessage="First name is required." Display="Dynamic" EnableClientScript="false" SetFocusOnError="true" ValidationGroup="bug" Text="*" runat="server" /> </EditItemTemplate> </asp:TemplateField> </Fields> <FooterTemplate> <asp:ValidationSummary ID="vs1" DisplayMode="List" ValidationGroup="bug" runat="server" /> <asp:Button ID="Button1" Text="Post Back" ValidationGroup="bug" runat="server" /> </FooterTemplate> </asp:DetailsView> </form> </body> </html>
If you run this page and do a view source you'll see that
the FirstNameTextBox gets rendered like this:
<input name="dv1$FirstNameTextBox" type="text" id="dv1_FirstNameTextBox" />
If you just do a post back without entering a value to cause
the validator to fail it will output this line of java
script in an attempt to set the focus to the invalid
element:
WebForm_AutoFocus('FirstNameTextBox');
See anything wrong with this? It would seem that the
validators just use the string value you typed in for the
ControlToValidate property rather than doing a FindControl
and using the UniqueID. This is exactly what happens and I
verified it with reflector. The Validate method on
BaseValidator does this:
if ((!this.IsValid && (this.Page != null)) && this.SetFocusOnError) { this.Page.SetValidatorInvalidControlFocus(this.ControlToValidate); }
If you follow the call to SetValidatorInvalidControlFocus
you'll see that it never resolves the full UniqueID of the
control that its going to set focus to.
Ok, so this sucks. How do I work around it. My solution was
to simply ditch using the SetFocusOnError property and
implement the focus logic myself which is actually pretty
easy. I overrode Validate method on my Page like this:
public override void Validate(string group) { base.Validate(group); // find the first validator that failed foreach (IValidator validator in GetValidators(group)) { if (validator is BaseValidator && !validator.IsValid) { BaseValidator bv = (BaseValidator)validator; // look up the control that failed validation Control target = bv.NamingContainer.FindControl(bv.ControlToValidate); // set the focus to it if (target != null) target.Focus(); break; } } }
If your using C# 3 this is even easier using LINQ:
public override void Validate(string group) { base.Validate(group); // get the first validator that failed var validator = GetValidators(group) .OfType<BaseValidator>() .FirstOrDefault(v => !v.IsValid); // set the focus to the control // that the validator targets if (validator != null) { Control target = validator .NamingContainer .FindControl(validator.ControlToValidate); if (target != null) target.Focus(); } }
I hope this saves someone the headache of tracking this down.