Friday, May 06, 2005

ASP.NET Repeater Woes

This week, I have spent a good deal of time learning the inner-workings of the Repeater web control of ASP.NET. I have also spent a good deal of time being frustrated with it. So, to hopefully save some others some time, I have decided to post my findings.

I have found that there is a lot of information on creating and working with Repeaters when you are doing things in a typical form. By typical, I mean, you create a webform page (mypage.aspx for instance), create all the fun html anf asp.net tags jsut so so, and then use the code-behind page to fill in the data and catch events, etc, etc etc. However, there is very little good information on dynamically creating a repeater, its templates, and populating its datasource, and maintaining all of that through post-backs.

So here's my scenario... I wanted to create a repeater control dynamically when my page loaded, complete with custom header, custom item, custom alternating item, and custom footer, and I wanted to create all of this dynamically. Also, in each item, and alternating item, I wanted to have image buttons that could be clicked, and I wanted to catch those click events, and do some more processing of the data. Not every row, however would have these buttons. The details as to why I wanted this are not really important, so I'll leave those out.

Everything seemed to be fine... at first. My page loaded, the repeater was populated with the data, and all appeared just as I expected. Then I clicked one of my buttons. The page posts-back, and poof, my data is gone, and I didn't catch the event from the repeater. So, I started pouring through google looking for an answer. I found a lot of good information, but nothing that described my scenario exactly.

So, like most hackers I began to tinker with my code... and tinker, and tinker. Nothing I tried seemed to work. So, come the end of my day at work, I decided to go for a run, and come back to it later.

After my run, I had an epiphany. I created a simple test of the conventional way of putting a repeater on a web page. Here's what I had so far:

"webform1.aspx"
<table>
<asp:Repeater id="Repeater1" runat="server">
<ItemTemplate>
<tr>
<td><%#Container.DataItem%></td>
<td><asp:button ID="button1" Runat="server" CommandName="cmd" CommandArgument="1"></asp:button></td>
</tr>
</ItemTemplate>
</asp:Repeater>
</table>

"webform1.aspx.cs"
private void Page_Load(object sender, EventArgs e)
{
if (!this.IsPostBack)
{
ArrayList data = new ArrayList();

for(int i = 0; i < 10; i++)
{
data.Add(i.ToString());
}

this.Repeater1.DataSource = data;
this.Repeater1.DataBind();
}

}


private void Repeater1_ItemCommand(object sender, ItemCommandEventArgs e)
{
((ImageButton)sender).Enabled = false;
}

So I did that, and everything worked as expected. I caught my events as expected, and the data remained through the post-back. So I thought, this is doing exactly what I am doing when I create the control dynamically... the only difference is that the control hierarchy is defined in my aspx page. So what's the deal?

Some more pondering, and it came to me. I was creating the controls WITH the dynamically rendered parts from the data in my item template class. It seemed that the conventional way was doing this too, but I thought, maybe it's not. Let me try creating the controls, as placeholders, if you will, and then when the repeater binds its data in the ItemDataBound event, then update the control to reflect the dynamically generated parts. BINGO! It worked. Here's what I had at this point:

"webform1.aspx"
<table>
<asp:Repeater id="Repeater1" runat="server">
<ItemTemplate>
<tr>
<td><%#Container.DataItem%></td>
<td><asp:button ID="button1" Runat="server" CommandName="cmd" CommandArgument="1"></asp:button></td>
</tr>
</ItemTemplate>
</asp:Repeater>
</table>
<asp:PlaceHolder ID="ph" runat="server"></asp:PlaceHolder>

"webform1.aspx.cs"
private Repeater results;

private void Page_Load(object sender, System.EventArgs e)
{
results = new Repeater();
results.ItemTemplate = new RepeaterTemplate();
results.ItemDataBound += new RepeaterItemEventHandler(results_ItemDataBound);
ph.Controls.Add(results);

if (!this.IsPostBack)
{
ArrayList data = new ArrayList();
for(int i = 0; i < 10; i++)
{
data.Add(i.ToString());
}

this.Repeater1.DataSource = data;
this.Repeater1.DataBind();

results.DataSource = data;
results.DataBind();

}
}

private void results_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
ImageButton b = (ImageButton)e.Item.FindControl("button");
b.Text = e.Item.DataItem.ToString();
}

class RepeaterTemplate : ITemplate
{
public void InstantiateIn(Control container)
{
ImageButton button = new LinkButton();
button.ID = "button";
container.Controls.Add(button);
button = null;
}
}

Now, my page loaded, the repeater got its data, and when I clicked the buttons, the page posted back, and the data all seemed to be intact. Now, though, how was I going to link my events up?

Normally when you add a button to a repeater, the event bubbles up the repeater, and you capture the event in the ItemCommand event. But could I just assign an event handler to the button and handle it that way? And how would this work for all of the buttons? Well, i need to create the event handler, and assign it to the button, but where should I do this? I knew that the ItemDataBound event only occurs when the repeater is bound to the data source. This only happens once, and I need this to happen every time the page is loaded, post-back or not. So, I have to hook it up in the ItemCreated event. This happens everytime the page loads. So, I have this now:

"webform.aspx.cs"
private void Page_Load(object sender, System.EventArgs e)
{
results = new Repeater();
results.ItemTemplate = new RepeaterTemplate();
results.ItemDataBound += new RepeaterItemEventHandler(results_ItemDataBound);
ph.Controls.Add(results);

if (!this.IsPostBack)
{
ArrayList data = new ArrayList();
for(int i = 0; i < 10; i++)
{
data.Add(i.ToString());
}

this.Repeater1.DataSource = data;
this.Repeater1.DataBind();

results.DataSource = data;
results.DataBind();

}
}

private void results_ItemCreated(object sender, RepeaterItemEventArgs e)
{
ImageButton b = (ImageButton)e.Item.FindControl("button");
b.Click += new EventHandler(b_Click);

}

private void b_Click(object sender, EventArgs e)
{
((LinkButton)sender).Text = ((LinkButton)sender).CommandArgument;
}

Now, one more piece... I need to distinguish which button was clicked. I thought that I could simply use the CommandArgument property on the ImageButton, and, sure enough, that worked. Here's the final piece of code:

"webform1.aspx.cs"
private void Page_Load(object sender, System.EventArgs e)
{
results = new Repeater();
results.ItemTemplate = new RepeaterTemplate();
results.ItemDataBound += new RepeaterItemEventHandler(results_ItemDataBound);
ph.Controls.Add(results);

if (!this.IsPostBack)
{
ArrayList data = new ArrayList();
for(int i = 0; i < 10; i++)
{
data.Add(i.ToString());
}

this.Repeater1.DataSource = data;
this.Repeater1.DataBind();

results.DataSource = data;
results.DataBind();

}
}

private void results_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
LinkButton b = (LinkButton)e.Item.FindControl("button");
b.CommandArgument = e.Item.DataItem.ToString();
}

So, hopefully this will help some other poor soul hacking away with the Repeater, and getting nowhere.



No comments: