Monday, October 1, 2007

ImageButton Control nested in a GridView Control throws EventValidation error.

Ever try to include an ImageButton control in your gridview and then end up with the following beautiful error message :

Invalid postback or callback argument. Event validation is enabled using <pages enableEventValidation="true"/> in configuration or <%@ Page EnableEventValidation="true" %> in a page. For security purposes, this feature verifies that arguments to postback or callback events originate from the server control that originally rendered them. If the data is valid and expected, use the ClientScriptManager.RegisterForEventValidation method in order to register the postback or callback data for validation.

Thats right, its an EventValidation issue. This also occurs only with an ImageButton nested in a gridview. Moreover, you must be calling DataBind on the gridview if its a postback as well. Code speaks a thousand words, so lets look an example when this could occur :
protected void Page_Load(object sender, EventArgs e)
{
  //if (!IsPostBack)
  //{
   BindGrid();
  //}
 }

 private void BindGrid()
 {
  ArrayList al = new ArrayList();
  al.Add("a");
  al.Add("b");
  al.Add("c");
  GridView1.DataSource = al;
  GridView1.DataBind();
}

protected void ImageButton1_Click(object sender, EventArgs e)
{
}
</script> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Untitled Page</title> </head> <body> <form id="form1" runat="server"> <div> <asp:GridView ID="GridView1" runat="server"  OnRowDataBound="GridView1_RowDataBound"> <Columns> <asp:TemplateField> <ItemTemplate> <asp:ImageButton ID="ImageButton1"  OnCommand="ImageButton1_Click" runat="server" /> </ItemTemplate> </asp:TemplateField> </Columns> </asp:GridView> </div> </form> </body> </html>
As you can see from the following sample code, we are rebinding our grid even on a postback, this means when the user posts back via our ImageButton, the grid will get rebound again. When this happens the gridview will recreate the gridview Vs pulling values from viewstate. By calling DataBind again in the page_load method, the Gridview recreates the child controls, and by the time our page gets to the Postback event handling phase, RaisePostBackEvent is fired in our imagecontrol(since it implements the IPostBackEventHandler interface).

RaisePostBackEvent is what calls the ValidateEvent method, with two arguments. 1st argument is the UniqueID of our ImageButton and the second argument is a string argument. The issue here is with the UnqiueID argument. It has the id of the wrong control. How, why, I dont know exactly. I just know that by the time the ImageControl reaches the render phase, it has the correct uniqueID again. Since the gridview is a templated control, it prefixes the control with a unique namespace which happens to be the rowID of the current row which gets appeneded along with the gridview's id. Its this row id going wrong somehow only during RaisePostBackEvent. This happens only with the gridview too. I have tested with a repeater for eg and this worked correctly without issues. The 3-4 possible solutions I can think of are :

1. Put an IsPostBack check and only bind the gridview if its a postback. This means the gridview will be repopulating from viewstate which obviously does not have this wrong rowid issue. So if you remove the comments from our previous code, your set. But not always populating from viewstate might be an option.

2. You can try setting a unique id for the row generated by the ItemTemplate in our gridview. YOu can try this in either the RowCreated method or the RowDataBound method. In this manner we do not depend anymore on INamingContainer generating a uniqueID for the row, which in turn gets prefixed on our control since the row is now the direct NamingContainer and the problem is solved, eg :

protected void GridView1_RowCreated(object sender, GridViewRowEventArgs e)
{
    if (e.Row.RowType == DataControlRowType.DataRow)
        e.Row.ID = e.Row.RowIndex.ToString();
} 


and a 3rd way to work around this problem is to rebind the gridview at a later stage, a stage after RaisePostBackEvent has finished executing. Since EventValidation is called during this stage, its safe to rebind after this call. Meaning for eg, in the OnCommand/onclick event of our ImageButton or in the RowCommand method of the gridview. In short, calling rebind on the grid after the Postback event handling phase will resolve your issues. Also note that this issue is there only with the ImageButton, the same does not happen if you used a LinkButton for eg, or a Button control.

And yet a 4th way to work around this is to use a LinkButton control and nest an image in it making it behave like an ImageButton.

So why does this happen only with the ImageButton control ? This is because the ImageButton implements IPostBackDataHandler interface. Image elements unlike other form elements include the x,y co-ordinates location in the image where you clicked. In order to detect this change and raise an event, the ImageButton control needs to provide implementation for LoadPostData method. Also this is the same place where a call is being made for RegisterRequiresRaiseEvent. RegisterRequiresRaiseEvent is also the cause of the issue here.

Without this call we have no problems with the uniqueID of the ImageControl, which is what made EventValidation to choke because the control was registered for EventValidation with one ID and then checked for Validation after validation with another ID. For me this unusual behaviour is a bug. We can make a simple test as the code that follows and the output of that code is not surprisingly the following and in this order :
  1. LoadPostData: GridView1:_ctl2:ImageButton1
  2. RaisePostBackEvent: GridView1:_ctl4:ImageButton1 
  3. AddAttributesToRender: GridView1:_ctl2:ImageButton1
Notice how in RaisePostBackEvent, the id of the control has changed magically and is now GridView1:_ctl4:ImageButton1. RaisePostBackEvent is also the method that registers the control for EventValidation. Notice that in a later phase AddAttributesToRender, the control id is back again to what it was, and this is also asif by magic! For me this is a bug. Also the cause is none other than the call being made to RegisterRequiresRaiseEvent from within LoadPostData, which you can test by following the inline comments i have included in the LoadPostData method.

Following is the code to verify this behaviour :

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls; 
/// <summary>
/// Summary description for CustomImageButton
/// </summary>
[SupportsEventValidation]
public class CustomImageButton : ImageButton
{
 public CustomImageButton()
 {
  //
  // TODO: Add constructor logic here
  //
  
 } 

 protected override bool LoadPostData(string postDataKey, 
 System.Collections.Specialized.NameValueCollection postCollection)
 {
  HttpContext.Current.Response.Write("<br/>LoadPostData: " + 
                         this.UniqueID);
  return base.LoadPostData(postDataKey, postCollection);
  // The reason this control chokes is because of 
  // the call being made to RegisterRequiresRaiseEvent.
  // To test this, first commentout the call to 
  // return base.LoadPostData(postDataKey, postCollection); 
                // above and then
  // remove the following comments below and and try it. 
  // It will work without issues!
         /*  if (this.Page != null)
  {
   this.Page.RegisterRequiresRaiseEvent(this);
  } 
  return false;*/
 }

 protected override void RaisePostBackEvent(string eventArgument)
 {
  HttpContext.Current.Response.Write("<br/>RaisePostBackEvent: " + 
                        this.UniqueID);
  // base.RaisePostBackEvent is what actually registers this 
                // control for EventValidation
  // so dont call it in order to test this unexpected 
                // behaviour, otherwise, event
  // validation will occur and you will get an error ofcourse :D
  // base.RaisePostBackEvent(eventArgument);
  
 }
 protected override void AddAttributesToRender(HtmlTextWriter writer)
 {
  HttpContext.Current.Response.Write("<br/>AddAttributesToRender: " + 
      this.UniqueID);
     base.AddAttributesToRender(writer);
 }
}

5 comments:

  1. Changes IDs on OnRowDataBound is a better and short way - working with ajax too.

    But if we bind any data to control in any "repeatable" template control, i think to use Control.OnDataBinding will be better.

    ReplyDelete
  2. That helps me!!! Thank you a lot!

    ReplyDelete
  3. I think you're a Guru Sir, not only did you give us the solution to the problem but also explanations on how this bug occurs. I've been searching the net for like 10 hours trying to fix this issue after migrating our site from 1.1 to 2.0. The second option worked for me. Thanks a lot!

    ReplyDelete
  4. oh man, thanks a lot, this article absolutely solve my problem. be happy in your times.

    ReplyDelete
  5. Thanks a lot, the best solutions I ever found.

    ReplyDelete