Nested repeaters - different techniques and general tips - part 1.

Example source code for this article can be downloaded from here

Please send any comments here - please be constructive, I'm trying to learn how to write this stuff!

Currently updating this, I plan to have the new version up within the next few days...

So first thing's first, what is a "Repeater" and why would you want to nest it?


As you're probably aware, ASP.NET ships with three main tabular data display controls; the DataGrid, the DataList and the Repeater, these controls are each suited for different tasks and have their own advantages and disadvantages (for instance see here for an nice comparison).In short, the Repeater does the least for you, it just does what it says, repeats it's content and allows that content to be bound to data.


The repeater control is really handy when you need to have total control over the exact HTML your page outputs (for instance when you have designers who seem to think that 1 pixel is important), it is also the quickest of the three tabular data controls which ships with ASP.NET - mainly because it does the least for you...

'Nesting' means showing one Repeater embedded within another, a typical use for these is to show hierarchical data like Forums or calendar applications; so say you had a calendar application where you wanted to show a list of all the days in a week..then all the appointments during that day, one way to do this in ASP.NET would be to have 'Nested Repeaters' .

So, how do you get a Repeater control to show some content - simple, define the Header, Item and Footer templates- the Item template will then 'repeat' for each row of data you bind to the control.

For example, the code below defines a Repeater with the id Repeater1 - in that we have a simple HeaderTemplate and FooterTemplate which just start and finish a table. The ItemTemplate for this Repeater contain an Image control and three databound items, CompanyName, ContactTitle and ContactName - these will just output the items with these names contained in the data which was bound to the repeater.

< asp:Repeater id ="Repeater1" runat ="server">

         < HeaderTemplate >

           < table >

             < tr >

               < td ></ td >

               < td ></ td >

               < td ></ td >

               < td ></ td >

             </ tr >

         </ HeaderTemplate >

         < ItemTemplate >

           < tr >

             < td >< asp:Image ID ="CompanyNameImage" Runat ="server"></ asp:Image ></ td >

             < td > <% # ((DataRowView)Container.DataItem)["CompanyName"] %> </ td >

             < td > <% # ((DataRowView)Container.DataItem)["ContactTitle"] %>

              <% # ((DataRowView)Container.DataItem)["ContactName"] %>

             </ td >

             < td ></ td >

             < td ></ td >

           </ tr >

         </ ItemTemplate >

         < FooterTemplate >

           </ table >

         </ FooterTemplate >

       </ asp:Repeater >

A word about my freaky DataBinding syntax

You might have noticed that I use a databinding syntax which is a bit different to that used in most documentation, the DataBinder.Eval syntax - e.g., <%# DataBinder.Eval(Container.DataItem, "Price", "{0:c}") %> , instead I tend to use this syntax <%# string.Format("{0:c}",((DataRowView)Container.DataItem)["Price"])%> - I've covered my reasons for this in the past and well, I just prefer it...

Though I use Repeaters throughout this article, these techniques work equally as well for the DataGrids and DataLists as well!

All three of these controls share some common events, two of which we'll use in this article:

  1. ItemCommand - this is an event which is fired when a button / other object fires an event when that object is contained within the control, so if you click a button inside a DataList, the DataList's ItemCommand event will fire.
  2. ItemDataBound - this event fires immediately after the row's data is bound on to this particular item, at this point, all the controls within the item are accessible by name, as it the data contained in the current row.

For this example we're going to use some data from the Northwind SQL Server database - this is a database which is installed by default with SQL Server, if's pretty realistic data and it's structure makes it suitable for 'nesting'. All the examples in this article will use the data provided by this SQL query: SELECT CustomerID, CompanyName, ContactName, ContactTitle FROM Customers;SELECT CustomerId, OrderID, OrderDate, ShipName, ShipCountry FROM Orders. As you can see, this query returns two resultsets from the DB, both containing the 'CustomerId' field.

Three main methods exist for handling nested list controls:

  1. Events
  2. Member Methods
  3. Declarative

Events

As data list controls attach rows of data on to the RepeaterItems in that control they fire 'events' which let us know what the control's doing and then let us access that RepeaterItem and manipulate it.

So, say I want to access an Image webcontrol which sits inside an RepeaterItem of a Repeater at the point at which data is being bound on to that RepeaterItem. For the Repeater defined above, we can attach a handler on to the ItemDataBound event, this will let us perform some action when that event fires for each RepeaterItem in the control. The most common ways of hooking into this event are to either use the declarative syntax in the definition of the Repeater;

            <asp:Repeater id="Repeater1" runat="server" OnItemDataBound="Repeater1_ItemDataBound">

What this actually does is hook up the OnItemDataBound event to the specified handler (Repeater1_ItemDataBound) in the class which the ASP.NET compiler makes from the page - this is the reason that the handler method cannot be 'Private' when using this method - because if it is, the derived class (the class which ASP.NET makes from your page) can't invoke that method.

The second method has you hook up your handler to the event in code-behind, so in this example the following line hooks up the event;

this.Repeater1.ItemDataBound += new System.Web.UI.WebControls.RepeaterItemEventHandler(this.Repeater1_ItemDataBound);


Actually, you don't really have to add this line at all if you're using Visual Studio .NET. To add an event handler in Visual Studio.NET, go to the Designer view, select the Repeater and in the Properties window click on the little lightning icon (see below), here you can see all the events exposed by the Repeater, to hook up and create an empty event handler, you just click on the text box next to the 'ItemDataBound' event then just click 'enter'.

So, now you have an empty event handler - what next?

In this example, I'm going to hook up some data to the Repeater then modify the url which the ASP:Image control uses as the source of it's image to be an HttpHandler which creates a gif file containing the company name - or some other text if the name is more that 30 characters long (click here to see an example of this page).

For this, we're going to use the Repeater defined in the code I showed above. My codebehind file just fills up a DataSet and binds it on to the Repeater - like so;

private void Page_Load(object sender, System.EventArgs e)
{
	if(!Page.IsPostBack)
    BindData();
}

private void BindData()
{
	SqlDataAdapter da = new SqlDataAdapter(sqlString,Global.ConnectionString);
  DataSet ds = new DataSet();
  da.Fill(ds);
  Repeater1.DataSource = ds;
  Repeater1.DataBind();
}

As you can see, I just call the method BindData from my page_Load event - I put in the usual if(!Page.IsPostback) which isn't strictly necessary but we'll cover why you may want to do this later. So, now we have this bunch of data bound on to the Repeater each time a row of this data is bound on to an item in the Repeater , a couple of events are fired, ItemCreated and the one we're interested in, ItemDataBound.

In the handler for ItemDataBound, we have to make sure that the RepeaterItem we're going to modify is actually the one we want to modify. The reason for this is that the ItemDataBound event actually fires for every defined template in the Repeater including the Header and Footer templates. We do this with the following code;

ListItemType lt = e.Item.ItemType;
if(lt == ListItemType.Item || lt == ListItemType.AlternatingItem)
{
...
}

So here we're basically saying - if this RepeaterItem is an 'Item' or 'AlternatingItem' then do this... this is important since the controls I'll be looking for are only defined in the 'ItemTemplate'..

Next, I want to access the Image control in that RepeaterItem;

System.Web.UI.WebControls.Image img = e.Item.FindControl("CompanyNameImage") as System.Web.UI.WebControls.Image; 
if(img != null)
{
...
}

Here, I define a control called 'img' as a reference to the Image control in the item.'e.Item' represents this particular RepeaterItem, so, 'e.Item.Controls' represents the ControlCollection containing all the items in the current RepeaterItem - so we can get to out Image control by doing a FindControl. I then cast that control to the correct type using the 'as' operator - this is safer than a '(Image) e.Item.FindControl("CompanyNameImage");' as we can then go on to test for the existence of the object (using the 'if(img != null)') before trying to use it - preventing a possible runtime error...which is nice!

So now we have access to the image control we need access to the data we want...here's how we do that...

DataRowView dv = e.Item.DataItem as DataRowView;
if(dv != null)
{
string cName = dv["CompanyName"] as string;
}

So, here we use the e.Item.DataItem - this contains the row of data being used to populate this RepeaterItem. As in the previous example, I just cast then test to ensure our object exists before I go on to access it. So, I pull the cName string from the row in the usual way just treating my object 'dv' just as you'd treat any row of data in code.

Oh, should mention, the DataRowView object will only be valid if you're using a DataSet or DataTable as your data source, if you're using an IDataReader (most typically SqlDataReader), you should use DbDataRecord from the System.Data.Common namespace - that goes for the definition of the repeater as well - so instead of <%# string.Format("{0:c}",((DataRowView)Container.DataItem)["Price"])%> you'd just use <%# string.Format("{0:c}",((DbDataRecord)Container.DataItem)["Price"])%>...

So, now I have my data, I have a reference to my control. I now want to set the ImageUrl of the control to the Url of my HttpHandler which will generate an image for me containing the company name - but if that's more than 30 characters, it'll put something else out...here's my ItemDataBound handler code;

 

  private void Repeater1_ItemDataBound( object sender, System.Web.UI.WebControls.RepeaterItemEventArgs e)

    {

      ListItemType lt = e.Item.ItemType;

       if (lt == ListItemType.Item || lt == ListItemType.AlternatingItem)

      {

        System.Web.UI.WebControls.Image img = e.Item.FindControl( "CompanyNameImage" ) as System.Web.UI.WebControls.Image;

         if (img != null )

        {

          DataRowView dv = e.Item.DataItem as DataRowView;

           if (dv != null )

          {

             string cName = dv[ "CompanyName" ] as string ;

             if (cName.Length > 30)

            {

              cName = "Too Long!" ;

              img.ImageUrl = "ImageMaker.ashx?CName=" + Server.UrlEncode(cName) + "&color=red" ;

            }

             else

              img.ImageUrl = "ImageMaker.ashx?CName=" + Server.UrlEncode(cName);

          }

        }

      }

    }

The code for ImageMaker.ashx is included in the examples archive.

So, nice and all, but how does this get us towards having nested repeaters...well...we're actually going to use the same approach, just this time out control which we reference in the handler will be our nested repeater...

So, first thing we'll do is change the page which you saw above to include a Repeater nested within the first; called 'NestedRepeater' in the code below;

            <asp:Repeater id="Repeater1" runat="server">
                <HeaderTemplate>
                    <table>
                        <tr>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                        </tr>
                </HeaderTemplate>
                <ItemTemplate>
                    <tr>
                        <td><asp:Image ID="CompanyNameImage" Runat="server"></asp:Image></td>
                        <td><%# ((DataRowView)Container.DataItem)["CompanyName"]%></td>
                        <td><%# ((DataRowView)Container.DataItem)["ContactTitle"]%>
                            <%# ((DataRowView)Container.DataItem)["ContactName"]%>
                        </td>
                        <td>
                            <asp:Repeater id="NestedRepeater" runat="Server">
                                <HeaderTemplate>
                                <table>
                                </HeaderTemplate>
                                <ItemTemplate>
                                    <tr>
                                        <td><%# ((DataRowView)Container.DataItem)["OrderDate"]%></td>
                                        <td><%# ((DataRowView)Container.DataItem)["OrderId"]%></td>
                                        <td><%# ((DataRowView)Container.DataItem)["ShipName"]%></td>
                                        <td><%# ((DataRowView)Container.DataItem)["ShipCountry"]%></td>
                                    </tr>
                                </ItemTemplate>
                                <FooterTemplate>
                                </table>
                                </FooterTemplate>
                            </asp:Repeater>
                        </td>
                        <td></td>
                    </tr>
                </ItemTemplate>
                <FooterTemplate>
                    </table>
                </FooterTemplate>
            </asp:Repeater>

So as you can see, all we've done here is add another Repeater inside one of the cells of the Repeater we had previously, then we have a couple of new items in that Repeater's cells...now to get the data to fill that in.
If you'll recall from the SQL query we defined previously, we had two resultsets coming back from SQL Server, these both had the column 'CustomerId'...so now we want to hook up those two result sets inside the DataSet. What the DataSet actually does when you give it a multiple resultset data source is to create multiple DataTables - one for each resultset.
We create a relationship between these two tables using...not surprisingly...a DataRelation. So, here's the BindData method we presented above, with the addition of a single line which adds the DataRelation;

        private void BindData()
        {
            SqlDataAdapter da = new SqlDataAdapter(sqlString,Global.ConnectionString);
            DataSet ds = new DataSet();
            da.Fill(ds);
            ds.Relations.Add(new DataRelation("CustomerOrders",ds.Tables[0].Columns["CustomerID"], ds.Tables[1].Columns["CustomerId"]));
            Repeater1.DataSource = ds;
            Repeater1.DataBind();
        }

Pretty simple right! So, now we have our nice DataSet with it's data relation, how do we go about populating the nested Repeater with that data? Here's our modified ItemDataBound handler;

        private void Repeater1_ItemDataBound(object sender, System.Web.UI.WebControls.RepeaterItemEventArgs e)
        {
            ListItemType lt = e.Item.ItemType;
            if(lt == ListItemType.Item || lt == ListItemType.AlternatingItem)
            {
                System.Web.UI.WebControls.Image img = e.Item.FindControl("CompanyNameImage") as System.Web.UI.WebControls.Image; 
                if(img != null)
                {
                    DataRowView dv = e.Item.DataItem as DataRowView;
                    if(dv != null)
                    {
                        string cName = dv["CompanyName"] as string;
                        if(cName.Length > 30)
                        {
                            cName = "Too Long!";
                            img.ImageUrl = "ImageMaker.ashx?CName=" + Server.UrlEncode(cName) + "&color=red";
                        }
                        else
                            img.ImageUrl = "ImageMaker.ashx?CName=" + Server.UrlEncode(cName);

                        Repeater nestedRepeater = e.Item.FindControl("NestedRepeater")  as Repeater;
                        if(nestedRepeater != null)
                        {
                            nestedRepeater.DataSource =     dv.CreateChildView("CustomerOrders");
                            nestedRepeater.DataBind();
                        }
                    }
                }
            }

So, as before, we get a reference to our control inside the ItemTemplate - just in this case, it's our 'NestedRepeater ' . So, we now have our Repeater now we need to get our data...that's what the 'dv.CreateChildView("CustomerOrders")' line is doing - this just creates a View based on the Relation we defined previously. We then set the DataSource of the nested repeater as that View then DataBind as normal.

So, that's it! We now have a nested repeater...you can see an example of what this actually outputs here - not very pretty admittedly...but you get the idea...

Still to come...other ways of doing this and other tips....