Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

The CustomTreeView ASP.NET 2.0 Server Control


November, 2005: The CustomTreeView ASP.NET 2.0 Server Control

Shahram is a senior software engineer with Schlumberger Information Solutions (SIS). He specializes in ADO.NET, ASP.NET, and XML web services. He can be reached at [email protected].


The ASP.NET 2.0 TreeView server control is used to display hierarchical data. Consider, for instance, an XML-based threaded discussion forum that uses the XML file in Example 1(a) as its underlying data store. You can set the value of the DataFile property of an XmlDataSource control to the path of the XML file (data.xml, for instance) to load the entire XML file into memory and use a TreeView control to display the contents of the file, as in Example 1(b). Loading the entire XML file into memory would not be practical in threaded discussion forums that contain thousands of messages. It would waste server resources if the server were to load all the messages for each user request.

One of the important features of the TreeView server control is that it lets page developers load data on demand where the child nodes of a given node are not loaded and displayed until users expand the node. For this to work, you must perform these tasks:

  1. Statically add a TreeNode instance to the Nodes property of the TreeView control and set its Text, Value, and PopulateOnDemand properties. The TreeNode instance is used to display the root node of the TreeView. In the case of a threaded discussion forum, Example 2 is required.
  2. Register a method, for example, TreeView1_OnTreeNodePopulate, which is called when users expand a node.
  3. The method must extract the required data from the underlying data store, create instances of the TreeNode class, and set the Text, Value, and PopulateOnDemand properties of the instances.

In short, you must do a fair amount of coding to take advantage of the TreeView control's on-demand loading feature. This goes against an important feature of ASP.NET 2.0 in which you must be allowed to declaratively use the ASP.NET 2.0 server controls without writing a single line of code!

In this article, I implement CustomTreeView, a server control derived from the ASP.NET 2.0 TreeView server control. CustomTreeView lets you use the on-demand feature without writing a single line of code. It automatically takes care of all the steps without any coding on your part. The complete source code is available electronically; see "Resource Center," page 4. Note that I used Visual Web Developer 2005 Express Edition beta to develop the sample application.

OnTreeNodePopulate Method

The TreeView control exposes an event of type TreeNodeEventHandler named TreeNodePopulate. The event is raised when users expand a node whose PopulateOnDemand property is set to True. The TreeNodeEventArgs class is used to hold the event data. The class exposes an important property of type TreeNode named Node that points to the node that raises the TreeNodePopulate event.

The TreeView control also exposes a protected method named OnTreeNodePopulate that is used to raise the TreeNodePopulate event. The CustomTreeView control overrides the OnTreeNodePopulate method (see CustomTreeView.cs, available electronically, see "Resource Center," page 4). The OnTreeNodePopulate method saves the reference to the Node property value in the currentNode field for future references. The main responsibility of the OnTreeNodePopulate method of CustomTreeView is to perform these tasks:

  1. Extract the required data from the underlying data store.
  2. Enumerate the data and create an instance of the TreeNode class for each enumerated object.
  3. Use the extracted data to set the value of the Text property of each TreeNode instance. The CustomTreeView control exposes a property named TextField that you must set to the appropriate value.
  4. Use the extracted data to set the value of the Value property of each TreeNode instance. The CustomTreeView control exposes a property named ValueField that you must set to the appropriate value.
  5. Set the value of the PopulateOnDemand property of each TreeNode instance to True.

The main challenge is to extract the data from the underlying data store. This is a challenge because in a data-driven web-application, data comes from a variety of sources including Microsoft SQL Server, Oracle, XML documents, and flat files, to name a few. This means that you cannot use the same data access code to access different types of data stores. For instance, you have to use System.Xml classes such as XmlDocument and XPathDocument to access XML documents. However, you cannot use the same System.Xml classes to access relational databases such as Microsoft SQL Server or Oracle. For that you have to use ADO.NET classes such as SqlCommand or OracleCommand. This is where the ASP.NET 2.0 data source control model comes to the rescue. The OnTreeNodePopulate method uses the ASP.NET 2.0 data source control model to load the data from the underlying data store.

ASP.NET 2.0 Data Source Control Model

The ASP.NET 2.0 data source control model isolates the OnTreeNodePopulate method from the underlying data store and presents the method with the appropriate view of the data store. The model consists of new server controls collectively known as "data source controls." ASP.NET 2.0 comes with several types of data source controls: SqlDataSource, AccessDataSource, ObjectDataSource, XmlDataSource, and SiteMapDataSource.

Every data source control could present both tabular and hierarchical views of its underlying data store. Currently, only the XmlDataSource and SiteMapDataSource controls present both types of view. All other data source controls (SqlDataSource, AccessDataSource, and ObjectDataSource) only present tabular views.

Each data source control is specifically designed to extract tabular or hierarchical data from a specific type of data store. For instance, the SqlDataSource is specifically designed to extract tabular data from relational databases such as Microsoft SQL Server and Oracle.

If the OnTreeNodePopulate method were to use a specific type of data source control (such as SqlDataSource) to access the underlying data store, the method would be tied to the data store that the data source control is specifically designed to work with and could not be used to access other types of data stores.

If the OnTreeNodePopulate method must not use a specific type of data source control such as SqlDataSource, what else could the method use to access the underlying data store? The answer lies in the fact that all tabular and hierarchical data source controls implement the IDataSource and IHierarchicalDataSource interfaces, respectively.

This lets the OnTreeNodePopulate method treat all types of tabular or hierarchical data source controls the same. As far as the OnTreeNodePopulate method is concerned all tabular and hierarchical data source controls are of type IDataSource and IHierarchicalDataSource, respectively.

Therefore, the OnTreeNodePopulate method can simply use the methods of the IDataSource or IHierarchicalDataSource interface to deal with all types of data source controls without knowing the real type of the data source being used; for example, whether it is a SqlDataSource or XmlDataSource control.

However, this raises a new problem. If the OnTreeNodePopulate method does not know the real type of the data source control, how can it then instantiate the data source control? The answer lies in the fact that all data source controls also derive from the Control class, where they inherit these two important features:

  • The Control class provides two mechanisms for data source controls to save and restore their property values across page post backs. The first mechanism provides data source controls with a StateBag collection object named ViewState to save and restore the values of their simple properties. The second mechanism provides data source controls with three methods named TrackViewState, LoadViewState, and SaveViewState that subclasses can override to save and restore the values of their complex properties.
  • The Control class lets you declaratively add data source controls to the respective .aspx page without writing a single line of code. The ASP.NET runtime automatically parses the .aspx page, dynamically creates instances of the declared data source controls, and adds the instances to the control tree of the containing page.

Because the underlying data source control is automatically added to the control tree of the containing page, the OnTreeNodePopulate method simply uses the FindControl method of the containing page to access the data source control without knowing the real type of the data source control; for example, whether it is a SqlDataSource or XmlDataSource control:

Control c = Page.FindControl
(treeNodePopulateDataSourceID);

The FindControl method takes the value of the ID property of the data source control as its argument. The CustomTreeView control exposes a property of type string named TreeNodePopulateDataSourceID that you must set to the value of the ID property of the desired data source control.

Therefore, the only dependency between the OnTreeNodePopulate method and the underlying data source control is the value of the ID property of the data source control, which is nothing but a string value.

The OnTreeNodePopulate method supports both tabular and hierarchical views. As Listing One shows, the OnTreeNodePopulate method typecasts the returned value of the FindControl method to IHierarchicalDataSource or IDataSource. This means that, as far as the OnTreeNodePopulate method is concerned, all data source controls are of type IHierarchicalDataSource and/or IDataSource.

Tabular Data Source Controls

The IDataSource interface exposes an important method named GetView that takes the name of the tabular view and returns an instance of the DataSourceView class that represents the view. Recall every tabular data source control such as SqlDataSource is specifically designed to expose one or more tabular views of a specific type of data store. The tabular views that a given data source control exposes are instances of a particular class.

For instance, the tabular views that the SqlDataSource and XmlDataSource controls expose are instances of the SqlDataSourceView and XmlDataSourceView classes, respectively. All view classes such as SqlDataSourceView and XmlDataSourceView derive from the DataSourceView abstract base class.

The DataSourceView class exposes all the methods and properties that its subclasses must implement to expose tabular views of their underlying data stores. One of these methods is the Select method.

The OnTreeNodePopulate method calls the Select method of the view object to select the data from the underlying data store. The Select method takes two arguments. The first argument is of type DataSourceSelectArguments. This argument lets the callers request extra operations such as sorting and paging on the query result. The OnTreeNodePopulate method does not need any extra operations.

The second argument of the Select method is a delegate instance of type DataSourceViewSelectCallback. The OnTreeNodePopulate method uses this instance to register the SelectCB method as the callback for the Select method. This is necessary because the Select method is asynchronous.

The Select method extracts the data from the data store, automatically calls the SelectCB method, and passes the data as its argument. The data that is passed to the SelectCB method is of type IEnumerable. The IEnumerable interface exposes a method named GetEnumerator that returns an object of type IEnumerator. The SelectCB method then uses the IEnumerator object to enumerate the records and creates an instance of the TreeNode class to display each record. However, a record is a collection of fields. Therefore, you need a way to specify which field of a given record to display.

That is why the CustomTreeView control exposes a property named TextField that you can set to the name of the database field whose value they want to display. The DataBinder class exposes a static method named Eval that uses the .NET Framework reflection to evaluate the value of the specified field of the current record. This value is then assigned to the Text property of the TreeNode object:

node.Text = (string)DataBinder.Eval
(iter.Current, TextField);

Every record also exposes a primary key field. The CustomTreeView control exposes a property named ValueField that you can use to specify the name of the primary key field. The SelectCB method uses the Eval method of the DataBinder class to evaluate the value of the respective primary key field and stores the value in the Value property of the TreeNode object:

node.Value = (DataBinder.Eval(iter.Current,
ValueField)).ToString();

The CustomTreeView control also exposes two properties named NavigateUrlFormatString and NavigateUrlField. You must set the value of the NavigateUrlField property to the name of the database field used to calculate the value of the NagivateUrl property of the TreeNode object. You must also set the value of the NavigateUrlFormatString property to the appropriate format string. For instance, consider the values in Example 3(a). The SelectCB method uses the expression in Example 3(b) to evaluate the value of the NavigateUrl property of the respective TreeNode object. For example, for the record whose MessageID value is 1, the aforementioned expression evaluates to Example 3(c). The SelectCB method also sets the PopulateOnDemand property of the TreeNode object to True. This lets users expand the node and display its child nodes on demand. The SelectCB method finally creates an instance of the NodeEventArgs class and calls OnNodeDataBound method to raise the NodeDataBound event.

Hierarchical Data Source Controls

The OnTreeNodePopulate method also supports hierarchical data source controls such as XmlDataSource. Hierarchical data source controls expose hierarchical views of their underlying data store whether or not the data store itself is hierarchical. Again, all hierarchical data source controls implement the IHierarchicalDataSource interface. The OnTreeNodePopulate method calls the GetHierarchicalView method of the IHierarchicalDataSource interface and passes the view path as its argument:

HierarchicalDataSourceView hdv =
hds.GetHierarchicalView(e.Node.Value);

Notice the Value property of the TreeNode object exposes the hierarchical path of the TreeNode object. The HierarchicalDataSourceView class is the base class of all hierarchical view classes. For instance, the hierarchical views that the XmlDataSource control exposes are all instances of the XmlHierarchicalDataSourceView class, which is the subclass of the HierarchicalDataSourceView class.

The HierarchicalDataSourceView class exposes all the methods and properties that its subclasses need to implement in order to expose hierarchical views of their underlying data stores. The OnTreeNodePopulate method calls the Select method of the HierarchicalDataSourceView class to extract the data from the underlying data store. The Select method returns an object of type IHierarchicalEnumerable. The OnTreeNodePopulate method then calls the HierarchicalSelectCB method and passes the IHierarchicalEnumerable object as its argument.

Because the IHierarchicalEnumerable interface implements the IEnumerable interface, it exposes a method named GetEnumerator. The HierarchicalSelectCB method calls the method to access the IEnumerator object and uses the object to enumerate the hierarchical nodes of the extracted data. The HierarchicalSelectCB method uses an instance of the TreeNode class to display each enumerated hierarchical node.

The HierarchicalSelectCB method then calls the GetHierarchyData method of the IHierarchicalEnumerable interface to access the IHierarchyData object that encapsulates the actual enumerated object. The IHierarchyData interface exposes a property named Item that refers to the actual encapsulated enumerated object, which is normally an instance of a class that implements the IXPathNavigable interface. For instance, if the underlying hierarchical data source control is an XmlDataSource control, the value of the Item property will be an object of type XmlElement, which implements the IXPathNavigable interface. The great thing about the IXPathNavigable interface is that it exposes a method named CreateNavigator, which returns an object of type XPathNavigator. The XPathNavigator class is the .NET implementation of the XPath technology.

The XPath technology uses the hierarchical path of a given node to locate the node in an in-memory tree representation. The hierarchical path of a node is a collection of what is known as "location steps." Every location step takes us from the node we are currently at to the next node. A location step consists of three parts—axis, node test, and predicate.

You can think of the axis as the traversal direction. The HierarchicalSelectCB method uses the child node as the axis. This means that you are traversing the tree in the direction of children. Therefore, the axis returns a node set that contains all the child nodes of the current node. The node test selects those nodes from the node set that satisfy the node test. The HierarchicalSelectCB method uses "*" as the node test. This means that only the element child nodes of the current node are selected.

The predicate further filters the resultant node set. The HierarchicalSelectCB method uses this as the predicate:

[position()=
'" + currentNode.ChildNodes.Count + "']

The HierarchicalSelectCB method uses the count of the child nodes of the current node to determine the position of the underlying current hierarchical node. Therefore, the respective location step is:

/*[position()=
'" + currentNode.ChildNodes.Count + '"]

The HierarchicalSelectCB method concatenates the /*[position()... location step to the hierarchical path of the parent node to arrive at the hierarchical path of the underlying hierarchical node:

node.Value =
currentNode.Value + "/*[position()='" +
currentNode.ChildNodes.Count + '"]";

Because the enumerated object implements the IXPathNavigable interface, the HierarchicalSelectCB method uses the Eval method of the XPathBinder class to evaluate the XPath expression contained in the TextField property against the object.

node.Text =
XPathBinder.Eval(idata.Item, TextField).ToString();

This means that you must set the value of the TextField property to the required XPath expression. The HierarchicalSelectCB method also uses the values of the NavigateUrlField and NavigateUrlFormatString properties to evaluate the value of the NavigateUrl property of the TreeNode object:

if (NavigateUrlFormatString != null &&
NavigateUrlFormatString.Trim() != "" &&
NavigateUrlField !=
null && NavigateUrlField.Trim() != "")
node.NavigateUrl =
String.Format(NavigateUrlFormatString,
XPathBinder.Eval
(idata.Item, NavigateUrlField));

The HierarchicalSelectCB method finally sets the PopulateOnDemand property to True and raises the NodeDataBound event.

NodeDataBound Event

The CustomTreeView control uses the standard .NET Framework event pattern to define the NodeDataBound event. The pattern involves the following steps:

  1. Define the class that holds the event data (see code available electronically). The NodeEventArgs class holds two event data; for example, the TreeNode object that raises the event and the respective database record. The DataItem and Node properties refer to these two event data, respectively.
  2. Define the event delegate:
  3. public delegate void
         NodeEventHandler
              (Object sender, NodeEventArgs e);

    You must use an instance of the NodeEventHandler delegate to register callbacks for the NodeDataBound event.

  4. Define the event itself:
  5. private static readonly object
          NodeDataBoundKey = new Object();
    public event NodeEventHandler
          NodeDataBound
    {
          add
          {
            Events.AddHandler
                (NodeDataBoundKey, value);
          }
          remove
         {
            Events.RemoveHandler
                (NodeDataBoundKey, value);
         }
    }

    The CustomTreeView control defines a new object named NodeDataBoundKey that is used as a key to access the event.

  6. Define the method that raises the event:
  7. protected virtual void
          OnNodeDataBound(NodeEventArgs e)
    {
        NodeEventHandler handler = (NodeEventHandler)Events [NodeDataBoundKey];
       if (handler != null)
           handler(this, e);
    }

    The OnNodeDataBound method uses the NodeDataBoundKey key to access the event and raises the event if necessary.

Root Name and Value

When users visit the containing page for the first time, the CustomTreeView control only displays the root node. CustomTreeView exposes two properties—RootText and RootValue (available electronically).

The setters of both RootText and RootValue properties create an instance of the TreeNode class to display the root node, if necessary. Notice both setters set the PopulateOnDemand property of the TreeNode object to True. When users expand the TreeNode object, it automatically calls the OnTreeNodePopulate method to dynamically load the child nodes of the root node.

Control State

CustomTreeView must save and restore its property values across page post backs to function properly. ASP.NET 2.0 presents you with a new way to save and restore those properties of the control that are essential to its operation.

The main problem with ASP.NET 1.x view state is the fact that you have the option of turning it off. This causes major problems for server controls that need to save/restore their property values across page post backs to function properly.

You can think of the ASP.NET 2.0 control state as the private storage of the control where you would not be able to turn off the feature. The Control class exposes two methods named SaveControlState and LoadControlState that the CustomTreeView control overrides to save and restore its property values across post backs. The SaveControlState method uses an array of size 8 to hold the data that it needs to save to the view state.

The CustomTreeView control overrides the OnInit method to register its interest in control state with the containing page:

protected override void OnInit(EventArgs e)
{
Page.RegisterRequiresControlState(this);
base.OnInit(e);
}

The web page in Listing One shows how you can use CustomTreeView to load data on demand from a Microsoft SQL Server database without writing a single line of code. The CustomTreeView control takes advantage of the ASP.NET 2.0 data source control model to allow page developers to use any of the ASP.NET 2.0 tabular or hierarchical data source controls.

DDJ



Listing One

<%@ Page Language="C#" %>
<%@ Register TagPrefix="custom" Namespace="CustomControls" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" 
              "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<script runat="server">
    void databound(Object sender, NodeEventArgs e)
    {
        e.Node.Text = ((string)DataBinder.Eval(e.DataItem, 
                    "Subject")).ToUpper() + ", " +
                    (string)DataBinder.Eval(e.DataItem, "AddedDate", "{0:d}");
    }
</script>

<html xmlns="http://www.w3.org/1999/xhtml" >
<head id="Head1" runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
        <custom:CustomTreeView ID="tv" Runat="Server"
        RootText="Messages" RootValue="0"
        TextField="Subject"
        ValueField="MessageID"
        TreeNodePopulateDataSourceID="MySource"
        OnNodeDataBound="databound"
        ExpandDepth="FullyExpand" />
        <asp:SqlDataSource ID="MySource" Runat="Server"
         ConnectionString="<%$ ConnectionStrings:MyConnectionString %>" 
         SelectCommand="Select * From Messages Where ParentID=@ParentID">
            <SelectParameters>
                <asp:ControlParameter Name="ParentID" Type="Int32" 
                             ControlID="tv" PropertyName="CurrentValue" />
            </SelectParameters>
         </asp:SqlDataSource>
    </form>
</body>
</html>
Back to article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.