Click here to Skip to main content
15,867,832 members
Articles / Web Development / ASP.NET
Article

A NestedRepeater Control for ASP.NET

Rate me:
Please Sign up or sign in to vote.
3.79/5 (9 votes)
17 Nov 20076 min read 93K   1.6K   49   19
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


Written By
Web Developer
France France
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionA very nice article. I need to add expand and collapse to nested repeater-can you please help me Pin
Travelthrprog12-Aug-13 8:35
Travelthrprog12-Aug-13 8:35 
GeneralNice Control Pin
stixoffire1-May-08 16:26
stixoffire1-May-08 16:26 
QuestionPlease Help Pin
Siow Thian Choong12-Nov-07 16:19
Siow Thian Choong12-Nov-07 16:19 
AnswerRe: Please Help Pin
meggash m13-Nov-07 10:44
meggash m13-Nov-07 10:44 
GeneralRe: Please Help Pin
Siow Thian Choong13-Nov-07 17:11
Siow Thian Choong13-Nov-07 17:11 
GeneralRe: Please Help [modified] Pin
Siow Thian Choong25-Nov-07 18:38
Siow Thian Choong25-Nov-07 18:38 
GeneralRe: Please Help Pin
meggash m26-Nov-07 10:34
meggash m26-Nov-07 10:34 
Well, I actually prefer the recursive way, because it is more in line with what the NestedRepeater does.
But still, it is possible to have the Items property work the way you want. Another solution would be to add a second property, called for example EntireItems. This property would expose the whole set of child items.
If you work with .net 1.0, you 'll have to type this property as an ArrayList, not as NestedRepeaterItem[] : that's because you can't elegantly know how many items there will be in this array when you instantiate it. If you work with .Net 2.0, then make it a List<NestedRepeaterItem>.
Like for Items, this property can be read-only, but be sure to call EnsureChildControls before returning the inner variable (called for example m_entireItems).

public ArrayList EntireItems
{
 get 
 {
 EnsureChildControls();
 return m_entireItems;
 }
}

}
Intantiate this array at the beginning of the CreateControlHierarchy function.
Then, all you have to do is add the created items into this array :
  • 1st, in function CreateControlHierarchy :
    <code>
    * * *
    m_items[i] = childItem;
    m_entireItems.Add(childItem); // Added line
    </code>

  • 2nd in function CreateItem
    <code>
    * * *
    item.Items[i] = childItem;
    m_entireItems.add(childItem); //Added line
    </code>


Remark : make sure to use the variable m_entireItems directly, not the property EntireItems. Because, if you try to read the property EntireItems, it will call EnsureChildControls, thus trying to initiate a second creation of the child items.

As for you other question (property Depth is missing on postback) : all you need to do is displace 3 lines of code in function CreateItem :

if (row != null) // we are reading data from the DataSource
{
    // prepare data for the ViewState
    childRows = row.GetChildRows(RelationName);
    nbChildren = childRows.Length;
    m_lstNbChildren.Add(nbChildren);

    // REMOVE THE FOLLOWING 4 LINES ...
    // set various data for the item
    item.Position = pos;
    item.NbChildren = childRows.Length;
    item.Depth = depth;
}
else // we are reading data from the viewstate
{
    nbChildren = (int)
        m_lstNbChildren[m_current++];
    childRows = new DataRow[nbChildren];
}

// ... AND PLACE THEM HERE INSTEAD
// set various data for the item
item.Position = pos;
item.NbChildren = childRows.Length;
item.Depth = depth;


That should be OK then ...
GeneralRe: Please Help Pin
Siow Thian Choong26-Nov-07 14:45
Siow Thian Choong26-Nov-07 14:45 
QuestionIntellisense problems. Pin
ympeng21-Mar-07 8:36
ympeng21-Mar-07 8:36 
AnswerRe: Intellisense problems. Pin
Libin Chen22-May-07 14:46
Libin Chen22-May-07 14:46 
QuestionDoes this function in VS2005? Pin
CHHS IS User5-Mar-07 9:10
CHHS IS User5-Mar-07 9:10 
AnswerRe: Does this function in VS2005? Pin
meggash m6-Mar-07 11:11
meggash m6-Mar-07 11:11 
QuestionMultiple Columns Pin
Nullpro3-Jan-07 20:40
Nullpro3-Jan-07 20:40 
AnswerRe: Multiple Columns [modified] Pin
meggash m6-Mar-07 11:31
meggash m6-Mar-07 11:31 
General_onitemdatabound Pin
rvern17-Nov-06 4:06
rvern17-Nov-06 4:06 
GeneralWebDemo Error~!!! Pin
romeo.zhang13-Dec-05 23:30
romeo.zhang13-Dec-05 23:30 
QuestionRecursiveGrid? Pin
Rohit Wason13-Dec-05 12:08
Rohit Wason13-Dec-05 12:08 
GeneralMultiple Relations Pin
Joshua Lunsford8-Dec-05 11:22
Joshua Lunsford8-Dec-05 11:22 
GeneralRe: Multiple Relations Pin
meggash m11-Dec-05 12:31
meggash m11-Dec-05 12:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.