AutoPostBack Event On Control In ITemplate Fires Twice

This was an ASP.Net bug that a colleague of mine requested help with a little while ago.  The page in question had a data grid that was being provided with a custom implementation of ITemplate for one of it's columns.  Inside this template was a check box with AutoPostBack set to true and a server side event that performed some processing.

When the user clicked one of the check boxes the server side event was fired as expected.  When the user clicked on a second check box the event would fire twice meaning that the server side processing had now been performed three times in total!  As it turns out the event would fire once for each check box in the grid that was currently set to checked, not an expected behaviour and certainly not a desired one.  Initially this seems like a pretty bizarre thing for ASP.Net to do but once you understand a few details of the page life cycle it actually has a reasonable explanation.

Consider the following example code behind which exhibits the behaviour described:

    public partial class _Default : Page

    {

        protected void Page_Load(object sender, EventArgs e)

        {

            int[] data = new[] { 1, 2, 3 };

 

            repeater.ItemTemplate = new TestTemplate();

            repeater.DataSource = data;

            repeater.DataBind();

        }

    }

 

    class TestTemplate : ITemplate

    {

       public void InstantiateIn(Control container)

        {

            CheckBox checkBox = new CheckBox();

            checkBox.ID = "checkBox";

            checkBox.Text = "Check Me";

            checkBox.AutoPostBack = true;

            checkBox.CheckedChanged += new EventHandler(checkBox_CheckedChanged);

 

            container.Controls.Add(checkBox);

        }

 

        void checkBox_CheckedChanged(object sender, EventArgs e)

        {

            HttpContext.Current.Response.Write("Event fired!\n");           

        }

    }

The following sequence of images shows the user loading the page for the first time, selecting the first checkbox and selecting a second checkbox:

CheckBox1

CheckBox2

CheckBox3

The first hint as to what is wrong with the code is that there is no check for IsPostBack in the page load event.  This means that the repeater is being rebound every time and that all of the data being stored in the viewstate is being blown away and recreated.  This is a classic problem that every ASP.Net programmer makes at some point.  Adding a check so that the data is only bound when the page first loads actually causes the data to stop displaying at all after the first event is fired, what's going on?

Our next clue comes courtesy of the Visual Studio debugger.  The following breakpoint is being hit just after the user clicks the first checkbox, note the line highlighted in blue:

Debugger

Here we are recreating the checkbox inside the page load event.  The strange thing is that by the time the page load event is executed we would normally expect checkboxes to already be available with their updated values set and ready to use.  The other issue is that when we add the IsPostBack check this line of code is never run which means we are never recreating the checkboxes.  Shouldn't the repeater know how to recreate the controls because it has their details stored in viewstate?

This missing link here is that loading of viewstate happens earlier in the page life cycle than the page load event.  The repeater is trying to load the postback data but at that stage in the request we have not yet set ItemTemplate to tell it how to recreate the controls.  The solution is to set the ItemTemplate before viewstate is loaded which gives us the following:

    public partial class _Default : Page

    {

        protected void Page_Init(object sender, EventArgs e)

        {

            repeater.ItemTemplate = new TestTemplate();

        }

 

        protected void Page_Load(object sender, EventArgs e)

        {

            if (!IsPostBack)

            {

                int[] data = new[] { 1, 2, 3 };

 

                repeater.DataSource = data;

                repeater.DataBind();

            }

        }

    }

Now the page behaves as expected.  Taking a look in the debugger we can see that the checkboxes are being instantiated in an entirely different part of the page life cycle, before the viewstate is loaded and the page load event is fired:

Debugger2

As a general rule I would suggest that unless you have a specific need, it will be simpler and more reliable to specify your template in the ASPX file than create it by implementing ITemplate in code.  If the template is there in the ASPX with the repeater you will not have any timing issues where the template is not loaded.

If you do find you need to set templates programmatically you will need to have a better understanding of the stages each request goes through.  A typical scenario would be swapping in different templates for the repeater depending on what data is loaded and bound to it.

Labels: ,

04/09/2008 05:33 AM (UTC -07:00)