Introducing the ASP.NET 2.0 TreeView and Menu Controls

 

Stephen Walther
Superexpert

August 2004

Applies to:
   Microsoft ASP.NET 2.0
   Microsoft Visual Studio 2005

Summary: Illustrates how you can use the TreeView and Menu controls in ASP.NET 2.0 to represent hierarchical data in your applications. Shows how to use both controls with various data sources as well as how to customize the appearance of both controls. (50 printed pages)

Contents

Introduction
Using the TreeView Control
Displaying a Simple TreeView
Displaying a Navigation Menu with a TreeView Control
Binding to Database Data with a TreeView Control
Using Dynamic Tree Node Population
Using the Menu Control
Displaying a Simple Menu
Controlling the Appearance of the Menu Control
Displaying a Navigation Menu with the Menu Control
Binding a Menu Control to Database Data
Conclusion
Related Books

Introduction

Microsoft ASP.NET 2.0 introduces two new controls, the TreeView and Menu controls, which can be used to represent hierarchical data. Both controls are feature rich and were designed to work in a wide-variety of scenarios. Both controls can be used to display static data, site-map data, and even database data. In addition, both controls support a rich client-side experience. The TreeView and Menu controls render JavaScript for uplevel browsers, but they are also compatible with downlevel browsers.

In this article, you'll be provided with a number of sample pages that illustrate how you can use both the TreeView and Menu controls in your applications. You'll learn how to use both controls with a variety of data sources and you'll also learn how to customize the appearance of both controls.

Using the TreeView Control

The ASP.NET 2.0 TreeView control is an entirely new control introduced with ASP.NET 2.0. (In particular, this control is not based on the Microsoft Internet Explorer TreeView Web Control.)

You can use the TreeView control in any situation in which you need to display hierarchical data. For example, you can use this control when displaying a navigation menu, displaying database records from database tables in a Master/Detail relation, displaying the contents of an XML document, or displaying files and folders from the file system.

Displaying a Simple TreeView

The easiest way to use the TreeView control is to specify a static set of tree nodes. For example, you can use the TreeView control to display a simple navigational menu for a Web site. Or, you can use the TreeView control to display the table of contents for a help file.

You specify the appearance of a static TreeView control by declaring one or more TreeNode controls within the TreeView control's <Nodes> tag. For example, the page in Listing 1 contains a TreeView control that renders an outline for a book (see Figure 1). The outline contains two chapters (these are the root nodes) and each chapter contains two sections (these are the child nodes).

ms379604.treeview_fig01(en-US,VS.80).gif

Figure 1. Displaying a simple TreeView control

Listing 1. SimpleTreeView.aspx (C#)

<%@ Page Language="C#" %>
<script runat="server">

    void TreeView1_SelectedNodeChanged(object sender, EventArgs e)
    {
        Literal1.Text = TreeView1.SelectedNode.Text;
    }
    
</script>
<html>
<head>
    <title>Simple TreeView</title>
</head>
<body>
    <form id="form1" runat="server">
    <table width="100%">
    <tr><td valign="top" bgcolor="#eeeeee" width="200">

    <asp:TreeView 
        ID="TreeView1"
        ExpandDepth="0"
        OnSelectedNodeChanged="TreeView1_SelectedNodeChanged"
        Runat="Server">
        <Nodes>
        <asp:TreeNode Text="Chapter 1">
            <asp:TreeNode Text="Section 1" />
            <asp:TreeNode Text="Section 2" />
        </asp:TreeNode>        
        <asp:TreeNode Text="Chapter 2">
            <asp:TreeNode Text="Section 1" />
            <asp:TreeNode Text="Section 2" />
        </asp:TreeNode>        
        
        </Nodes>
    </asp:TreeView>

    </td><td valign="top">

    <h1><asp:Literal ID="Literal1" Runat="Server" /></h1>
    
    </td></tr>
    </table>
    </form>
</body>
</html> 

Listing 1. SimpleTreeView.aspx (Visual Basic .NET)

<%@ Page Language="VB" %>
<script runat="server">

    Sub TreeView1_SelectedNodeChanged(ByVal sender As Object, _
      ByVal e As EventArgs)
        Literal1.Text = TreeView1.SelectedNode.Text
    End Sub
    
</script>
<html>
<head>
    <title>Simple TreeView</title>
</head>
<body>
    <form id="form1" runat="server">
    <table width="100%">
    <tr><td valign="top" bgcolor="#eeeeee" width="200">

    <asp:TreeView 
        ID="TreeView1"
        ExpandDepth="0"
        OnSelectedNodeChanged="TreeView1_SelectedNodeChanged"
        Runat="Server">
        <Nodes>
        <asp:TreeNode Text="Chapter 1">
            <asp:TreeNode Text="Section 1" />
            <asp:TreeNode Text="Section 2" />
        </asp:TreeNode>        
        <asp:TreeNode Text="Chapter 2">
            <asp:TreeNode Text="Section 1" />
            <asp:TreeNode Text="Section 2" />
        </asp:TreeNode>        
        
        </Nodes>
    </asp:TreeView>

    </td><td valign="top">

    <h1><asp:Literal ID="Literal1" Runat="Server" /></h1>
    
    </td></tr>
    </table>
    </form>
</body>
</html>

By default, each node displayed by a TreeView control has two parts. You can click the image that appears to the left of the text in a tree node to expand and collapse the tree node. For example, when you click the image next to the Chapter 1 node, the node expands and you can see the Section 1 and Section 2 nodes.

You can also click the actual text of a tree node. By default, clicking the text of a tree node posts the page containing the TreeView back to the server and raises the SelectedNodeChanged event. For example, when you click the text rendered by a tree node in the page in Listing 1, the SelectedNodeChanged event handler is executed and the text of the selected node is displayed in a Literal control.

Controlling the Appearance of a TreeView Control

The TreeView control includes several properties you can use to control its appearance. We won't discuss all of these properties here. However, the most dramatic way to change the appearance of a TreeView is by modifying the TreeView control's ImageSet property. This property accepts one of the following values:

  • Arrow
  • BulletedList
  • BulletedList2
  • BulletedList3
  • BulletedList4
  • Contacts
  • Custom
  • Events
  • Faq
  • Inbox
  • News
  • Simple
  • Simple2
  • Msdn
  • WindowsHelp
  • XPFileExplorer

By default, the ImageSet property has the value Custom. When the ImageSet property is set to the value Custom, you can customize the appearance of the TreeView control by supplying paths to custom images for the TreeView control's CollapseImageUrl, ExpandImageUrl, LineImagesFolderUrl, and NoExpandImageUrl properties.

If you assign any of the other values to the ImageSet property, then the images are supplied for you. The following figures illustrate each of the values of the ImageSet property.

ms379604.treeview_fig02(en-US,VS.80).gif

Figure 2. Custom

ms379604.treeview_fig03(en-US,VS.80).gif

Figure 3. BulletedList

ms379604.treeview_fig04(en-US,VS.80).gif

Figure 4. BulletedList2

ms379604.treeview_fig05(en-US,VS.80).gif

Figure 5. BulletedList3

ms379604.treeview_fig06(en-US,VS.80).gif

Figure 6. BulletedList4

ms379604.treeview_fig07(en-US,VS.80).gif

Figure 7. Contacts

ms379604.treeview_fig08(en-US,VS.80).gif

Figure 8. Events

ms379604.treeview_fig09(en-US,VS.80).gif

Figure 9. Faq

ms379604.treeview_fig10(en-US,VS.80).gif

Figure 10. Inbox

ms379604.treeview_fig11(en-US,VS.80).gif

Figure 11. News

ms379604.treeview_fig12(en-US,VS.80).gif

Figure 12. Simple

ms379604.treeview_fig13(en-US,VS.80).gif

Figure 13. Simple2

ms379604.treeview_fig14(en-US,VS.80).gif

Figure 14. MSDN

ms379604.treeview_fig15(en-US,VS.80).gif

Figure 15. WindowsHelp

ms379604.treeview_fig16(en-US,VS.80).gif

Figure 16. XPFileExplorer

By default, lines are not displayed between the nodes in a TreeView. If you want to display connecting lines between tree nodes, then you can take advantage of the TreeView control's ShowLines property (see Figure 17).

ms379604.treeview_fig17(en-US,VS.80).gif

Figure 17. Displaying lines between tree nodes

You can customize the appearance of the lines by creating your own line images. The TreeView control uses the LineImagesFolder property to find the folder that contains the line images. If you are using Microsoft Visual Web Developer, then the easiest way to modify the line images is to take advantage of the ASP.NET TreeView Line Image Generator tool (Right-click the TreeView in design view and select Customize Line Images).

Modifying the Default Behavior of a TreeView

The default behavior of the TreeView control is to raise the SelectedNodeChanged event whenever a tree node is clicked. The page containing the TreeView is posted back to the Web server and the SelectedNodeChanged event is raised. You can modify this default behavior of the TreeView control by modifying the properties of individual TreeNode controls or the TreeView control itself.

For example, if you are using a TreeView control as a simple navigation menu, then you might want to link directly to another page when a tree node is clicked. In that case, you can take advantage of the following two properties of the TreeNode control:

  • NavigateUrl. The URL of a page.
  • Target. The target of the URL. The name of a frame, window, or the values _blank, _parent, _self, _top.

The Target property is valuable when you are using a TreeView with HTML frames. For example, if you display a TreeView control in a menu frame, then you can load content in a separate frame when you click a tree node by setting the Target property to the name of the content frame.

You also can use the SelectAction property of the TreeNode control to modify the behavior of the TreeView control when you click a node. This property accepts the following values:

  • Expand. Raises the TreeNodeExpanded event and expands the tree node.
  • None. Nothing happens when you click the tree node.
  • Select. Raises the SelectedNodeChanged event.
  • SelectExpand. Raises both the TreeNodeExpanded and SelectNodeChanged events.

For example, when displaying the table of contents of a help file with a TreeView control, you might want to display a help topic only when someone clicks a leaf node. If someone clicks the text displayed by a parent node, you might want the node to simply expand. The page in Listing 2 illustrates how you can prevent the SelectedNodeChanged event from being raised when you click a parent node.

Listing 2. SelectActions.aspx

<asp:TreeView 
        ID="TreeView1"
        Runat="Server">
        <Nodes>
        <asp:TreeNode Text="Chapter 1" SelectAction="Expand">
            <asp:TreeNode Text="Section 1" />
            <asp:TreeNode Text="Section 2" />
        </asp:TreeNode>        
        <asp:TreeNode Text="Chapter 2" SelectAction="Expand">
            <asp:TreeNode Text="Section 1" />
            <asp:TreeNode Text="Section 2" />
        </asp:TreeNode>        
        </Nodes>
</asp:TreeView>

In Listing 2, both the Chapter 1 and Chapter 2 TreeNode control's have their SelectAction properties set to the value Expand. When you click these nodes, the SelectedNodeChanged event is not raised on the server (even in the case of downlevel browsers).

One other property of the TreeView control is worth mentioning in this context. You can use the ShowCheckBoxes property of the TreeView control or the ShowCheckBox property of the TreeNode control to display checkboxes with a TreeView control (see Figure 18). For example, the page in Listing 3 enables you to subscribe to one or more newsgroups by selecting checkboxes.

Listing 3. TreeViewCheckBoxes.aspx (C#)

<%@ Page Language="C#" %>

<script runat="server">

    void Subscribe(object sender, EventArgs e)
    {
        Label1.Text = "You are subscribed to:";
        foreach (TreeNode node in TreeView1.CheckedNodes)
            Label1.Text += "<li> " + node.Value;
    }
    
</script>

<html>
<head runat="server">
    <title>TreeView CheckBoxes</title>
</head>
<body>
    <form id="form1" runat="server">

    <asp:TreeView
        id="TreeView1"
        ShowCheckBoxes="Leaf"
        ImageSet="News"
        Runat="Server">
    <Nodes>
    <asp:TreeNode 
        Text="ASP.NET Newsgroups"
        SelectAction="Expand">
        <asp:TreeNode
            Text="ASP.NET"
            Value="Microsoft.public.dotnet.framework.aspnet" />
        <asp:TreeNode
            Text="ADO.NET"
            Value="Microsoft.public.dotnet.framework.adonet" />
    </asp:TreeNode>    
    </Nodes>    
    </asp:TreeView>
    <br />
    <asp:Button 
        Text="Subscribe"
        OnClick="Subscribe" 
        Runat="Server" />
    <p>&nbsp;</p>
    <asp:Label ID="Label1" Runat="Server" />

    </form>
</body>
</html>

Listing 3. TreeViewCheckBoxes.aspx (Visual Basic .NET)

<%@ Page Language="VB" %>

<script runat="server">

    Sub Subscribe(ByVal sender As Object, ByVal e As EventArgs)
        Label1.Text = "You are subscribed to:"
        For Each node As TreeNode In TreeView1.CheckedNodes
            Label1.Text += "<li> " & node.Value
        Next
    End Sub
    
</script>

<html>
<head id="Head1" runat="server">
    <title>TreeView CheckBoxes</title>
</head>
<body>
    <form id="form1" runat="server">

    <asp:TreeView
        id="TreeView1"
        ShowCheckBoxes="Leaf"
        ImageSet="News"
        Runat="Server">
    <Nodes>
    <asp:TreeNode 
        Text="ASP.NET Newsgroups"
        SelectAction="Expand">
        <asp:TreeNode
            Text="ASP.NET"
            Value="Microsoft.public.dotnet.framework.aspnet" />
        <asp:TreeNode
            Text="ADO.NET"
            Value="Microsoft.public.dotnet.framework.adonet" />    
    </asp:TreeNode>    
    </Nodes>    
    </asp:TreeView>
    <br />
    <asp:Button ID="Button1" 
        Text="Subscribe"
        OnClick="Subscribe" 
        Runat="Server" />
    <p>&nbsp;</p>
    <asp:Label ID="Label1" Runat="Server" />

    </form>
</body>
</html>

Notice that the ShowCheckBoxes property of the TreeView control in Listing 3 is set to the value Leaf. This causes the checkboxes to be displayed only next to leaf elements of the TreeView. When a user clicks the Subscribe button, the Subscribe method executes and the values of the checked nodes are displayed in a Label control.

ms379604.treeview_fig18(en-US,VS.80).gif

Figure 18. Displaying a TreeView with check boxes

Displaying a Navigation Menu with a TreeView Control

Another common use for the TreeView control is for displaying a navigation menu. You can display a navigation menu with the TreeView control by binding the control to a SiteMapDataSource control.

The SiteMapDataSource control exposes Web site navigation information in a standard way by using a standard API. All of the navigation controls in an application can feed off of the same SiteMapDataSource control to display navigation information. For example, the pages in an application might contain TreeView controls, Menu controls, and SiteMapPath controls that all use the same SiteMapDataSource as their data source.

The advantage of using the TreeView control with a SiteMapDataSource is that Web site navigation information can be maintained in a central location. The default provider for the SiteMapDataSource control is the XmlSiteMapProvider. This provider, by default, retrieves site navigation information from a file named Web.sitemap, which must appear in the root directory of your ASP.NET application. You can modify the navigational relationships between pages in your Web application by modifying this single Web.sitemap file.

For example, Listing 4 contains a simple Web.sitemap file.

Listing 4. Web.sitemap

<?xml version="1.0" encoding="utf-8" ?>
<siteMap>
<siteMapNode url="Default.aspx" title="Home">
<siteMapNode title="Products"  description="Our Products">
   <siteMapNode 
      url="Product1.aspx" 
      title="ASP.NET TicTacToe"  
      description="Tic Tac Toe for ASP.NET" />
   <siteMapNode 
      url="Product2.aspx" 
      title="ASP.NET Checkers" 
      description="Checkers for ASP.NET" />
</siteMapNode>
<siteMapNode title="Services"  description="Our Services">
   <siteMapNode 
      url="Service1.aspx" 
      title="ASP.NET Consulting"  
      description="Best ASP.NET Consulting" />
   <siteMapNode 
      url="Service2.aspx" 
      title="ASP.NET Training"  
      description="Best ASP.NET Training" />
</siteMapNode>
</siteMapNode>
</siteMap>

Notice that the file in Listing 3 is nothing more than a simple XML file that describes the structure of a Web site. This file describes a Web site that contains two sections: a Products section and a Services section.

You can bind a TreeView control to a SiteMapDataSource, and ultimately the Web.sitemap file, by setting the TreeView control's DataSourceID property. The page in Listing 5 demonstrates how you can use a TreeView control to display the contents of the Web.sitemap file (see Figure 19).

Listing 5. NavigationTreeView.aspx

<%@ Page Language="C#" %>
<html>
<head>
    <title>Navigation TreeView</title>
</head>
<body>
    <form id="form1" runat="server">

    <asp:TreeView 
        DataSourceID="SiteMapDataSource1"
        ImageSet="XPFileExplorer"
        Runat="Server" />
    
    <asp:SiteMapDataSource
        ID="SiteMapDataSource1"
        Runat="Server" />
    
    </form>
</body>
</html>

ms379604.treeview_fig19(en-US,VS.80).gif

Figure 19. Displaying a navigation menu with the TreeView control

Binding to Database Data with a TreeView Control

Binding a TreeView control to database data is a little more complicated than binding to other types of data. Unlike an XML file, a database table does not intrinsically represent the parent/child relationships between its elements. Therefore, when working with database data, you must take extra steps to represent the database data hierarchically.

There are two types of database parent/child relationships you might want to represent with a TreeView control. You might want to represent the relationship between the rows in two different database tables. Alternatively, you might want to represent the relationship between the rows in a single database table. We'll investigate both scenarios.

Displaying Multiple Database Tables in a TreeView Control

The Northwind database (the sample database included with Microsoft SQL Server) contains both a Categories and Products database table. You can use the TreeView control to display the list of categories. When you click a category, you can display the list of matching products (see Figure 20).

ms379604.treeview_fig20(en-US,VS.80).gif

Figure 20. Displaying the Categories and Products tables

The page in Listing 6 displays the contents of the Categories and Products tables.

Listing 6. MasterDetailTreeView.aspx (C#)

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>

<script runat="server">

    const string connectionString = 
      "Server=localhost;Integrated Security=True;Database=Northwind";
    
    void Page_Load()
    {
        if (!IsPostBack)
            PopulateNodes();
    }

    void PopulateNodes()
    {
        DataSet dst = GetTreeViewData();
        foreach (DataRow masterRow in dst.Tables["Categories"].Rows)
        {
            TreeNode masterNode = 
              new TreeNode((string)masterRow["CategoryName"]);
            TreeView1.Nodes.Add(masterNode);
            foreach (DataRow childRow in 
              masterRow.GetChildRows("Children"))
            {
                TreeNode childNode = 
                  new TreeNode((string)childRow["ProductName"]);
                masterNode.ChildNodes.Add(childNode);
            }
        }
    }   
        
    DataSet GetTreeViewData()
    {    
        SqlConnection con = new SqlConnection(connectionString);
        SqlDataAdapter dadCats = new 
          SqlDataAdapter("SELECT * FROM Categories", con);
        SqlDataAdapter dadProducts = new 
          SqlDataAdapter("SELECT * FROM Products", con);
        DataSet dst = new DataSet();
        dadCats.Fill(dst, "Categories");
        dadProducts.Fill(dst, "Products");
        dst.Relations.Add("Children", 
   dst.Tables["Categories"].Columns["CategoryID"], 
   dst.Tables["Products"].Columns["CategoryID"]);
        return dst;
    }
    
</script>

<html>
<head>
    <title>Master/Detail TreeView</title>
</head>
<body>
    <form id="form1" runat="server">
    
    <asp:TreeView
        ID="TreeView1"
        Runat="Server" />
        
    </form>
</body>
</html>

Listing 6. MasterDetailTreeView.aspx (Visual Basic .NET)

<%@ Page Language="VB" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>

<script runat="server">

    Const connectionString As String = _
      "Server=localhost;Integrated Security=True;Database=Northwind"
    
    Sub Page_Load()
        if NOT IsPostBack Then
            PopulateNodes()
        End If
    End Sub

    Sub PopulateNodes()
        Dim dst As DataSet = GetTreeViewData()
        for each masterRow As DataRow in dst.Tables("Categories").Rows
            Dim masterNode As New _
              TreeNode(masterRow("CategoryName").ToString())
            TreeView1.Nodes.Add(masterNode)
            for each childRow As DataRow in _
              masterRow.GetChildRows("Children")
                Dim childNode As New _
                  TreeNode(childRow("ProductName").ToString())
                masterNode.ChildNodes.Add(childNode)
            next
        Next
    End Sub   
        
    Function GetTreeViewData() As DataSet
        Dim con As SqlConnection = new SqlConnection(connectionString)
        Dim dadCats As SqlDataAdapter = new _
          SqlDataAdapter("SELECT * FROM Categories", con)
        Dim dadProducts As SqlDataAdapter = new _
          SqlDataAdapter("SELECT * FROM Products", con)
        Dim dst As DataSet = new DataSet()
        dadCats.Fill(dst, "Categories")
        dadProducts.Fill(dst, "Products")
        dst.Relations.Add("Children", _ 
            dst.Tables("Categories").Columns("CategoryID"), _ 
            dst.Tables("Products").Columns("CategoryID"))
        return dst
    End Function
    
</script>

<html>
<head>
    <title>Master/Detail TreeView</title>
</head>
<body>
    <form id="form1" runat="server">
    
    <asp:TreeView
        ID="TreeView1"
        Runat="Server" />
        
    </form>
</body>
</html>

When the page in Listing 6 is first loaded, the PopulateNodes() method is called. This method programmatically adds all of the nodes to the TreeView control. A new TreeNode control is created for each row in the Categories table. Additional TreeNode controls are created for each row in the Products table.

The database data is represented by a DataSet returned by the GetTreeViewData() method. This method loads a DataTable, which represents the Categories table, and a DataTable, which represents the Products table into a DataSet. A DataRelation is defined between the two DataTable objects in the DataSet.

Displaying a Single Database Table in a TreeView Control

Imagine that you want to build a threaded discussion group for a Web application. You want to enable users to post new messages and post replies to the messages and nest the messages infinitely deep.

You can represent the messages in a threaded discussion group with the database table schema illustrated in Figure 21.

ms379604.treeview_fig21(en-US,VS.80).gif

Figure 21. The Discuss Database table

The Discuss database table has four columns named MessageID (an identity column), ParentID, Subject, and Body. The ParentID field represents the relationship between the different messages in the table. Some sample data is illustrated in Figure 22.

ms379604.treeview_fig22(en-US,VS.80).gif

Figure 22. Sample Discuss data

You can use the TreeView control contained in the page in Listing 7 to display the contents of the Discuss database table.

Listing 7. TreeViewDiscuss.aspx (C#)

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<script runat="server">
    
    const string connectionString = 
      @"Data Source=.\SQLExpress;Integrated 
        Security=True;AttachDBFilename=|DataDirectory|Discuss.mdf";
    
    void Page_Load()
    {
        if (!IsPostBack)
            PopulateNodes();
    }

    void PopulateNodes()
    {
        DataTable messages = GetTreeViewData();
        DataView threads = GetThreads(messages);
        foreach (DataRowView row in threads)
        {
            TreeNode threadNode = new TreeNode();
            threadNode.Text = row["Subject"].ToString();
            threadNode.Value = row["MessageID"].ToString();
            TreeView1.Nodes.Add(threadNode);
            AddReplies(messages, threadNode);
        }
    }

    DataTable GetTreeViewData()
    {
        SqlConnection con = new SqlConnection(connectionString);
        SqlDataAdapter dad = new 
          SqlDataAdapter("SELECT * FROM Discuss", con);
        DataTable dtbl = new DataTable();
        dad.Fill(dtbl);
        return dtbl;
    }

    DataView GetThreads(DataTable discuss)
    {
        DataView view = new DataView(discuss);
        view.RowFilter = "ParentID=0";
        return view;
    }

    void AddReplies(DataTable messages, TreeNode node)
    {
        DataView replies = GetReplies(messages, node.Value);
        foreach (DataRowView row in replies)
        {
            TreeNode replyNode = new TreeNode();
            replyNode.Text = row["Subject"].ToString();
            replyNode.Value = row["MessageID"].ToString();
            node.ChildNodes.Add(replyNode);
            AddReplies(messages, replyNode);
        }
    }

    DataView GetReplies(DataTable messages, string messageID)
    {
        DataView view = new DataView(messages);
        view.RowFilter = "ParentID=" + messageID;
        return view;
    }
    
        
</script>

<html>
<head>
    <title>TreeView Discuss</title>
</head>
<body>
    <form id="form1" runat="server">

    <asp:TreeView
        ID="TreeView1"
        ImageSet="Arrows"
        Runat="Server" />

    </form>
</body>
</html>

Listing 7. TreeViewDiscuss.aspx (Visual Basic .NET)

<%@ Page Language="VB" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<script runat="server">
    
    const connectionString As String = _
      "Data Source=.\SQLExpress;Integrated " &
      "Security=True;AttachDBFilename=|DataDirectory|Discuss.mdf"
    
    sub Page_Load()
        if Not IsPostBack then
            PopulateNodes()
        End If
    end sub

    sub PopulateNodes()
        Dim messages As DataTable = GetTreeViewData()
        Dim threads As DataView = GetThreads(messages)
        for each row As DataRowView in threads
        
            Dim threadNode as new TreeNode()
            threadNode.Text = row("Subject").ToString()
            threadNode.Value = row("MessageID").ToString()
            TreeView1.Nodes.Add(threadNode)
            AddReplies(messages, threadNode)
        next
    end sub

    function GetTreeViewData() As DataTable
        Dim con As New SqlConnection(connectionString)
        Dim dad As New SqlDataAdapter("SELECT * FROM Discuss", con)
        Dim dtbl As New DataTable()
        dad.Fill(dtbl)
        Return dtbl
    end function

    function GetThreads(discuss As DataTable) As DataView
        Dim view As new DataView(discuss)
        view.RowFilter = "ParentID=0"
        return view
    end function

    sub AddReplies(messages As DataTable, node As TreeNode)
        Dim replies As DataView = GetReplies(messages, node.Value)
        for each row As DataRowView in replies
            Dim replyNode as new TreeNode()
            replyNode.Text = row("Subject").ToString()
            replyNode.Value = row("MessageID").ToString()
            node.ChildNodes.Add(replyNode)
            AddReplies(messages, replyNode)
        next
    end sub

    Function GetReplies(ByVal messages As DataTable, _
      ByVal messageID As String) As DataView
        Dim view As New DataView(messages)
        view.RowFilter = "ParentID=" & messageID
        Return view
    End Function
    
        
</script>

<html>
<head>
    <title>TreeView Discuss</title>
</head>
<body>
    <form id="form1" runat="server">

    <asp:TreeView
        ID="TreeView1"
        ImageSet="Arrows"
        Runat="Server" />

    </form>
</body>
</html>

The page in Listing 7 programmatically populates the TreeNode controls in the TreeView control. When the page first loads, the PopulateNodes() method is called. This method calls the GetTreeViewData() method to load the Discuss database table into a DataTable. Next, the top-level messages are retrieved by the GetThreads() method.

The message replies are added to the TreeView by the AddReplies() method. The AddReplies() method is recursive. It calls itself in order to show replies to replies, replies to replies to replies, and so on. The end result is displayed in Figure 23.

ms379604.treeview_fig23(en-US,VS.80).gif

Figure 23. Displaying a discussion forum with a TreeView

Using Dynamic Tree Node Population

In all of the sample TreeView pages that we have created to this point, we've loaded all of the TreeView data at once. This is an impractical approach when working with large amounts of data. For example, if you want to use the TreeView control to display a threaded discussion group containing thousands of messages, then loading all of the messages at once would be overly demanding on your server.

Fortunately, the TreeView control provides you with the option of loading TreeView nodes on demand. In other words, the contents of the child nodes are not loaded until you click the parent node. The page in Listing 8 demonstrates how you can take advantage of this feature.

Listing 8. DynamicTreeView.aspx (C#)

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<script runat="server">
  
    const string connectionString = @"Data Source=.\SQLExpress;Integrated Security=True;AttachDBFilename=|DataDirectory|Discuss.mdf";
  
    void TreeView1_TreeNodePopulate(object sender, TreeNodeEventArgs e)
    {
        SqlConnection con = new SqlConnection(connectionString);
        SqlCommand cmd = new 
          SqlCommand("SELECT * FROM Discuss WHERE ParentID=@ParentID", 
          con);
        cmd.Parameters.AddWithValue("@ParentID", e.Node.Value);
        con.Open();
        using (con)
        {
            SqlDataReader dtr = cmd.ExecuteReader();
            while (dtr.Read())
            {
                TreeNode newNode = new TreeNode();
                newNode.PopulateOnDemand = true;
                newNode.Text = dtr["Subject"].ToString();
                newNode.Value = dtr["MessageID"].ToString();
                e.Node.ChildNodes.Add(newNode);
            }
        }
    }
    
</script>

<html>
<head>
    <title>Dynamic TreeView</title>
</head>
<body>
    <form id="form1" runat="server">

    <asp:TreeView
        id="TreeView1"
        ImageSet="Arrows"
        ShowLines="true"
        ExpandDepth="1"
        OnTreeNodePopulate="TreeView1_TreeNodePopulate" 
        Runat="Server">
        <Nodes>
        <asp:TreeNode 
            Text="Messages" 
            Value="0"
            PopulateOnDemand="true" />
        </Nodes>
    </asp:TreeView>    

    <p>
    <%=DateTime.Now %>
    </p>

    </form>
</body>
</html>

Listing 8. DynamicTreeView.aspx (Visual Basic .NET)

<%@ Page Language="VB" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<script runat="server">
  
    const connectionString As String = _
     "Data Source=.\SQLExpress;Integrated " & _
     "Security=True;AttachDBFilename=|DataDirectory|Discuss.mdf"
  
    sub TreeView1_TreeNodePopulate(sender As Object, _
      e As TreeNodeEventArgs)
        Dim con As new SqlConnection(connectionString)
        Dim cmd As new _
    SqlCommand("SELECT * FROM Discuss WHERE ParentID=@ParentID", _
          con)
        cmd.Parameters.AddWithValue("@ParentID", e.Node.Value)
        con.Open()
        Try
            Dim dtr As SqlDataReader = cmd.ExecuteReader()
            while dtr.Read()
                Dim newNode As new TreeNode()
                newNode.PopulateOnDemand = true
                newNode.Text = dtr("Subject").ToString()
                newNode.Value = dtr("MessageID").ToString()
                e.Node.ChildNodes.Add(newNode)
            End While
        Finally
            con.Close()
        End Try
    End Sub
    
</script>

<html>
<head>
    <title>Dynamic TreeView</title>
</head>
<body>
    <form id="form1" runat="server">

    <asp:TreeView
        id="TreeView1"
        ImageSet="Arrows"
        ShowLines="true"
        ExpandDepth="1"
        OnTreeNodePopulate="TreeView1_TreeNodePopulate" 
        Runat="Server">
        <Nodes>
        <asp:TreeNode 
            Text="Messages" 
            Value="0"
            PopulateOnDemand="true" />
        </Nodes>
    </asp:TreeView>    

    <p>
    <%=DateTime.Now %>
    </p>

    </form>
</body>
</html>

The TreeView control in Listing 8 contains a single statically declared node. This node has it PopulateOnDemand property set to the value True. In addition, the TreeView control has an event handler defined for its TreeNodePopulate event.

When you click a node in the TreeView, the TreeView1_TreeNodePopulate() method executes and retrieves any child nodes associated with the node clicked. Since the child nodes are not retrieved until the node is actually clicked, the Web server is not required to perform as much work to render the TreeView.

You can populate tree nodes on demand when using the TreeView control with any browser. The TreeView control supports an additional feature that only works in the case of uplevel browsers. This additional feature is called client-side node population.

When you display the page in Listing 8 in Microsoft Internet Explorer 5.5 and higher or Netscape Navigator 6.0 and higher, the page is not posted back to the server. Instead, the TreeView node is dynamically populated on the client. You can verify this behavior by checking the time displayed in the Label control at the bottom of the page in Listing 8. When you expand a node, the time does not change.

When used with Microsoft Internet Explorer, client-side node population takes advantage of the Microsoft.XMLHttp ActiveX component. When used with Netscape Navigator, the XMLHttpRequest object is used. Both objects work in a similar manner. They perform an HTTP GET operation against the Web server to retrieve the additional tree nodes.

Using the Menu Control

The Menu control, like the TreeView control, can be used to display hierarchical data. You can use the Menu control to display static data, site map data, and database data. The main difference between the two controls is their appearance.

Displaying a Simple Menu

The simplest method of displaying a menu is to statically define the individual menu items. The menu items are represented, appropriately enough, by the MenuItem control. The page in Listing 9 demonstrates how you can create a simple static menu that displays the table of contents for a book.

Listing 9. SimpleMenu.aspx (C#)

<%@ Page Language="c#" %>
<script runat="server">

    void Menu1_MenuItemClick(Object s, 
     System.Web.UI.WebControls.MenuEventArgs e)
    {
        Label1.Text = "You selected " + e.Item.Text;
    }
    
</script>
<html>
<head>
    <title>Simple Menu</title>
</head>
<body>
    <form id="form1" runat="server">

    <asp:Menu
        id="Menu1"
        OnMenuItemClick="Menu1_MenuItemClick"
        Runat="Server">
    <Items>
    <asp:MenuItem Text="Part I">
        <asp:MenuItem 
            Text="Chapter 1" />
        <asp:MenuItem 
            Text="Chapter 2" />
    </asp:MenuItem>
    <asp:MenuItem Text="Part II">
        <asp:MenuItem 
            Text="Chapter 3" />
        <asp:MenuItem 
            Text="Chapter 4" />
    </asp:MenuItem>
    </Items>
    </asp:Menu>

    <p>&nbsp;</p>
    <asp:Label ID="Label1" Runat="Server" />
    
    </form>
</body>
</html>

Listing 9. SimpleMenu.aspx (Visual Basic .NET)

<%@ Page Language="vb" %>
<script runat="server">

    Sub Menu1_MenuItemClick(ByVal sender As Object, _
      ByVal e As System.Web.UI.WebControls.MenuEventArgs)
        Label1.Text = "You selected " & e.Item.Text
    End Sub
    
</script>
<html>
<head>
    <title>Simple Menu</title>
</head>
<body>
    <form id="form1" runat="server">

    <asp:Menu
        id="Menu1"
        OnMenuItemClick="Menu1_MenuItemClick"
        Runat="Server">
    <Items>
    <asp:MenuItem Text="Part I">
        <asp:MenuItem 
            Text="Chapter 1" />
        <asp:MenuItem 
            Text="Chapter 2" />
    </asp:MenuItem>
    <asp:MenuItem Text="Part II">
        <asp:MenuItem 
            Text="Chapter 3" />
        <asp:MenuItem 
            Text="Chapter 4" />
    </asp:MenuItem>
    </Items>
    </asp:Menu>

    <p>&nbsp;</p>
    <asp:Label ID="Label1" Runat="Server" />
    
    </form>
</body>
</html>

By default, the Menu control displays a vertically oriented menu (see Figure 24). In the next section, you'll learn how to create a more traditional looking menu.

ms379604.treeview_fig24(en-US,VS.80).gif

Figure 24. The default appearance of the Menu control

The default appearance of the Menu control is appropriate for displaying a navigation menu for a Web page. You'll notice that the menu items are declared with the <Items> tag. The MenuItem control's Text property determines the text displayed by each menu item. The MenuItem control also supports a Value property that can be used to associate a hidden value with each MenuItem.

When you click a MenuItem in the page in Listing 9, a MenuItemClick event is raised. The page handles the MenuItemClick event by retrieving the value of the Text property associated with the selected MenuItem and displaying the text in a Label control.

By default, when you click the text associated with a MenuItem, the page is posted back to itself. You can modify this default behavior by setting the NavigateUrl property. When the NavigateUrl property is set to a valid URL, clicking a menu item links to the URL. In addition, both the Menu and MenuItem controls support a Target property that you can use to specify the frame or window into which the new page is loaded.

For example, the page in Listing 10 opens a new window when you click a menu item.

Listing 10. MenuNavigateUrl.aspx

<html>
<head>
    <title>Menu NavigateUrl</title>
</head>
<body>
    <form id="form1" runat="server">

    <asp:Menu ID="Menu1"
        Target="_blank"
        Runat="Server">
    <Items>
    <asp:MenuItem Text="Products">
        <asp:MenuItem 
            Text="Product 1" 
            NavigateUrl="Product1.aspx" />
        <asp:MenuItem 
            Text="Product 2" 
            NavigateUrl="Product2.aspx" />
    </asp:MenuItem>
    </Items>
    </asp:Menu>

    </form>
</body>
</html>

Controlling the Appearance of the Menu Control

As you saw in the previous section, by default, the Menu control displays a vertical menu. If you want a more traditional desktop application menu, then you can modify the Menu control's Orientation property.

The page in Listing 11 more closely resembles the type of menu used in a desktop application (see Figure 25).

Listing 11. HorizontalMenu.aspx

<html>
<head>
    <title>Horizontal Menu</title>
</head>
<body>
    <form id="form1" runat="server">

    <asp:Menu ID="Menu1"
        Orientation="Horizontal"
        Runat="Server">

  <StaticMenuStyle 
    BackColor="#eeeeee"
    BorderColor="Black"
    BorderStyle="Solid"
    BorderWidth="1" />

  <StaticMenuItemStyle
    HorizontalPadding="5" />      

  <DynamicMenuStyle
    BorderColor="Black"
    BorderStyle="Solid"
    BorderWidth="1" />

  <DynamicMenuItemStyle
    BackColor="#eeeeee"
    HorizontalPadding="5" 
    VerticalPadding="3" />  

    <Items>
    <asp:MenuItem Text="File">
        <asp:MenuItem 
            Text="New" />
        <asp:MenuItem 
            Text="Open..." />
    </asp:MenuItem>
    <asp:MenuItem Text="Edit">
        <asp:MenuItem 
            Text="Cut"  
            ImageUrl="Cut.gif" />
        <asp:MenuItem 
            Text="Copy"  
            ImageUrl="Copy.gif" />
        <asp:MenuItem 
            Text="Paste" 
            ImageUrl="Paste.gif" 
            SeparatorImageUrl="Divider.gif" />
        <asp:MenuItem 
            Text="Select All" />
    </asp:MenuItem>
    </Items>
    </asp:Menu>

    </form>
</body>
</html>

ms379604.treeview_fig25(en-US,VS.80).gif

Figure 25. Displaying a horizontal menu with the Menu control

The page in Listing 11 actually illustrates several properties you can modify to control the appearance of the Menu control. First, notice that the Menu control includes StaticMenuStyle, StaticMenuItemStyle, DynamicMenuStyle, and DynamicMenuItemStyle style objects. These style objects are used to specify the formatting of different parts of the Menu control.

Also, notice that several of the MenuItem controls have an ImageUrl property defined. The ImageUrl property provides the path to an image to display next to the menu item. Finally, notice that the Paste MenuItem has a SeparatorImageUrl property defined. This property provides the path to an image that appears below the MenuItem.

Displaying a Navigation Menu with the Menu Control

You can use the Menu control with the SiteMapDataSource. The SiteMapSource provides you with a standard way to represent navigation information for a Web application. This is the same data source that can be used with the TreeView and SiteMapPath controls.

For example, the page in Listing 12 contains both a Menu control and a TreeView control. Both controls consume the same SiteMapDataSource.

Listing 12. SiteMapMenu.aspx

<html>
<head>
    <title>SiteMap Menu</title>
</head>
<body>
    <form id="form1" runat="server">

    <table width="100%">
    <tr>
        <td valign="top" bgcolor="#eeeeee" width="200">
        
        <asp:TreeView
            DataSourceID="SiteMapDataSource1"
            Runat="Server" />
        
        </td>
        <td valign="top">
        
        <asp:Menu
            DataSourceID="SiteMapDataSource1"
             
            Orientation="Horizontal"
            Runat="Server" />
            
        </td>
    </tr>
    </table>

    <asp:SiteMapDataSource
        ID="SiteMapDataSource1"
        ShowStartingNode="false"
        Runat="Server" />

    </form>
</body>
</html>

The page in Listing 12 is divided into two columns by an HTML table. A TreeView control displays a navigation menu in the left column and a Menu control displays the same navigation information in the right column (see Figure 26). Both controls are bound to a SiteMapDataSource control (located at the bottom of the page).

ms379604.treeview_fig26(en-US,VS.80).gif

Figure 26. Binding a Menu control to a SiteMapDataSource

Notice that the SiteMapDatasource control has its ShowStartingNode property set to the value False. The Home node contained in the Web.sitemap file is not displayed. Both the TreeView and Menu controls render navigational elements starting with the second level of elements.

Binding a Menu Control to Database Data

You cannot bind a Menu control directly to a non-hierarchical data source, such as a database table. However, you can programmatically add MenuItem controls to a Menu control. Therefore, by performing a little work, you can get a Menu control to display the contents of a database table.

A Menu control has an Items property that represents its menu items. You can add MenuItem controls to this collection. You can also add MenuItem controls to the ChildItems collection of any MenuItem control. In this way, you can build the hierarchy of menu items rendered by a Menu control.

The page in Listing 13 demonstrates how you can use a Menu control to display the Categories and Products database tables (see Figure 27).

Click here for larger image.

Figure 27. Displaying Database data in a Menu control

Listing 13. DatabaseMenu.aspx (C#)

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>

<script runat="server">

    
    const string connectionString = 
      "Server=localhost;Integrated Security=True;Database=Northwind";
    
    void Page_Load()
    {
        if (!IsPostBack)
            PopulateMenu();
    }

    void PopulateMenu()
    {
        DataSet dst = GetMenuData();
        foreach (DataRow masterRow in 
          dst.Tables["Categories"].Rows)
        {
            MenuItem masterItem = new 
              MenuItem((string)masterRow["CategoryName"]);
            Menu1.Items.Add(masterItem);
            foreach (DataRow childRow in 
              masterRow.GetChildRows("Children"))
            {
                MenuItem childItem = new 
                  MenuItem((string)childRow["ProductName"]);
                masterItem.ChildItems.Add(childItem);
            }
        }
    }   
        
    DataSet GetMenuData()
    {    
        SqlConnection con = new SqlConnection(connectionString);
        SqlDataAdapter dadCats = new 
          SqlDataAdapter("SELECT * FROM Categories", con);
        SqlDataAdapter dadProducts = new 
          SqlDataAdapter("SELECT * FROM Products", con);
        DataSet dst = new DataSet();
        dadCats.Fill(dst, "Categories");
        dadProducts.Fill(dst, "Products");
        dst.Relations.Add("Children", 
          dst.Tables["Categories"].Columns["CategoryID"], 
          dst.Tables["Products"].Columns["CategoryID"]);
        return dst;
    }
    
</script>

<html>
<head>
    <title>Database Menu</title>
</head>
<body>
    <form id="form1" runat="server">

    <asp:Menu
        id="Menu1"
        Orientation="Horizontal"
        Runat="Server">
        <StaticMenuStyle
            BackColor="#eeeeee" 
            BorderStyle="solid" 
            BorderColor="Black"
            BorderWidth="1" />
        <DynamicMenuStyle 
            BackColor="#eeeeee" 
            BorderStyle="solid" 
            BorderColor="Black"
            BorderWidth="1" />
    </asp:Menu>

    </form>
</body>
</html>

Listing 13. DatabaseMenu.aspx (Visual Basic .NET)

<%@ Page Language="VB" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>

<script runat="server">

    Const connectionString As String = 
      "Server=localhost;Integrated Security=True;Database=Northwind"
    
    Sub Page_Load()
        If Not IsPostBack Then
            PopulateMenu()
        End If
    End Sub

    Sub PopulateMenu()
        Dim dst As DataSet = GetMenuData()
        For Each masterRow As DataRow In 
          dst.Tables("Categories").Rows
            Dim masterItem As New 
              MenuItem(masterRow("CategoryName").ToString())
            Menu1.Items.Add(masterItem)
            For Each childRow As DataRow In 
              masterRow.GetChildRows("Children")
                Dim childItem As New 
                  MenuItem(childRow("ProductName").ToString())
                masterItem.ChildItems.Add(childItem)
            Next
        Next
    End Sub
        
    Function GetMenuData() As DataSet
        Dim con As New SqlConnection(connectionString)
        Dim dadCats As New _
          SqlDataAdapter("SELECT * FROM Categories", con)
        Dim dadProducts As New _
          SqlDataAdapter("SELECT * FROM Products", con)
        Dim dst As New DataSet()
        dadCats.Fill(dst, "Categories")
        dadProducts.Fill(dst, "Products")
        dst.Relations.Add("Children", _
            dst.Tables("Categories").Columns("CategoryID"), _
            dst.Tables("Products").Columns("CategoryID"))
        Return dst
    End Function
    
</script>

<html>
<head>
    <title>Database Menu</title>
</head>
<body>
    <form id="form1" runat="server">

    <asp:Menu
        id="Menu1"
        Orientation="Horizontal"
        Runat="Server">
        <StaticMenuStyle
            BackColor="#eeeeee" 
            BorderStyle="solid" 
            BorderColor="Black"
            BorderWidth="1" />
        <DynamicMenuStyle 
            BackColor="#eeeeee" 
            BorderStyle="solid" 
            BorderColor="Black"
            BorderWidth="1" />
    </asp:Menu>

    </form>
</body>
</html>

When the page in Listing 13 first loads, the PopulateMenu() method is called. This method calls the GetMenuData() method to retrieve a DataSet that represents the Categories and Products tables. A MenuItem is added to the Menu control's Items collection for each row in the Categories table.

As each category menu item is added to the Menu control, all of the child product MenuItem controls are added. The products are added to the ChildItems collection for each category MenuItem.

Conclusion

We've all had to create tree views and menus for our Web applications. Prior to ASP.NET 2.0, building these user interface elements required a substantial expenditure of time writing messy, difficult-to-debug, client-side JavaScript code. The ASP.NET 2.0 TreeView and Menu controls shield us from the JavaScript while providing us with the flexibility of building Web applications that offer rich, client-side user experiences.

Related Books

 

About the author

Stephen Walther wrote the best-selling book on ASP.NET, ASP.NET Unleashed. He was also the architect and lead developer of the ASP.NET Community Starter Kit, a sample ASP.NET application produced by Microsoft. He has provided ASP.NET training to companies across the United States, including NASA and Microsoft, through his company Superexpert (http://www.superexpert.com).

Show: