Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

A NestedRepeater Control for ASP.NET

3.79/5 (9 votes)
17 Nov 20076 min read 1   1.7K  
A server control, similar to the ASP.NET Repeater in its principles, that can handle recursive (or hierachical) data.

Introduction

Every time I have to use the ASP.NET Repeater control with recursive data, I feel frustrated. There's no easy way to use the power of databinding given by the Repeater control for such data, for instance, when I have to display a treeview where a node might have several "child" nodes with an undefined number of sublevels of data.

Of course, in a simple case, when there are only two or three levels of data, I can put as many Repeaters on my WebForm, create the adequate DataRelations in the data source and the work is done. But this solution is impossible to use if I can't tell in advance how many sublevels of data I'll have to display. And even if I could, this solution would not be elegant: the <ItemTemplate> sections of all these Repeaters are identical. There should be an easy way to declare a single control, with an <ItemTemplate> section that will be identical for all the data, and to ask this control to act differently in accordance with the sublevel of the current node. This is why I have decided to come up with a new control, which I've called NestedRepeater.

I wanted this control to support declarative syntax (i.e. declare an <ItemTemplate> section on my WebForm, and put all my controls in it), instead of being forced to use the code-behind. This control does not need to know in advance how many sublevels of data it must handle.

Background

Please note that the "How it Works" section of this article assumes that you're familiar with templated data-bound controls (see MSDN for further details).

How to Use the NestedRepeater

As an example for this document, I'll use the NestedRepeater to display the animal classification of the species. I want to visually render the hierarchy within this classification.

Data Source

For the animal classification that I intend to display, data comes from the following SQL table, called Animals:

There can be as many columns as necessary, but the three columns seen here are the only ones required by NestedRepeater (note that you can give whatever name you want to these columns as you will "DataBind" the data, as you would do with any other control):

  • ANI_ID: the primary key, NOT NULL
  • ANI_NAME: the data that will be displayed
  • ANI_PARENT: the foreign key, which references ANI_ID

The NestedRepeater exposes a public property called DataSource:

C#
public virtual DataSet DataSource;

As you can see, DataSource is typed as DataSet and not as object.

You just do the data binding as usual:

C#
SqlCommand cmd = new SqlCommand();
SqlConnection cnx = new SqlConnection(myCnxString);
cmd.Connection = cnx;
cmd.CommandText = "select * from ANIMALS";
SqlDataAdapter da = new SqlDataAdapter(cmd);
DataSet ds = new DataSet();
da.Fill(ds,"ani");

Then, you create a DataRelation that reflects the Primary key/Foreign key relation:

C#
ds.Relations.Add(RelationName,
    ds.Tables[0].Columns["ANI_ID"],
    ds.Tables[0].Columns["ANI_PARENT"]);

At this time, the NestedRepeater needs two properties:

C#
protected NestedRepeater myRep;
myRep.RelationName = RelationName;
myRep.RowFilterTop = "ANI_PARENT is null";
  • myRep.RelationName is the name you gave to the DataRelation in the DataSet
  • myRep.RowFilterTop tells the NestedRepeater how to determine which records will be the top nodes. Here, the topmost nodes are the records where the column ANI_PARENT is null. In my SQL table, there are two topmost nodes: Invertebrates and Vertebrates

Optionally, you can use the DataMamber property to indicate the name of the table. If you don't, NestedRepeater assumes that the data is available in ds.Tables[0].

Then:

C#
myRep.DataSource = ds;
myRep.DataBind();

Nothing to explain here.

On the WebForm

To use the NestedRepeater on your WebForm, you must first register the assembly:

ASP.NET
<%@ Register tagprefix="meg" Namespace="WebCustomControls" 
                       Assembly="WebCustomControls"%>

If you use Visual Studio, you must add a reference to WebCustomControls.dll in your project for this line to work properly.

Then:

HTML
<meg:NestedRepeater id=myRep runat=server>
    <HeaderTemplate>
        This is the animal classification. <br>
    </HeaderTemplate>
    <FooterTemplate>
        The end.
    </FooterTemplate>
    <ItemTemplate>
        <img src="http://www.codeproject.com/pix.gif" height="10" 
            width="<%# (Container.Depth * 10) %>">
        <%# (Container.DataItem as DataRow)["ANI_NAME"]%>
    <br>
    </ItemTemplate>
</meg:NestedRepeater>

Here, Container is of type NestedRepeaterItem (this class is discussed later in the "How it Works" section), and it gives several details about the current context, that are vital to truly render a hierarchical view.

Container.Depth, for instance, tells us how deep we are in the hierarchy. At the topmost level, Container.Depth is 0. On the sublevel immediately lower, it's 1, then 2 etc. Here, in order to give the feeling of the hierarchy, I use the depth to put an (invisible) image whose width is proportionate to the depth. You check this property if you want to personalize the display in accordance with the current sublevel.

Container.DataItem is always typed as a DataRow, so we can cast directly instead of using the slower version with DataBinder.Eval(…). (Note that we must import the System.Data namespace.)

The <HeaderTemplate> and <FooterTemplate> sections work in the same way as in <asp:Repeater>. I don't use the feature in this example, but they both support databinding.

Then, the result should be as follows:

How it Works

The work is done in two functions:

CreateControlHierachy

C#
protected virtual void CreateControlHierarchy(bool createFromDataSource)
{
    int nbTopNodes = 0;
    DataView dv = null;

    // HeaderTemplate
    if (m_headerTemplate != null)
    // Do we have a <HeaderTemplate> section ?
    {
        NestedRepeaterHeaderFooter header = 
                  new NestedRepeaterHeaderFooter();
        m_headerTemplate.InstantiateIn(header);

        if (createFromDataSource)
            header.DataBind();

        Controls.Add(header);
    }

    // ItemTemplate
    if (createFromDataSource &&
        DataSource != null &&
        DataSource.Tables.Count != 0)
    {
        DataTable tbSource;

        if (DataMember != String.Empty)
            tbSource = DataSource.Tables[DataMember];
        else
            tbSource = DataSource.Tables[0];

        if (tbSource == null)
            throw new ApplicationException("No valid" + 
              " DataTable in the specified position.");

        /* When creating from the ViewState (on PostBack),
            * we'll need to know how many nodes
            * there are under each node. So, when creating
            * from the datasource, we store this 
            * information in m_lstNbChildren,
            * which we'll also save in the viewstate.
            * */
        m_lstNbChildren = new ArrayList(tbSource.Rows.Count);
        
        dv = new DataView(tbSource);
        
        if (m_rowFilterTop != String.Empty)
            dv.RowFilter = m_rowFilterTop;

        nbTopNodes = dv.Count;
        m_lstNbChildren.Add(nbTopNodes);
    }
    else
    {
        m_lstNbChildren = (ArrayList)ViewState["ListNbChildren"];
        m_current = 0;
        nbTopNodes = (int)m_lstNbChildren[m_current++];
    }

    NestedElementPosition currentPos;

    for(int i=0; i< nbTopNodes; ++i)
    {
        if (i==0 && i==nbTopNodes-1)
            currentPos = NestedElementPosition.OnlyOne;
        else if (i ==0)
            currentPos = NestedElementPosition.First;
        else if (i == nbTopNodes - 1)
            currentPos = NestedElementPosition.Last;
        else
            currentPos = NestedElementPosition.NULL;

        if(createFromDataSource)
            CreateItem(dv[i].Row, 0, currentPos);
        else
            CreateItem(null, 0, currentPos++);
    }

    if (createFromDataSource)
        ViewState["ListNbChildren"] = m_lstNbChildren;

    // FooterTemplate
    if (m_footerTemplate != null)
    {
        NestedRepeaterHeaderFooter footer = 
              new NestedRepeaterHeaderFooter();
        m_footerTemplate.InstantiateIn(footer);

        if (createFromDataSource)
            footer.DataBind();

        Controls.Add(footer);
    }

    ChildControlsCreated = true;
}

This function is called upon databinding (or, on PostBack, during ViewState loading). It creates the header and determines the number of top nodes. For each of these top nodes, it calls CreateItem. Finally, it creates the footer.

CreateItem

C#
private void CreateItem(DataRow row, int depth, NestedElementPosition pos)
{
    DataRow[] childRows;
    int nbChildren=0;

    if (m_itemTemplate != null)
    {
        NestedRepeaterItem item = new NestedRepeaterItem();

        if (row != null)
        {
            childRows = row.GetChildRows(RelationName);
            nbChildren = childRows.Length;
            m_lstNbChildren.Add(nbChildren);

            item.Position = pos;
            item.NbChildren = childRows.Length;
            item.Depth = depth;

        }
        else // we use the viewstate
        {
            nbChildren = (int)
                m_lstNbChildren[m_current++];
            childRows = new DataRow[nbChildren];
        }

        m_itemTemplate.InstantiateIn(item);
        Controls.Add(item);

        NestedRepeaterItemEventArgs args = 
            new NestedRepeaterItemEventArgs();
        
        args.Item = item;
        OnItemCreated(args);

        if (row != null)
        {
            item.DataItem = row;
            item.DataBind();
            OnItemDataBound(args);
        }

        // Recursive call
        NestedElementPosition currentPos;

        for(int i =0; i< nbChildren; ++i)
        {
            if (i==0 && i==nbChildren-1)
                currentPos = NestedElementPosition.OnlyOne;
            else if (i ==0)
                currentPos = NestedElementPosition.First;
            else if (i == nbChildren-1)
                currentPos = NestedElementPosition.Last;
            else
                currentPos = NestedElementPosition.NULL;

            if (row != null)
                CreateItem(childRows[i], depth + 1, currentPos);
            else
                CreateItem(null, depth + 1, currentPos);
        }
    }
}

This function instantiates an item template for each row of data found in the datasource:

C#
m_template.InstantiateIn(item);

m_template is of type ITemplate. It is the variable that backs the property:

C#
public virtual ITemplate  ItemTemplate
{
    get{return m_itemTemplate;};
    set{m_itemTemplate = value;};
}

When the WebForm is parsed by ASP.NET, the <ItemTemplate> section is "transformed" into a class that implements the ITemplate interface (this class is masked from us), and an instance of this class is affected to the ItemTemplate property of the NestedRepeater. This class owns all the controls declared by the WebForm developer inside this section. In my example, there's an <img> tag and a literal string. In the method InstanteIn (generated by .NET), .NET instantiates these two controls and adds them to the Controls property of item. The pseudo-code should look like this:

C#
// this code is generated by .NET. We don't see it
void InstantiateIn(Control container)
{
    HtmlImage img = new HtmlImage();
    // further initialisation here
    . . . 

    LiteralControl lit = new LiteralControl();
    // further initialisation here
    . . . 

    container.Controls.Add(img);
    container.Controls.Add(lit);
    . . .
}

When item.DataBind() is called, both expressions:

ASP.NET
<%# (Container.Depth * 10) %>

and

ASP.NET
<%# (Container.DataItem as DataRow)["ANI_NAME"]%>

are evaluated. We can raise the ItemDataBound event:

C#
OnItemDataBound(args);

item is of type NestedRepeaterItem. This class gathers the necessary information for this node:

C#
item.Position = pos;
item.NbChildren = childRows.Length;
item.Depth = depth;
item.DataItem = row;

With the Position property, a page developer can determine if the current node is the first child of its parent, the last one, or if it is the only one. Position is defined as an enum:

C#
public enum NestedElementPosition
{
    First,   // current record is the first child of the immediate parent
    Last,    // current record is the last child of the immediate parent
    OnlyOne, // current record is the only child of the immediate parent
    NULL     // None of the above
}

The NbChildren property indicates the number of immediate children to the current node.

The Depth property was already mentioned, and the DataItem property is the same as with other .NET controls. The DataItem property is always a DataRow.

Once the item is added to the Controls property of the NestedRepeater, CreateItem is called for each child node, with an incremented level.

As we use a recursive function, there can be as many levels of data as necessary: there's no need to know in advance how many sublevels we have. That is: we can add another sublevel in the SQL table, without changing or adding any single line of code.

Update

A new property called Items has been added to the NestedRepeater and to the NestedRepeaterItem classes. This allows to loop through the items programmatically as shown in the following:

C#
// a NestedRepeater called myRepeater has been declared elsewhere...
foreach(NestedRepeaterItem item in myRepeater.Items)
{
    DoSomething(item);
}

// DoSomething is a recursive function
private void DoSomething(NestedRepeaterItem current)
{
    // do whatever is required with the current item
    // * * * 
    
    // then call DoSomething recursively for all sub-items
    foreach(NestedRepeaterItem child in current.Items)
    {
        DoSomething(child);
    }
}

Conclusion

The NestedRepeater certainly fills a void when it comes to dealing with recursive data. .NET 2.0 introduces a new interface and a set of new classes that tackle hierarchical data. As you can see in MSDN, their use is not as straightforward as it is with other .NET controls. That's why you might be interested in the NestedRepeater even with .NET 2.0, especially when your data is simple to deal with.

The example I've used here is quite simple because I did not want to add useless stuff and just focus on the control itself. In a next article, I'll show how the NestedRepeater can help build elegant generic TreeView controls with just a few lines of code.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here