Repeater looses its data collection. ViewState problem?

A while back I experienced a really strange problem in one of our ASP.net applications, namely my repeater control which was loosing its data collection after a postback to the server. I got some minutes of time and thought to quickly post about the problem for you guys out there. I created a small demo app which reproduces the problem very clearly.

Consider the following ASPX page:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="RepeaterViewstateProblem._Default" %>
<%@ Register src="Controls/RecursiveIterateControl.ascx" tagname="RecursiveIterateControl" tagprefix="uc1" %>
<!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="form1" runat="server">
<div>
<h2>List of Persons</h2>
<asp:Repeater ID="repeaterPersons" runat="server">
<HeaderTemplate>
<ul>
</HeaderTemplate>
<ItemTemplate>
<li><%# Eval("Firstname") %> <%# Eval("Surname") %></li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</asp:Repeater>

<asp:Button ID="buttonSubmit" runat="server" Text="Provocate a postback!"
onclick="buttonSubmit_Click" />
</div>
</form>
</body>
</html>
It's quite simple. There is a Repeater control and a button for creating a postback to the server. In the codebehind I write the following
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace RepeaterViewstateProblem
{
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
repeaterPersons.DataSource = CreateDummyPersonList();
repeaterPersons.DataBind();
}
}

private IList<Person> CreateDummyPersonList()
{
return new List<Person>{
new Person { Firstname = "Juri", Surname="Strumpflohner"},
new Person { Firstname = "Jack", Surname="XYZ"}
};
}

protected void buttonSubmit_Click(object sender, EventArgs e)
{

}
}

class Person
{
public string Firstname { get; set; }
public string Surname { get; set; }
}
}
I have a dummy class Person which is used for data binding. For that purpose I create a simple collection of Person objects which are being bound to the Repeater control. I'm doing this just once, when a GET request is done to the page. During POSTs the ASP.net ViewState mechansim will keep the bound data collection for me. Running the example, you should get the following output:



When clicking on the button, a postback to the server will be done and the ViewState will still maintain the elements of your Repeater control. So everything fine till now.

A functionality which is often needed in web apps is to dynamically search for control instances at runtime when you just have the corresponding IDs. This can be achieved through the Control.FindControl(...) method, but often I found that you have the need for a recursive version which traverses the Control hierarchy starting from a certain root. I've implemented such a recursive method. There's nothing bad with this except that you have to pay attention which root you provide such that you don't traverse the whole hierarchy which, on large sites, may cause some performance problems. So let's implement such a recursive find in a UserControl which is then placed on the main page I've shown above.
RecursiveIterateControl.ascx.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Collections;

namespace RepeaterViewstateProblem.Controls
{
public partial class RecursiveIterateControl : System.Web.UI.UserControl
{
public string ControlIDToSearch { get; set; }

protected override void OnInit(EventArgs e)
{
Control ctrl = FindControlRecursive(this.Page, ControlIDToSearch);
}

/// <summary>
/// Recursive version of the FindControl method.
/// </summary>
/// <param name="parent">Parent to where to start the search for the control</param>
/// <param name="controlId">ID of the control to search for</param>
/// <returns></returns>
private Control FindControlRecursive(Control parent, string controlId)
{
if (controlId == parent.ID)
return parent;

foreach (Control ctrl in parent.Controls)
{
Control tmp = FindControlRecursive(ctrl, controlId);
if (tmp != null)
return tmp;
}

return null;
}
}
}
On the main page I include the UserControl and pass it the ID of - say - the button "buttonSubmit":
<uc1:RecursiveIterateControl ID="RecursiveIterateControl1" ControlIDToSearch="buttonSubmit" 
runat="server" />

Now re-run the application and hit the button to launch the postback. You'll notice that your repeater is empty, getting the following screen:

So why did this happen? The only thing we have added is the method for recursively iterating through the control hierarchy which doesn't contain any objectionable code. I'll tell what the problem is.

There are a few things here you have to consider. First of all about the ASP.net page lifecycle and how the ViewState is being handled. The following diagram shows it:

Note, the ViewState is deserialized and loaded back on the controls after the OnInit and before the PageLoad event. Actually the FindControlRecursive() is called during the OnInit event but we are doing nothing with the ViewState, so that should be fine.

The next thing I looked at was at the Repeater code directly using the Reflector. The only thing that's being accessed on the Repeater is its Control property when it is being traversed during the execution of the FindControlRecursive. The Repeater control actually overrides that property and defines it as follows:
public override ControlCollection Controls
{
get
{
this.EnsureChildControls();
return base.Controls;
}
}

The "EnsureChildControls()" will trigger a call to the CreateChildControls() which is usually overridden by composite server controls. If you continue to search that on the Repeater control you get this code...
protected internal override void CreateChildControls()
{
this.Controls.Clear();
if (this.ViewState["_!ItemCount"] != null)
{
this.CreateControlHierarchy(false);
}
else
{
this.itemsArray = new ArrayList();
}
base.ClearChildViewState();
}
...and here is the problem. According to the ASP.net page lifecycle we know that the ViewState will be loaded after the OnInit event of the page. However our call of the FindControlRecursive(..) method inside the OnInit will trigger down till the CreateChildControls() method of the Repeater control is being called. And since the ViewState is not yet loaded back, the condition (this.ViewState["_!ItemCount"] != null) will evaluate to false with the effect that a new empty collection is being created and the ViewState of the childs is being cleared. In fact, every call to the "Control" property of the Repeater during a state in the page lifecycle where the ViewState has not yet been loaded back, will result in loosing all of its values.

Now imagine you notice this problem in a big web app, probably days or even weeks after you or even someone else in the team has added that FindControlRecursive(..) call somewhere in the codebase. It actually took me two days to figure out this problem!

As a solution you may pay attention to just call the FindControlRecursive(..) method during the PageLoad event or later. Alternatively you could implement a SafeFindControlRecursive(..) which takes care of this problem. This could look like the following:
private Control SafeFindControlRecursive(Control parent, string controlId)
{
if (controlId == parent.ID)
return parent;

foreach (Control ctrl in GetControlCollection(parent))
{
Control tmp = SafeFindControlRecursive(ctrl, controlId);
if (tmp != null)
return tmp;
}

return null;
}

private ICollection GetControlCollection(Control parent)
{
if (parent is Repeater)
{
return new Control[0];
}
else
{
return parent.Controls;
}
}
This takes care of not accessing the Control collection of a Repeater control which is normally also not needed to be traversed.

You can download the Visual Studio project showing this problem here.
Kindle

Comments

0

Your ad here?