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

Templated Hierarchical Repeater Control

4.12/5 (10 votes)
5 Mar 2009CPOL7 min read 46.2K   1K  
A Templated Databound control for making TreeViews and displaying hierarchical data

Introduction

I once had the need to implement a treeview type control with some very tricky CSS requirements. I looked at the ASP.NET TreeView and the corresponding CSS Friendly Adapter, but I still couldn't bend it to get the layout the way I needed it. The problem with built in TreeView was the lack of control over how the HTML was rendered. What I wanted was a templated treeview so I could change the HTML output on the aspx page quickly and easily without having to write lots of code and recompile.

That's what the HierarchicalRepeater is. It has an ItemTemplate, similar to that of the Repeater control, where you can specify the HTML output of each data item.

The sample project you can download with this article shows working examples of this control as a TreeView, and also provides an ASP.NET implementation of the Nested Sortable jQuery Plugin which allows drag-and-drop resorting of the dataitems in the tree. 

Using the Control

Add a reference in your website to DaveControls.dll, which can be found in the downloadable project. Add the following line to the top of your aspx page to register the control for use in your page:

ASP.NET
<%@ Register Assembly="DaveControls"
    Namespace="DaveControls.HierarchicalControls" TagPrefix="hc" %>

You can then place the HierarchicalRepeater control anywhere in your aspx page as follows:

ASP.NET
<hc:HierarchicalRepeater runat="server" ID="SimpleHierarchicalRepeater">

    <HeaderTemplate>
        <p>This is the repeater header</p>
        <ul class="indent">
    </HeaderTemplate>

    <ItemTemplate>
        <li>
            <%# DataBinder.Eval(Container.DataItem, "Text") %>
            <asp:PlaceHolder ID="ChildrenPlaceholder" runat="server" />
        </li>
    </ItemTemplate>

    <FooterTemplate>
        </ul>
        <p>This is the repeater footer</p>
    </FooterTemplate>

    <ChildrenHeaderTemplate>
        <ul>
    </ChildrenHeaderTemplate>

    <ChildrenFooterTemplate>
        </ul>
    </ChildrenFooterTemplate>

</hc:HierarchicalRepeater>

The main parts of the control to note are as follows:

HeaderTemplate

This is the header of the control. This part will render before all of the contents. In this example, I want to render the contents as an unordered list with a CSS class of indent, so I include the following in my header:

ASP.NET
<ul class="indent">

ItemTemplate

This is the template that will be rendered for each data item. You can data bind to properties of the underlying data item object just as you would in the ItemTemplate of a Repeater. I have data bound to the property Text using the following code:

ASP.NET
<%# DataBinder.Eval(Container.DataItem, "Text") %>

There is also a Placeholder control in the ItemTemplate which will contain the children of each data item. The child data items will be rendered using the same template as ItemTemplate. The Placeholder ID must be set to ChildrenPlaceholder for the children to render:

ASP.NET
<asp:PlaceHolder ID="ChildrenPlaceholder" runat="server" />

FooterTemplate

This will be rendered at the bottom of my control. I need to close the unordered list that I opened in the HeaderTemplate so I include the following:

ASP.NET
</ul>

ChildrenHeaderTemplate

This will be rendered at the top of the list of child dataitems. In this example, I want all child lists to be rendered as unordered lists with no CSS class so I use the following in my ChildrenHeaderTemplate:

ASP.NET
<ul>

ChildrenFooterTemplate

This will be rendered at the bottom of the list of children. For this example, I just want to close my unordered list so I use the following:

ASP.NET
</ul>

Data Binding

The data items to be displayed by the control must inherit from the class DaveControls.HierarchicalControls.Data.HierarchyDataItemBase, found in the download for this article. This base class manages the hierarchical structure of the data and provides some methods for locating data items. The derived class you create therefore only needs the properties that you wish to be displayed in the control.

I will use the following very simple example. The derived class only contains one property called Text, which is displayed in the ItemTemplate above

C#
private class SimpleTestData : HierarchyDataItemBase // inherits from base class
{
    private string _text;
    public string Text
    {
        get { return _text; }
        set { _text = value; }
    }

    public SimpleTestData(string text)
    {
        _text = text;
    }
}

HierarchyDataItemBase implements ICollection and acts as a container for its child items. The following code builds a data hierarchy of SimpleTestData objects by creating a root object, testDataSource, and adding children to it using the Add method.

C#
SimpleTestData testDataSource = new SimpleTestData("rootItem");

testDataSource.Add(new SimpleTestData("Pie"));
testDataSource.Add(new SimpleTestData("Cake"));
SimpleTestData item3 = new SimpleTestData("Fruit");

SimpleTestData item3a = new SimpleTestData("Apples");
SimpleTestData item3b = new SimpleTestData("Pears");
SimpleTestData item3c = new SimpleTestData("Oranges");

item3.Add(item3a);
item3.Add(item3b);
item3.Add(item3c);

SimpleTestData item3a1 = new SimpleTestData("Granny Smith");
SimpleTestData item3a2 = new SimpleTestData("Golden Delicious");

item3a.Add(item3a1);
item3a.Add(item3a2);

testDataSource.Add(item3);

testDataSource.Add(new SimpleTestData("Crisps"));

The HierarchicalRepeater control can be data bound to the above object using the following code:

C#
SimpleHierarchicalRepeater.DataSource = testDataSource;
SimpleHierarchicalRepeater.DataBind();

Rendered HTML Output

The HTML output of the control is as follows:

ASP.NET
<p>This is the repeater header</p>
<ul class="indent">
    <li>Pie</li>
    <li>Cake</li>
    <li>Fruit
        <ul>
            <li>Apples
                <ul>
                    <li>Granny Smith</li>
                    <li>Golden Delicious</li>
                </ul>
            </li>
            <li>Pears</li>
            <li>Oranges</li>
        </ul>
    </li>
    <li>Crisps</li>
</ul>
<p>This is the repeater footer</p>

Accessing Data Items - Useful Properties

The above example showed how to use the control to display data only. But what about accessing the different items once bound to the control? Firstly, let's look at some of the properties that are available to each node in the control that describe the node's location in the data hierarchy.

GlobalIndex

An integer index for each item from top to bottom, regardless of its nesting within the hierarchy.

SiblingIndex

An integer index for each item relative to its siblings, that are the other items on the same nesting level, with the same parent.

Path

A relative path for each element from the root composed of its sibling index and the sibling indexes of its parents.

Clear as mud? Here's an example to help illustrate each property. The code below shows the ItemTemplate with the syntax to output the above properties.

ASP.NET
<ItemTemplate>
    <li><strong><%# DataBinder.Eval(Container.DataItem, "Text") %></strong>
        GlobalIndex: <%# Container.GlobalIndex %>,
        SiblingIndex: <%# Container.SiblingIndex %>,
        Path: <%# Container.Path %>
    <asp:PlaceHolder ID="ChildrenPlaceholder" runat="server" />
    </li>
</ItemTemplate>

Shown below is the control bound to the same data source as in the previous example:

  • Pie GlobalIndex: 0, SiblingIndex: 0, Path: /0
  • Cake GlobalIndex: 1, SiblingIndex: 1, Path: /1
  • Fruit GlobalIndex: 2, SiblingIndex: 2, Path: /2
    • Apples GlobalIndex: 3, SiblingIndex: 0, Path: /2/0
      • Granny Smith GlobalIndex: 4, SiblingIndex: 0, Path: /2/0/0
      • Golden Delicious GlobalIndex: 5, SiblingIndex: 1, Path: /2/0/1
    • Pears GlobalIndex: 6, SiblingIndex: 1, Path: /2/1
    • Oranges GlobalIndex: 7, SiblingIndex: 2, Path: /2/2
  • Crisps GlobalIndex: 8, SiblingIndex: 3, Path: /3

Events

Shown below are the events that have been implemented. When the events are fired, the event handler receives an event arguments object that describes which particular node in the control fired the event.

OnItemCommand

Same as the OnItemCommand of a Repeater control. Fired when a LinkButton or similar control is clicked, for example. The event handler takes an event arguments object of type HierarchicalRepeaterCommandEventArgs:

ASP.NET
<hc:HierarchicalRepeater ID="HierarchicalRepeater1" runat="server"
    OnItemCommand="PropertiesHierarchicalRepeater_ItemCommand">
...
C#
// event handler
protected void PropertiesHierarchicalRepeater_ItemCommand(object sender,
    DaveControls.HierarchicalControls.HierarchicalRepeaterCommandEventArgs e)
{
    // can access properties for each item as follows

    int globalIndex = e.Item.GlobalIndex;
    int siblingIndex = e.Item.SiblingIndex;
    string path = e.Item.Path;
}

OnItemCreated

Fired when an item in the control is created, before it is data bound. The event handler takes an event arguments object of type HierarchicalRepeaterEventArgs:

ASP.NET
<hc:HierarchicalRepeater ID="HierarchicalRepeater1" runat="server"
    OnItemCreated="PropertiesHierarchicalRepeater_ItemCreated">
...
C#
// event handler
protected void PropertiesHierarchicalRepeater_ItemCreated(object sender,
    DaveControls.HierarchicalControls.HierarchicalRepeaterEventArgs e)
{
    // can access properties for each item in the same way as before

    int globalIndex = e.Item.GlobalIndex;
    int siblingIndex = e.Item.SiblingIndex;
    string path = e.Item.Path;
}

OnItemDataBound

Fired after the item has been databound. The event handler for this event takes the same event arguments object type as OnItemDataBound (HierarchicalRepeaterEventArgs):

ASP.NET
<hc:HierarchicalRepeater ID="HierarchicalRepeater1" runat="server"
    OnItemDataBound="PropertiesHierarchicalRepeater_ItemDataBound">
...
C#
// event handler
protected void PropertiesHierarchicalRepeater_ItemDataBound(object sender,
    DaveControls.HierarchicalControls.HierarchicalRepeaterEventArgs e)
{
    // can access properties for each item in the same way as before

    int globalIndex = e.Item.GlobalIndex;
    int siblingIndex = e.Item.SiblingIndex;
    string path = e.Item.Path;
}

Using the Properties to Access Data Items from the Data Source

The above section described useful properties for the control's nodes that give their location in the data hierarchy. These properties can be used to find the corresponding data item in the data source. The following methods are members of the HierarchyDataItemBase class and can be called to retrieve data items from the data source.

HierarchyDataItemBase GetItem(string path)

Return the HierarchyDataItemBase object by Path:

C#
// Executing this code on the data source in our example, retrievedItem
// will be the HierarchyDataItemBase object for the item "Granny Smith"

HierarchyDataItemBase retrievedItem = testDataSource.GetItem("/2/0/0");

HierarchyDataItemBase GetItem(int globalIndex)

Return the HierarchyDataItemBase object by GlobalIndex:

C#
// This code will also retrieve the item "Granny Smith", but this time
// uses the globalIndex rather than its path

HierarchyDataItemBase retrievedItem = testDataSource.GetItem(4);

Creating an Expanding and Collapsing Tree View

It is best to just look at the TreeView.aspx page in the downloadable sample to see an example of a working treeview.

The sample shows how div elements with JavaScript onclick events make the expand and collapse buttons. The lists of child elements are given id values with the GlobalIndex property to make them unique so they can be shown/hidden according to which expand/collapse div is clicked.

The expanding and collapsing of the nodes in the tree view is done purely client side with JavaScript. In order to persist the user's changes, the expand/collapse states of the nodes are serialised into a string and place in a HiddenField control. This serialised value is then read server side to update the data source with the new expand/collapse states so that the HierarchicalRepeater control can be re-databound.

Nested Sortable ASP.NET Implementation

The Nested Sortable jQuery Plugin (not my work) is a JavaScript plugin that allows the items of a tree view to be reorganised using drag and drop. The NestedSortable.aspx page in the samples project shows how the NestedSortable JavaScript can be integrated with the HierarchicalRepeater control to implement drag-and-drop sorting.

To persist the changes over postback, a similar approach has been used as with the TreeView sample:

  1. Place the serialised changes into a hidden field whenever the user moves an item (functionality already provided in the JavaScript plugin)
  2. Perform the ItemCommand event by extracting the selected data item from the data source before it is reordered
  3. Reorder the data source with the changes the user has made, which are serialised in the hidden field.
  4. Store the reordered data source in the page's ViewState
  5. DataBind the HierarchicalRepeater with the re-ordered data source

Again, it is best to look at the NestedSortable.aspx.cs file for a clearer picture of how it works.

Conclusion

The HierarchicalRepeater control is a Templated Databound control, similar to a Repeater, that provides a quick and easy way to display hierarchical data. It claws back control over the HTML rendering that is lost from using the built-in ASP.NET TreeView control.

The examples in the downloadable project that accompany this article show how the control can be used to implement a fully working TreeView control and a nested sortable list. 

History

  • 5th March, 2009: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)