3

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.

lsuarez
  • 4,952
  • 1
  • 29
  • 51
  • Could you provide an example of what `OnClientClick` would realistically be set to (instead of just `return true`)? – Michael Liu Mar 03 '12 at 04:50
  • `return confirm("Are you certain you wish to delete this item"?)` or any other reprompt. I've actually figured a way around this and I'll post my solution soon. It involved a little digging through the guts of ASP.NET's postback handling but if I just add the validation as a listener on the click event, things work well. – lsuarez Mar 03 '12 at 22:08

3 Answers3

6

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);
    }
}
Michael Liu
  • 52,147
  • 13
  • 117
  • 150
  • Thanks for the consideration in producing an answer. I'd prefer to do the validation client-side regardless of the user response, which is why I wasn't too keen on concatenation. Your answer gives me a good look at causing server-side validation, so I appreciate that. I'd forgotten to move the validation into my `RaisePostBackEvent` handler after moving the button from implementing `IPostBackDataHandler`. – lsuarez Mar 05 '12 at 14:28
0

continuing Bui Cuion what you need is these 2 things in your cs 1:

   protected override void AddAttributesToRender(HtmlTextWriter writer)
    {
        bool useSubmitBehavior = this.UseSubmitBehavior;
        if (this.Page != null)
        {
            this.Page.VerifyRenderingInServerForm(this);
        }
        if (useSubmitBehavior)
        {
            writer.AddAttribute(HtmlTextWriterAttribute.Type, "submit");
        }
        else
        {
            writer.AddAttribute(HtmlTextWriterAttribute.Type, "button");
        }
        PostBackOptions postBackOptions = this.GetPostBackOptions();
        writer.AddAttribute(HtmlTextWriterAttribute.Value, this.Text);
        bool isEnabled = base.IsEnabled;
        string firstScript = string.Empty;
        if (isEnabled)
        {
            firstScript = this.EnsureEndWithSemiColon(this.OnClientClick);
            if (base.HasAttributes)
            {
                string strOnClick = base.Attributes["onclick"];
                if (strOnClick != null)
                {
                    firstScript = firstScript + this.EnsureEndWithSemiColon(strOnClick);
                    base.Attributes.Remove("onclick");
                }
            }
            if (!this.AutoPostBack)
            {
                firstScript = this.MergeScript(this.OnClientClick, "return false;");
            }
            if (this.Page != null)
            {
                string postBackEventReference = this.Page.ClientScript.GetPostBackEventReference(postBackOptions, false);
                if (postBackEventReference != null)
                {
                    firstScript = this.MergeScript(firstScript, postBackEventReference);
                }
            }
        }
        if (this.Page != null)
        {
            this.Page.ClientScript.RegisterForEventValidation(postBackOptions);
        }
        if (firstScript.Length > 0)
        {
            writer.AddAttribute(HtmlTextWriterAttribute.Onclick, firstScript);
        }
        if (this.Enabled && !isEnabled)
        {
            writer.AddAttribute(HtmlTextWriterAttribute.Disabled, "disabled");
        }
        base.AddAttributesToRender(writer);

and

 protected virtual PostBackOptions GetPostBackOptions()
    {
        PostBackOptions options = new PostBackOptions(this, string.Empty);
        options.ClientSubmit = false;
        if (this.Page != null)
        {
            if (this.CausesValidation && (this.Page.GetValidators(this.ValidationGroup).Count > 0))
            {
                options.PerformValidation = true;
                options.ValidationGroup = this.ValidationGroup;
            }
            if (!string.IsNullOrEmpty(this.PostBackUrl))
            {
                options.ActionUrl = HttpUtility.UrlPathEncode(base.ResolveClientUrl(this.PostBackUrl));
            }
        }
        return options;
    }

options.ClientSubmit = false; is the secret

bresleveloper
  • 5,940
  • 3
  • 33
  • 47
0

Please have a look at .NET framework Button implementation. Especially AddAttributesToRender method. You can then modify the code to make it works the way you want:

public class Button : WebControl, IButtonControl, IPostBackEventHandler
{
    private readonly static object EventClick;

    private readonly static object EventCommand;

    [WebSysDescription("Button_CausesValidation")]
    [WebCategory("Behavior")]
    [DefaultValue(true)]
    [Themeable(false)]
    public bool CausesValidation
    {
        get
        {
            object item = this.ViewState["CausesValidation"];
            if (item == null)
            {
                return true;
            }
            else
            {
                return (bool)item;
            }
        }
        set
        {
            this.ViewState["CausesValidation"] = value;
        }
    }

    [Bindable(true)]
    [DefaultValue("")]
    [Themeable(false)]
    [WebCategory("Behavior")]
    [WebSysDescription("WebControl_CommandArgument")]
    public string CommandArgument
    {
        get
        {
            string item = (string)this.ViewState["CommandArgument"];
            if (item == null)
            {
                return string.Empty;
            }
            else
            {
                return item;
            }
        }
        set
        {
            this.ViewState["CommandArgument"] = value;
        }
    }

    [Themeable(false)]
    [WebCategory("Behavior")]
    [WebSysDescription("WebControl_CommandName")]
    [DefaultValue("")]
    public string CommandName
    {
        get
        {
            string item = (string)this.ViewState["CommandName"];
            if (item == null)
            {
                return string.Empty;
            }
            else
            {
                return item;
            }
        }
        set
        {
            this.ViewState["CommandName"] = value;
        }
    }

    [Themeable(false)]
    [WebSysDescription("Button_OnClientClick")]
    [DefaultValue("")]
    [WebCategory("Behavior")]
    public string OnClientClick
    {
        get
        {
            string item = (string)this.ViewState["OnClientClick"];
            if (item != null)
            {
                return item;
            }
            else
            {
                return string.Empty;
            }
        }
        set
        {
            this.ViewState["OnClientClick"] = value;
        }
    }

    [DefaultValue("")]
    [WebCategory("Behavior")]
    [WebSysDescription("Button_PostBackUrl")]
    [Editor("System.Web.UI.Design.UrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
    [Themeable(false)]
    [UrlProperty("*.aspx")]
    public string PostBackUrl
    {
        get
        {
            string item = (string)this.ViewState["PostBackUrl"];
            if (item == null)
            {
                return string.Empty;
            }
            else
            {
                return item;
            }
        }
        set
        {
            this.ViewState["PostBackUrl"] = value;
        }
    }

    [WebSysDescription("Button_Text")]
    [WebCategory("Appearance")]
    [DefaultValue("")]
    [Localizable(true)]
    [Bindable(true)]
    public string Text
    {
        get
        {
            string item = (string)this.ViewState["Text"];
            if (item == null)
            {
                return string.Empty;
            }
            else
            {
                return item;
            }
        }
        set
        {
            this.ViewState["Text"] = value;
        }
    }

    [WebSysDescription("Button_UseSubmitBehavior")]
    [WebCategory("Behavior")]
    [DefaultValue(true)]
    [Themeable(false)]
    public bool UseSubmitBehavior
    {
        get
        {
            object item = this.ViewState["UseSubmitBehavior"];
            if (item == null)
            {
                return true;
            }
            else
            {
                return (bool)item;
            }
        }
        set
        {
            this.ViewState["UseSubmitBehavior"] = value;
        }
    }

    [WebSysDescription("PostBackControl_ValidationGroup")]
    [WebCategory("Behavior")]
    [DefaultValue("")]
    [Themeable(false)]
    public string ValidationGroup
    {
        get
        {
            string item = (string)this.ViewState["ValidationGroup"];
            if (item == null)
            {
                return string.Empty;
            }
            else
            {
                return item;
            }
        }
        set
        {
            this.ViewState["ValidationGroup"] = value;
        }
    }

    static Button()
    {
        Button.EventClick = new object();
        Button.EventCommand = new object();
    }

    public Button() : base(47)
    {
    }

    protected override void AddAttributesToRender(HtmlTextWriter writer)
    {
        bool useSubmitBehavior = this.UseSubmitBehavior;
        if (this.Page != null)
        {
            this.Page.VerifyRenderingInServerForm(this);
        }
        if (!useSubmitBehavior)
        {
            writer.AddAttribute(HtmlTextWriterAttribute.Type, "button");
        }
        else
        {
            writer.AddAttribute(HtmlTextWriterAttribute.Type, "submit");
        }
        PostBackOptions postBackOptions = this.GetPostBackOptions();
        string uniqueID = this.UniqueID;
        if (uniqueID != null && (postBackOptions == null || postBackOptions.TargetControl == this))
        {
            writer.AddAttribute(HtmlTextWriterAttribute.Name, uniqueID);
        }
        writer.AddAttribute(HtmlTextWriterAttribute.Value, this.Text);
        bool isEnabled = base.IsEnabled;
        string empty = string.Empty;
        if (isEnabled)
        {
            empty = Util.EnsureEndWithSemiColon(this.OnClientClick);
            if (base.HasAttributes)
            {
                string item = base.Attributes["onclick"];
                if (item != null)
                {
                    empty = string.Concat(empty, Util.EnsureEndWithSemiColon(item));
                    base.Attributes.Remove("onclick");
                }
            }
            if (this.Page != null)
            {
                string postBackEventReference = this.Page.ClientScript.GetPostBackEventReference(postBackOptions, false);
                if (postBackEventReference != null)
                {
                    empty = Util.MergeScript(empty, postBackEventReference);
                }
            }
        }
        if (this.Page != null)
        {
            this.Page.ClientScript.RegisterForEventValidation(postBackOptions);
        }
        if (empty.Length > 0)
        {
            writer.AddAttribute(HtmlTextWriterAttribute.Onclick, empty);
            if (base.EnableLegacyRendering)
            {
                writer.AddAttribute("language", "javascript", false);
            }
        }
        if (this.Enabled && !isEnabled)
        {
            writer.AddAttribute(HtmlTextWriterAttribute.Disabled, "disabled");
        }
        base.AddAttributesToRender(writer);
    }

    protected virtual PostBackOptions GetPostBackOptions()
    {
        PostBackOptions postBackOption = new PostBackOptions(this, string.Empty);
        postBackOption.ClientSubmit = false;
        if (this.Page != null)
        {
            if (this.CausesValidation && this.Page.GetValidators(this.ValidationGroup).Count > 0)
            {
                postBackOption.PerformValidation = true;
                postBackOption.ValidationGroup = this.ValidationGroup;
            }
            if (!string.IsNullOrEmpty(this.PostBackUrl))
            {
                postBackOption.ActionUrl = HttpUtility.UrlPathEncode(this.ResolveClientUrl(this.PostBackUrl));
            }
            postBackOption.ClientSubmit = !this.UseSubmitBehavior;
        }
        return postBackOption;
    }

    protected virtual void OnClick(EventArgs e)
    {
        EventHandler item = (EventHandler)base.Events[Button.EventClick];
        if (item != null)
        {
            item(this, e);
        }
    }

    protected virtual void OnCommand(CommandEventArgs e)
    {
        CommandEventHandler item = (CommandEventHandler)base.Events[Button.EventCommand];
        if (item != null)
        {
            item(this, e);
        }
        base.RaiseBubbleEvent(this, e);
    }

    protected internal override void OnPreRender(EventArgs e)
    {
        base.OnPreRender(e);
        if (this.Page != null && base.IsEnabled)
        {
            if ((!this.CausesValidation || this.Page.GetValidators(this.ValidationGroup).Count <= 0) && string.IsNullOrEmpty(this.PostBackUrl))
            {
                if (!this.UseSubmitBehavior)
                {
                    this.Page.RegisterPostBackScript();
                }
            }
            else
            {
                this.Page.RegisterWebFormsScript();
                return;
            }
        }
    }

    protected virtual void RaisePostBackEvent(string eventArgument)
    {
        base.ValidateEvent(this.UniqueID, eventArgument);
        if (this.CausesValidation)
        {
            this.Page.Validate(this.ValidationGroup);
        }
        this.OnClick(EventArgs.Empty);
        this.OnCommand(new CommandEventArgs(this.CommandName, this.CommandArgument));
    }

    protected internal override void RenderContents(HtmlTextWriter writer)
    {
    }

    private void System.Web.UI.IPostBackEventHandler.RaisePostBackEvent(string eventArgument)
    {
        this.RaisePostBackEvent(eventArgument);
    }

    [WebCategory("Action")]
    [WebSysDescription("Button_OnClick")]
    public event EventHandler Click;
    [WebCategory("Action")]
    [WebSysDescription("Button_OnCommand")]
    public event CommandEventHandler Command;
}
Bui Cuong
  • 149
  • 2
  • I've already demonstrated a scenario in which the default ASP.NET button handling is inadequate in the post above. This implementation already fails to address using OnClientClick together with a validation group. – lsuarez Feb 27 '12 at 15:30