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 Repeater
s on my WebForm, create the adequate DataRelation
s 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 Repeater
s 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 displayedANI_PARENT
: the foreign key, which references ANI_ID
The NestedRepeater
exposes a public property called DataSource
:
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:
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:
ds.Relations.Add(RelationName,
ds.Tables[0].Columns["ANI_ID"],
ds.Tables[0].Columns["ANI_PARENT"]);
At this time, the NestedRepeater
needs two properties:
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:
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:
<%@ 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:
<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
protected virtual void CreateControlHierarchy(bool createFromDataSource)
{
int nbTopNodes = 0;
DataView dv = null;
if (m_headerTemplate != null)
{
NestedRepeaterHeaderFooter header =
new NestedRepeaterHeaderFooter();
m_headerTemplate.InstantiateIn(header);
if (createFromDataSource)
header.DataBind();
Controls.Add(header);
}
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.");
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;
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
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
{
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);
}
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:
m_template.InstantiateIn(item);
m_template
is of type ITemplate
. It is the variable that backs the property:
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:
void InstantiateIn(Control container)
{
HtmlImage img = new HtmlImage();
. . .
LiteralControl lit = new LiteralControl();
. . .
container.Controls.Add(img);
container.Controls.Add(lit);
. . .
}
When item.DataBind()
is called, both expressions:
<%# (Container.Depth * 10) %>
and
<%# (Container.DataItem as DataRow)["ANI_NAME"]%>
are evaluated. We can raise the ItemDataBound
event:
OnItemDataBound(args);
item
is of type NestedRepeaterItem
. This class gathers the necessary information for this node:
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
:
public enum NestedElementPosition
{
First,
Last,
OnlyOne,
NULL
}
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:
foreach(NestedRepeaterItem item in myRepeater.Items)
{
DoSomething(item);
}
private void DoSomething(NestedRepeaterItem current)
{
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.