I am creating a custom server control to generate button elements with specific markup and JavaScript handlers for my Web Forms application. They are, of course, able to cause postbacks, so I would like them to function with any of ASP's validation controls for form validation, especially the client-side framework.
This button server control supports an OnClientClick
property to emit an onclick
attribute in the button tag with the code provided (primarily used for a simple confirmation reprompt when a user clicks a delete button for a list view or similar), so using the asp:Button
control's method of emitting the validation script as an onclick attribute will be pretty ineffectual. As a matter of fact, specifying both OnClientClick
and ValidationGroup
attributes on a standard asp:Button
turns out pretty badly. Here's a painfully obvious example of why that's not working out of the box:
Page Markup
<asp:Button ID="btnSaveAsp" ValidationGroup="vgMyValidationGroup" OnClientClick="return true;" runat="server" />
Rendered Markup
<input type="submit" name="ctl00$cphBodyContent$lvMyList$ctrl0$btnSaveAsp" value="Save" id="cphBodyContent_lvUsers_btnSaveAsp_0"
onclick='return true; WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions("ctl00$cphBodyContent$lvMyList$ctrl0$btnSaveAsp", "", true, "vgMyValidationGroup", "", false, false))'>
Here is the existing non-working code for wiring up the control with validation. I was unable to find much documentation on how best to accomplish this with a method aside from emitting a similar onclick
attribute. I thought my call to Page.ClientSCript.RegisterForEventValidation
in the overridden AddAttributesToRender
method would wire up the client-side validation, but that does not appear to be functioning as I assumed. If necessary, jQuery is available for use in binding additional handling to the button's click event:
Custom Server Button Control
<ToolboxData("<{0}:Button runat=server></{0}:Button>")> _
<ParseChildren(False)> _
<PersistChildren(True)> _
Public Class Button
Inherits System.Web.UI.WebControls.WebControl
Implements IPostBackDataHandler
Public Sub New()
MyBase.New(HtmlTextWriterTag.Button)
End Sub
<Category("Behavior")> _
<DefaultValue("")> _
Public Overridable Property PostBackUrl As String
Get
Return If(ViewState("PostBackUrl"), String.Empty)
End Get
Set(value As String)
ViewState("PostBackUrl") = value
End Set
End Property
<Category("Validation")> _
<DefaultValue(True)> _
Public Overridable Property CausesValidation As Boolean
Get
Return If(ViewState("CausesValidation"), True)
End Get
Set(value As Boolean)
ViewState("CausesValidation") = value
End Set
End Property
<Category("Validation")> _
<DefaultValue("")> _
Public Overridable Property ValidationGroup As String
Get
Return If(ViewState("ValidationGroup"), String.Empty)
End Get
Set(value As String)
ViewState("ValidationGroup") = value
End Set
End Property
<Category("Behavior")> _
<DefaultValue("")> _
<Description("Client-side script to be run when the button is clicked.")> _
Public Property OnClientClick As String
Get
Return If(ViewState("OnClientClick"), String.Empty)
End Get
Set(value As String)
ViewState("OnClientClick") = value
End Set
End Property
Protected Overrides Sub AddAttributesToRender(writer As HtmlTextWriter)
MyBase.AddAttributesToRender(writer)
If Not String.IsNullOrEmpty(OnClientClick) Then
writer.AddAttribute(HtmlTextWriterAttribute.Onclick, OnClientClick)
End If
Dim postBackOptions = GetPostBackOptions()
If postBackOptions.TargetControl Is Me Then
writer.AddAttribute(HtmlTextWriterAttribute.Name, ClientID)
End If
If Page IsNot Nothing Then
Page.ClientScript.RegisterForEventValidation(postBackOptions)
End If
End Sub
Protected Overridable Function GetPostBackOptions() As PostBackOptions
Dim options As New PostBackOptions(Me) With {
.ClientSubmit = False
}
If Page IsNot Nothing Then
If CausesValidation AndAlso (Page.GetValidators(ValidationGroup).Count > 0) Then
options.PerformValidation = True
options.ValidationGroup = ValidationGroup
End If
If Not String.IsNullOrEmpty(PostBackUrl) Then
options.ActionUrl = HttpUtility.UrlPathEncode(ResolveClientUrl(PostBackUrl))
End If
End If
Return options
End Function
End Class
Presently, this code does not function with an asp:CompareValidator
in the same ValidationGroup
to determine if two password reset fields are equal before posting back to the server, nor does validation occur once the request gets to the server side.
Making OnClientClick
work with client-side form validation
Since the <asp:Button>
control concatenates the value of OnClientClick
with the form validation script, the easiest way to make them work together is to return false
when you want to block the form submission, and do nothing if you want the button to validate and submit the form:
OnClientClick="if (!confirm('Are you sure?')) return false;"
However, if you absolutely want to write return confirm('Are you sure?')
, then you can move the form validation code to an event listener (as you suggest), or you can wrap the OnClientClick
code like this:
writer.AddAttribute(
HtmlTextWriterAttribute.Onclick,
"if (!(function() { " + this.OnClientClick + "; return true; })()) return false;" +
this.Page.ClientScript.GetPostBackEventReference(options, false));
Server-side form validation
You need to implement the IPostBackEventHandler
interface and call the Page.Validate
method. The ClientScriptManager.RegisterForEventValidation
method is used for event validation (preventing unauthorized or malicious postbacks), not for form validation.
Sample code (C#)
Here is the code for a bare-bones custom button control that supports OnClientClick
and ValidationGroup
:
[ParseChildren(false)]
[PersistChildren(true)]
public class Button : WebControl, IPostBackEventHandler
{
private static readonly object EventClick = new object();
public Button()
: base(HtmlTextWriterTag.Button)
{
}
public bool CausesValidation
{
get { return ((bool?)this.ViewState["CausesValidation"]) ?? true; }
set { this.ViewState["CausesValidation"] = value; }
}
public string ValidationGroup
{
get { return (string)this.ViewState["ValidationGroup"] ?? ""; }
set { this.ViewState["ValidationGroup"] = value; }
}
public string OnClientClick
{
get { return (string)this.ViewState["OnClientClick"] ?? ""; }
set { this.ViewState["OnClientClick"] = value; }
}
public event EventHandler Click
{
add { this.Events.AddHandler(EventClick, value); }
remove { this.Events.RemoveHandler(EventClick, value); }
}
protected override void AddAttributesToRender(HtmlTextWriter writer)
{
base.AddAttributesToRender(writer);
writer.AddAttribute(HtmlTextWriterAttribute.Name, this.UniqueID);
if (this.Page != null && this.Enabled)
{
PostBackOptions options = this.GetPostBackOptions();
writer.AddAttribute(
HtmlTextWriterAttribute.Onclick,
this.OnClientClick + this.Page.ClientScript.GetPostBackEventReference(options, false));
}
}
protected virtual PostBackOptions GetPostBackOptions()
{
PostBackOptions options = new PostBackOptions(this) { ClientSubmit = false };
if (this.Page != null)
{
if (this.CausesValidation && this.Page.GetValidators(this.ValidationGroup).Count > 0)
{
options.PerformValidation = true;
options.ValidationGroup = this.ValidationGroup;
}
}
return options;
}
protected virtual void OnClick(EventArgs e)
{
EventHandler handler = (EventHandler)this.Events[EventClick];
if (handler != null)
{
handler(this, e);
}
}
void IPostBackEventHandler.RaisePostBackEvent(string eventArgument)
{
if (this.CausesValidation)
{
this.Page.Validate(this.ValidationGroup);
}
this.OnClick(EventArgs.Empty);
}
}