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:
<%@ Register Assembly="DaveControls"
Namespace="DaveControls.HierarchicalControls" TagPrefix="hc" %>
You can then place the HierarchicalRepeater
control anywhere in your aspx page as follows:
<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:
<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:
<%# 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: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:
</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
:
<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:
</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
private class SimpleTestData : HierarchyDataItemBase
{
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.
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:
SimpleHierarchicalRepeater.DataSource = testDataSource;
SimpleHierarchicalRepeater.DataBind();
Rendered HTML Output
The HTML output of the control is as follows:
<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.
<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
:
<hc:HierarchicalRepeater ID="HierarchicalRepeater1" runat="server"
OnItemCommand="PropertiesHierarchicalRepeater_ItemCommand">
...
protected void PropertiesHierarchicalRepeater_ItemCommand(object sender,
DaveControls.HierarchicalControls.HierarchicalRepeaterCommandEventArgs e)
{
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
:
<hc:HierarchicalRepeater ID="HierarchicalRepeater1" runat="server"
OnItemCreated="PropertiesHierarchicalRepeater_ItemCreated">
...
protected void PropertiesHierarchicalRepeater_ItemCreated(object sender,
DaveControls.HierarchicalControls.HierarchicalRepeaterEventArgs e)
{
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
):
<hc:HierarchicalRepeater ID="HierarchicalRepeater1" runat="server"
OnItemDataBound="PropertiesHierarchicalRepeater_ItemDataBound">
...
protected void PropertiesHierarchicalRepeater_ItemDataBound(object sender,
DaveControls.HierarchicalControls.HierarchicalRepeaterEventArgs e)
{
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
:
HierarchyDataItemBase retrievedItem = testDataSource.GetItem("/2/0/0");
HierarchyDataItemBase GetItem(int globalIndex)
Return the HierarchyDataItemBase
object by GlobalIndex
:
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:
- Place the serialised changes into a hidden field whenever the user moves an item (functionality already provided in the JavaScript plugin)
- Perform the
ItemCommand
event by extracting the selected data item from the data source before it is reordered - Reorder the data source with the changes the user has made, which are serialised in the hidden field.
- Store the reordered data source in the page's
ViewState
- 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