Thursday, January 19, 2006
« ActiveSync Guest Only | Main | Determine if ASP.NET Page is running in ... »

NOTE: I have been receiving a lot of traffic on this posting and don't see (m)any other solutions out there so I decided to port the code to Visual Basic and repost the entry. Most of the writeup is in C#, but a VB solution is included in the download. Sorry for the VB gripes, but I am trying to learn a bit here.... On another note, is reposting a social faux pas in the blog world?


I frequently use an ASP.NET TreeView as a navigation menu. This works well on a single page but if you include the control on a Master Page, it looses its expansion state as you move from one content page to another.

After populating my TreeView, either expand or collapse all of the nodes to insure that there is a consistent default behavior. Then restore your TreeView expansion state to a previously saved state by invoking the RestoreTreeView method of the TreeViewState class:

// set the default state of all nodes.

TreeViewMain.CollapseAll();

 

// get the saved state of all nodes.

new TreeViewState().RestoreTreeView(TreeViewMain, this.GetType().ToString());

Before navigating to a new page, save the existing state for future use but first you need to overcome the urge to use the NavigateUrl property on a TreeNode. Using this property will generate an HTML hyperlink tag on your TreeView which will cause the browser to initiate the transfer to the new Url. Control is not returned to the server and you will never have the opportunity to save the existing state. Instead, wire up the TreeView_SelectedNodeChanged event for the TreeView, put your page address into the Node.Value property and use a Response.Redirect():

protected void TreeViewMain_SelectedNodeChanged(object sender, EventArgs e)

{

    if (TreeViewMain.SelectedNode.Value != string.Empty)

    {

        Response.Redirect(TreeViewMain.SelectedNode.Value);

    }

}

Next, save the TreeView state by subscribing to the TreeView_Unload event. This event is fired just before the control is unloaded from memory. Pass your TreeView to the SaveTreeView method of the TreeViewState class:

protected void TreeViewMain_Unload(object sender, EventArgs e)

{

    // save the state of all nodes.

    new TreeViewState().SaveTreeView(TreeViewMain, this.GetType().ToString());

}

 

Within the SaveTreeView and RestoreTreeView methods of the class, I recursively walk the nodes collection and either save or restore the TreeNode.Expanded property in/from a generic list of type <bool?>. The TreeNode uses a nullable boolean value to store this state so I used the same type in a List.

The expansion state is saved in a Session variable. It is possible that you will want to save the state for two identically named TreeView controls on different master pages within your application. This would cause an issue with the name of the Session variable so I included a 'key' parameter that is concatenated with the ID of the TreeView to form the name of the session variable. You can pass any string value as the key, but using the name of the invoking master page class should eliminate most conflicts. I coded this as 'this.GetType().ToString()' which will return a string of the class type.

Lastly, if the number of nodes exceeds the element count in the List<> object, I simply return from the method. This might happen if the data source for your TreeView changes on the fly. If your data source if routinely changing, you will need to tweak the class to better handle this scenario.

As I indicated earlier, I have now included a VB.NET version of the solution. This is the most VB that I have coded since moving from VB6 to C# when I made the jump to .NET in the early beta days sometime in 2001. I love the simple syntax of C# compared to the wordiness of VB. Take a look as how the two handle generic collections:

VB.NET:

 

Public Sub RestoreTreeView(ByVal treeView As TreeView, ByVal key As String)

    Dim list As New List(Of Nullable(Of Boolean))

    If HttpContext.Current.Session(key + treeView.ID) IsNot Nothing Then

        list = CType(HttpContext.Current.Session(key + treeView.ID), List(Of Nullable(Of Boolean)))

    End If

 

    RestoreTreeViewIndex = 0

    RestoreTreeViewExpandedState(treeView.Nodes, list)

End Sub


C#:

 

public void RestoreTreeView(TreeView treeView, string key)

{

    RestoreTreeViewIndex = 0;

    RestoreTreeViewExpandedState(treeView.Nodes,

        (List<bool?>)HttpContext.Current.Session[key + treeView.ID] ?? new List<bool?>());

}

Is there a cleaner way of doing this in VB? Please let me know. Also is there a way of instantiating a class and invoking a method in one statement in VB.NET?

VB.NET:

Dim treeViewState As New TreeViewState()

treeViewState.SaveTreeView(TreeViewMain, Me.GetType.ToString())

 

C#:

new TreeViewState().SaveTreeView(TreeViewMain, this.GetType().ToString());

I am the first to admit that VB.NET has its place. I know that when it is time to work with Office automation, I will be using VB.NET so please, no holy wars.

Download the complete CS and VB solutions:
http://download.binaryocean.com/TreeViewSaveStateSolution.zip

Here is the complete CS class. The VB version is included in the download:

using System;

using System.Collections.Generic;

using System.Web;

using System.Web.UI.WebControls;

 

public class TreeViewState

{

    public void SaveTreeView(TreeView treeView, string key)

    {

        List<bool?> list = new List<bool?>();

        SaveTreeViewExpandedState(treeView.Nodes, list);

        HttpContext.Current.Session[key + treeView.ID] = list;

    }

 

    private int RestoreTreeViewIndex;

 

    public void RestoreTreeView(TreeView treeView, string key)

    {

        RestoreTreeViewIndex = 0;

        RestoreTreeViewExpandedState(treeView.Nodes,

            (List<bool?>)HttpContext.Current.Session[key + treeView.ID] ?? new List<bool?>());

    }

 

    private void SaveTreeViewExpandedState(TreeNodeCollection nodes, List<bool?> list)

    {

        foreach (TreeNode node in nodes)

        {

            list.Add(node.Expanded);

            if (node.ChildNodes.Count > 0)

            {

                SaveTreeViewExpandedState(node.ChildNodes, list);

            }

        }

    }

 

    private void RestoreTreeViewExpandedState(TreeNodeCollection nodes, List<bool?> list)

    {

        foreach (TreeNode node in nodes)

        {

            if (RestoreTreeViewIndex >= list.Count) break;

 

            node.Expanded = list[RestoreTreeViewIndex++];

            if (node.ChildNodes.Count > 0)

            {

                RestoreTreeViewExpandedState(node.ChildNodes, list);

            }

        }

    }

}

kick it on DotNetKicks.com   Thursday, January 19, 2006 10:05:42 AM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [31]  | 
Sunday, January 29, 2006 3:04:54 PM (Pacific Standard Time, UTC-08:00)
Hi Andrew,
Nice article! works like a charm, used it in my composite control. One note, when you add your tree programmatically into a panel, make sure the viewstate of this panel is turned off! Otherwise you get an array out of bounds error.. Also having some trouble with two different trees with different id´s on one page. Need to figure out what goes wrong :-)
cheers,
Frank
Tuesday, January 31, 2006 8:34:13 AM (Pacific Standard Time, UTC-08:00)
Frank,

Thanks for the note. I did some further testing and the class works correctly when placing multiple treeview controls on your page. I would be interested in learning more about your specific situation?

Thanks,
Andrew Robinson
Tuesday, February 28, 2006 6:05:28 AM (Pacific Standard Time, UTC-08:00)
Hi!

The article was usefull for my other learning processes. Has this been tested with dynamic treeviews? Because I'm working on one and I can't get it working. Any suggestions?

Iiro
Sunday, June 04, 2006 6:01:43 AM (Pacific Standard Time, UTC-08:00)
Thank for the article. Although i am still having problem when i set datasource of the treeview. Any suggestions?
Joko
Thursday, July 13, 2006 7:02:01 AM (Pacific Standard Time, UTC-08:00)
nice work learnt internal details of treeview control. I was managing state by storing node values in session. It was giving javascript errror. This solved my problem.
anupam
Tuesday, August 15, 2006 2:45:21 PM (Pacific Standard Time, UTC-08:00)
Seems to work great as long as i don't have "navigateurl" set in the node.
Great job!

thanks
Andreas
Thursday, October 19, 2006 1:25:35 PM (Pacific Standard Time, UTC-08:00)
Thanks for the article!
I had to do some minor modifications, as I am using the treeview together with a site map. But that was easily fixed:

Protected Sub LeftMenuTree_SelectedNodeChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles LeftMenuTree.SelectedNodeChanged
If Not LeftMenuTree.SelectedNode.Value = String.Empty Then
Response.Redirect(LeftMenuTree.SelectedNode.DataPath)
End If
End Sub

Protected Sub LeftMenuTree_TreeNodeDataBound(ByVal sender As Object, ByVal e As TreeNodeEventArgs) Handles LeftMenuTree.TreeNodeDataBound
e.Node.NavigateUrl = ""
End Sub
Tuesday, October 31, 2006 10:36:57 PM (Pacific Standard Time, UTC-08:00)
Hi Andrew,

Thanks Mate for your article. It is very helpful.

thomas
Thomas
Wednesday, December 06, 2006 3:09:29 PM (Pacific Standard Time, UTC-08:00)
Has anyone got this to work where the treenode are coming from a SQL backend, and can someone tell me, or show me, how to get it to highlight the selectednode as well

Thanks
Rob Black
Thursday, December 07, 2006 2:16:48 PM (Pacific Standard Time, UTC-08:00)
This code is cool!!! Onlt problem with this is that the selected node isnt highlighted, do you know how to highlight the selected node as well?
Thanks
Rob Black
Friday, February 09, 2007 8:24:50 AM (Pacific Standard Time, UTC-08:00)
Hey Andrew,

Great article!

Thanks!
Carlo
Tuesday, February 20, 2007 1:47:30 PM (Pacific Standard Time, UTC-08:00)
Hi Andrew,
Great job,
I'm realy thankful to you because your solution solved my problem after 10 hours dealing with damn tree view :)
thanks
aran
Wednesday, April 11, 2007 8:24:33 PM (Pacific Standard Time, UTC-08:00)
Hi Andrew

Great article! Really helped alot.
Minor modification: My tree's datasource is XML, so after setting the DataSourceID and treeView.ExpandAll() calling the RestoreTreeView immedtaitely afterwards rendered nothing. Had to call RestoreTreeView in the treeView_DataBound event.
Works fine!
Thanks
Clinton
Monday, May 07, 2007 2:34:37 PM (Pacific Standard Time, UTC-08:00)
To save the Selected node:

add to bottom of SaveTreeView():
-----------------
If treeView.SelectedNode IsNot Nothing Then
HttpContext.Current.Session(key + treeView.ID + "selected") = treeView.SelectedNode.ValuePath
Else
HttpContext.Current.Session.Remove(key + treeView.ID + "selected")
End If

add to bottom of RestoreTreeView():
-----------------
If HttpContext.Current.Session(key + treeView.ID + "selected") IsNot Nothing Then
Dim selNode As TreeNode = treeView.FindNode(HttpContext.Current.Session(key + treeView.ID + "selected").ToString())
If selNode IsNot Nothing Then
selNode.Selected = True
End If
End If

- Steve Yates
- Two bees or not two bees....Oops! Bumbled that one!

~ Taglines by Taglinator - www.srtware.com ~
Steve Yates
Wednesday, August 29, 2007 7:18:09 AM (Pacific Standard Time, UTC-08:00)
Hi

This is a nice solution for small trees. If you have trees with thousands of nodes then theres a performance hit because each node state is stored. The code can be modified to store indexes to expanded nodes though. This means that there is an object for each expanded node not an object for every tree node.

When expanding, the index needs to be used to obtain the correct treenode in the treenodecollection.

This method works really quickly (we have a tree with thousands of nodes and it expands instantly). E.g. if a user only expands 10 nodes then there will only be 10 indexes stored not thousands.

A pipe-delimited list of 0-based integer indexes into expanded nodes can be used.

Regards
Ahmet
Ahmet
Thursday, September 13, 2007 2:46:18 PM (Pacific Standard Time, UTC-08:00)
Hi ,
This is nice solution to maintain the treenode. But when i am using this code i am facing one problem. I am populating tree nodes dynamically by calling webservice.Assume that i have my tree have expanded upto 4th level and selected one node at 4th level. Based on this i am click on buttons on the right hand side.the code works fine. When i am selecting the node at 2nd level and click on buttons it is showing the screen related to the 4th level because the last viewstate is restoring. How can i make this code to work for 2nd level. I am placing treeview control inside updatepanel. There is no issue with updatepanel. Maintaining the latest selected node in treeview is problem for me.

I would appreciate your help to solve my issue.

Thanks
Viji
Friday, September 28, 2007 12:07:32 AM (Pacific Standard Time, UTC-08:00)
Hi,

Thanks for this article, I am new in Dotnet field. I have gone through this material. Really interesting learning with dotnet and Got some idea to save the treeview savestate.

Instead of saving the expanded node value, You can save state in single session variable. (Note: In my project, I don't have more than 50 nodes in tree view. Eventhough this idea will work fine if treeview will have more than 50 nodes).

The code is

****************************************************************
Imports Microsoft.VisualBasic
Imports System.Collections.Generic

Public Class TreeViewState
Inherits System.Web.UI.Page
Private SaveTreeViewIndex As Integer
Private ExpandStatus As String
Private RestoreTreeViewIndex As Integer
Private ListIndex As Integer

Public Sub SaveTreeView(ByVal treeView As TreeView)
SaveTreeViewIndex = 0
SaveTreeViewExpandedState(treeView.Nodes)
If ExpandStatus <> "" Then
Session("tvStatus") = Trim(ExpandStatus)
Else
Session.Remove("tvStatus")
End If

End Sub

Private Sub SaveTreeViewExpandedState(ByVal nodes As TreeNodeCollection)
For Each node As TreeNode In nodes
If node.Expanded Then
ExpandStatus = ExpandStatus + (SaveTreeViewIndex).ToString + " "
SaveTreeViewIndex += 1
If node.ChildNodes.Count > 0 Then
SaveTreeViewExpandedState(node.ChildNodes)
End If
Else
SaveTreeViewIndex += 1
If node.ChildNodes.Count > 0 Then
SaveTreeViewExpandedState(node.ChildNodes)
End If
End If
Next
End Sub

Public Sub RestoreTreeView(ByVal treeView As TreeView)
If Session("tvStatus") Is Nothing Then
Exit Sub
End If
ExpandStatus = Session("tvStatus")
RestoreTreeViewIndex = 0
ListIndex = 0
Dim List() As String = ExpandStatus.Split(" ")
RestoreTreeViewExpandedState(treeView.Nodes, List)
End Sub

Private Sub RestoreTreeViewExpandedState(ByVal nodes As TreeNodeCollection, ByVal List() As String)
For Each node As TreeNode In nodes
If ListIndex >= List.Length Then Exit Sub
If RestoreTreeViewIndex = CInt(List(ListIndex).ToString) Then
node.Expanded = True
ListIndex += 1
RestoreTreeViewIndex += 1
If node.ChildNodes.Count > 0 Then
RestoreTreeViewExpandedState(node.ChildNodes, List)
End If
Else
RestoreTreeViewIndex += 1
If node.ChildNodes.Count > 0 Then
RestoreTreeViewExpandedState(node.ChildNodes, List)
End If
End If
Next
End Sub

End Class

**********************************************************

ThankYou all,
Dinesh
Thursday, December 06, 2007 9:56:11 AM (Pacific Standard Time, UTC-08:00)
Wow, works like a charm. Thanks a lot
Frank
Tuesday, January 15, 2008 4:30:34 AM (Pacific Standard Time, UTC-08:00)
Your article helped me a lot...Thank You Very Much..
maya
Friday, January 25, 2008 5:10:39 AM (Pacific Standard Time, UTC-08:00)
Thanks heaps!
This solved a problem I was working on for far too long. Excellent article!
You should really try and get this page higher up on the specific Google search results, because all the other (forum) postings on the same topic don't really offer any valuable solution nor do they sum it up in such a good way.
Tuesday, March 11, 2008 9:47:03 AM (Pacific Standard Time, UTC-08:00)
This is a great solution. Thanks for the code. I was just wondering if there would be a significant performance hit on the web server as the number of page requests increase given the use of session state to store the tree view state. Any thoughts on how i can measure the performance changes in such a case? Thanks.
Nik
Sunday, April 13, 2008 7:38:58 AM (Pacific Standard Time, UTC-08:00)
very nice and helpful article.

Ahmed
Ahmed
Friday, May 02, 2008 11:16:02 AM (Pacific Standard Time, UTC-08:00)
Hi Andrew,

Over 2 years later, and your code is still helping people on the net (like me :) ). Just wanted to make a quick comment about supporting dynamic trees.

I changed your list from type List<bool?> to type Dictionary<string, bool?>, and then I use each node's ValuePath as the key. This way, when restoring the tree, I expand any tree nodes that existed before, and ignore any newly added or removed nodes.

--- To Save ---
list.Add(node.ValuePath, node.Expanded);

--- To Restore ---
if (list.ContainsKey(node.ValuePath))
{
node.Expanded = list[node.ValuePath];
}
Adam
Thursday, July 24, 2008 2:10:11 AM (Pacific Standard Time, UTC-08:00)
Thank you so much. I was struggling for 2 days to fix this issue. Thanks a lot.
Jans Mary
Thursday, July 31, 2008 6:24:52 AM (Pacific Standard Time, UTC-08:00)
Cool article. You have made my day!
Neil
Sunday, November 02, 2008 9:07:24 PM (Pacific Standard Time, UTC-08:00)
very thanks
Tuesday, January 13, 2009 2:27:55 AM (Pacific Standard Time, UTC-08:00)
I add the following in my masterpage Page_load()
TreeViewMain.CollapseAll();
new TreeViewState().RestoreTreeView(TreeViewMain, this.GetType().ToString());

and then add those two eventhandler to my treeview
and also add the savetreeview class

but my treeview state still not saved.
Am I doing something wrong here?

Tuesday, April 28, 2009 5:08:24 AM (Pacific Standard Time, UTC-08:00)
Hi,

I make this, this is working. My treeview is inside a master page and i get the datasource from a sitemap file.

In my master page Load_page

protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
TreeViewMain.DataBind();
new CmtTreeViewState().restoreTreeView(TreeViewMain);
}
}


and in the class which manages, this:

public void restoreTreeView(TreeView pTreeView)
{
if (pTreeView.SelectedNode != null)
{
pTreeView.CollapseAll();
pTreeView.SelectedNode.Selected = true;
pTreeView.SelectedNode.Parent.Expand();
pTreeView.SelectedNode.Expand();
}
}

Only this, Have fun!!!!
Angel
Tuesday, May 12, 2009 1:41:49 PM (Pacific Standard Time, UTC-08:00)
Andrew,

Nice code! I was really having trouble with the whole TreeView state thing. The only problem I had with your code was that it seemed to be one state off. I solved this by saving the state just before I sent it off to the selected page.

I'm probably just not using right. Anyway, nice job and thanks for putting it out there.

Chris
Chris
Thursday, June 25, 2009 12:15:05 PM (Pacific Standard Time, UTC-08:00)
Thank you so much! I have been struggling with treeview / sitemaps for a while - this article was a major piece in forming my understanding.

My biggest issue was that I could not find a way to use an XML sitemap file and still respond to onSelectedNodeChanged (which I still can't do). But this article enabled me to do the things I need to do and avoid the issue. It would be great to see an overall reference for working with sitemaps and treeviews.

Take care -
Murray
Tuesday, July 14, 2009 12:36:10 AM (Pacific Standard Time, UTC-08:00)
Grait job, I was getting mad with treeview state
Antonio
Name
E-mail
Home page

Comment (Some html is allowed: a@href@title, strike) where the @ means "attribute." For example, you can use <a href="" title=""> or <blockquote cite="Scott">.  

Enter the code shown (prevents robots):

Live Comment Preview