TreeView Amnesia And You

Persisting Node Expanded States in the ASP.NET TreeView

In keeping with my theme of spackling over the shortcomings of native ASP.NET controls, it is time to address our frustratingly forgetful friend, the TreeView.

While based on a Windows control rather than merely being an enhanced web control (like a TextBox or a DropDownList), the TreeView could be one of the most useful tools available to a web programmer. It naturally mimics the hierarchical structure of most reasonably complex websites, and groups things so that our tiny ridiculous minds can navigate a large number of options with ease.

However, one contentious bone that we can pick with this handy tool is that it does not automatically persist its node expansion states across pages. Imagine that you are visiting the bigbrotherholdingcompanyllc.com website, in which their unwitting (and untesting) web developer has simply dragged and dropped a TreeView control onto a Master Page, bound it to the sitemap, and called it navigation. You drill down through eighteen confusing layers of categories of levels of departments, and choose a likely target. But, bless its little heart node, by the time you get to where you are going, the TreeView had curled itself back up, and is regarding you with the cold eyes of a stranger.

My personal theory (because you care) is that long ago, when it was cooked up in the research kitchen over at Microsoft, the web TreeView had functionality built in to persist the state of its nodes. Tragically, in the process of stuffing the TreeView into the ASP.NET box, a junior developer inadvertently snapped this part off and it rolled behind his desk.  There it remains to this day, attracting dust and hair next to the UPS and a small pile of tabs from Mountain Dew cans.

The Plan

And so, we are left with two options. The first is that we fly to Redmond, break into Microsoft, and comb the cubicles for the TreeView‘s absent persistence. Plan B is to put on our control developer hats and do it ourselves. For my part, I’m short on airfare and breaking/entering skills, so let’s crack open Visual Studio and inherit the TreeView:

// yes, we're using c#
using System.Web;
using System.Web.UI.WebControls;

namespace Aptera.BlogSamples
{
    // You know, because elephants remember.
    public class ElephantTreeView : TreeView
    {
    }
}

The first decision that must be made when persisting anything is where to store it. To its credit, the TreeView persists its ViewState quite nicely in a postback situation. Our problem is at the inter-page level. In ASP.NET, the most obvious choice when it comes to cross-page storage is the Session. Before we can store anything in the Session, we have to give it a unique name. The TreeNode is a second-class class among web control classes, as it does not come with a convenient ID property, which would be useful here. Alas, we must rise above this inequity and assemble our own, from the ID of the TreeView, the node’s Depth, and the node’s Text:

private string GetNodeKey(TreeNode node)
{
    return string.Format("{0}_{1}_{2}", this.ID, node.Depth, node.Text);
}

The puritans among you will raise the point that this particular combination of strings is not necessarily unique from node to node. I will parry this point by asserting that if you have two nodes in the same tree, at the same depth, with the same text, and they have child nodes necessitating expansion persistence, then what you actually must deal with first is an ID Ten-T problem (more on that here). I will happily leave this as an exercise for the reader.

Moving On

Once we have established a method to uniquely identify each TreeNode, we will need a method to store a TreeNode‘s Expanded property in the Session, using its unique name as the key:

protected virtual void SaveNodeState(TreeNode node)
{
    // determine the node's unique key string
    string key = GetNodeKey(node);

    // store the node's expansion state
    HttpContext.Current.Session[key] = node.Expanded;
}

We also need a method that will walk the tree (yay, recursion!) and restore each node’s Expanded value:

protected virtual void RestoreNodeState(TreeNode node)
{
    // set the node's expansion state
    // from the session
    object expanded = HttpContext.Current.Session[GetNodeKey(node)];
    if (expanded != null)
    {
        node.Expanded = (bool)expanded;
    }

    // set the node's childrens' expansion
    // states from the session
    foreach (TreeNode childNode in node.ChildNodes)
    {
        RestoreNodeState(childNode);
    }
}

Thusly, we have laid our foundation for node expanded state persistence. All that remains is to hook our functions into the right TreeView events and make them work for the money. First, when the TreeView is DataBound, we want to restore whatever states may be saved in the Session:

protected override void OnDataBound(System.EventArgs e)
{
    // restore expanded state for each
    // top-level node and its children
    foreach (TreeNode node in this.Nodes)
    {
        RestoreNodeState(node);
    }

    base.OnDataBound(e);
}

Then, when any node is expanded or collapsed, we want to persist its expanded state:

protected override void OnTreeNodeExpanded(TreeNodeEventArgs e)
{
    // persist the state of the expanded node
    if (Page.IsPostBack)
    {
        SaveNodeState(e.Node);
    }

    // go about our other OnTreeNodeExpanded business
    base.OnTreeNodeExpanded(e);
}

protected override void OnTreeNodeCollapsed(TreeNodeEventArgs e)
{
    // persist the state of the collapsed node
    if (Page.IsPostBack)
    {
        SaveNodeState(e.Node);
    }

    // go about our other OnTreeNodeCollapsed business
    base.OnTreeNodeExpanded(e);
}

Note that we only call SaveNodeState() if we are in a PostBack.  This is because the node expanded and collapsed events are not only raised in response to user action, but when the TreeView is initially loaded and its nodes assume their default expanded state.  If we did not check for PostBack before attempting to save the node’s state, each node’s default state would overwrite its state saved in the Session, and our control would be rendered about as stateful as a bag of hair.

So every “i” has been crossed, every “t” has been dotted, and we’re ready to move on with our persistent-TreeView-enhanced lives now, right?.

Read The Following To Avoid Hours Of Fruitless Debugging (Not That I Am Speaking From Personal Experience)

Sadly, no. There’s one more change we have to make before this elephant tree business will remember anything. In their infinite wisdom, the ASP.NET TreeView developers included a feature called EnableClientScript that grants client-side node expand and collapse powers, bypassing the OnTreeNodeExpanded/OnTreeNodeCollapsed server-side handlers altogether. Without these events firing, we can’t persist our nodes’ states, and it’s back to programming Excel Macros for us. So before you place a head-shaped dent in the wall behind you, let’s add an OnLoad handler and turn this feature off:

protected override void OnLoad(System.EventArgs e)
{
    // prevent head-shaped dent
    this.EnableClientScript = false;
    base.OnLoad(e);
}

Persistence Pays Off

Now add the web.config line for our overridden control, so it can be used site-wide (under configuration/system.web/pages/controls):

<add tagPrefix="aptera" namespace="Aptera.BlogSamples"/>

And we can drop our control onto our site’s Master Page and connect it to a sitemap:

<aptera:ElephantTreeView ID="MyTreeView" runat="server"
    DataSourceID="MySiteMapDataSource" />
<asp:SiteMapDataSource ID="MySiteMapDataSource" runat="server" />

Now run the site…

…collapse a tree node…

…navigate to another page…

And finally, the nightmare is over. TreeView Amnesia is cured.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s