ASP.NET Tree View Populate Nodes From Client with SiteMap Data Source

Joe Audette

Updated

I ran into a challenge trying to get the PopulateNodesFromClient (PopulateOnDemand) on a TreeView control working to my expectations this weekend and didn’t see much about this while googling so thought I would post some notes about how I got it working.

First I should say I’m using the CSSAdapter version from here:
http://www.asp.net/CSSAdapters/TreeView.aspx not the normal TreeView so I don’t know if the same issues would be experienced using the normal TreeView but I suspect they would.

aspnet treeview

There is a walkthrough tutorial showing how to use Populate On Demand, but if you follow that you will get off the tracks right away. If the TreeView is bound to a SiteMapDataSource you will never be able to get the OnTreeNodePopulate event to fire or at least I never could, it never hits a breakpoint there.

Since this event is where you would populate the child nodes you might think you really need to get this event to fire, that’s what I thought but it’s not correct. The data binding to the SiteMapDataSource handles this event for you apparently and won’t let you in there to muck things up.

Nevertheless, there is a problem to be solved. If I set ExpandDepth=0 which is what you want when using populate on demand it doesn’t automatically expand to the node that represents the current page. You have to expand the menu down to the current page starting from its topmost parent.

When it’s a postback (i.e. clicking the expand icon) the TreeView knows how far to expand, but when it’s a new request for a page down deep in the menu you must expand down to it and select it. A postback happens when you click the expand icon next to a tree node, but when you click an actual menu item that is just a get request for the URL in the menu item.

In the case where it is a postback you want to make sure you are not re-binding the menu as this is not needed and will make the TreeView lose track of where it should expand to, so I have code like this before my DataBinding code:

if (Page.IsPostBack)
{
// return if menu already bound
if(treeMenu1.Nodes.Count > 0) return;
}
treeMenu1.PathSeparator = '|';
treeMenu1.DataSourceID = this.siteMapDataSource.ID;
treeMenu1.DataBind();

Note that I also specified the | character as the PathSeparator, the PathSeparator is used to construct the valuePath for each node in the menu and is also used when you want to find a specific node so when Some Page has Some Sub Page beneath it, the valuePath for Some Sub Page is:
Some Page|Some Sub Page

I have a utility function, SiteUtils.GetActivePageValuePath() to help me calculate the expected valuePath for the current page then I use this valuePath to find and expand the topmost parent on down to the current page as shown below.

You have to start at the topmost parent and work your way down because you can’t get to the child node until the parent has been expanded.

String valuePath = SiteUtils.GetActivePageValuePath();
if (!valuePath.Contains(treeMenu1.PathSeparator.ToString()))
{
    ExpandToValuePath(valuePath);
}
else
{
    string[] pathSegments 
    = valuePath.Split(new char[] { '|' });
   //expand top parent first
    string pathToExpand = pathSegments[0];
    ExpandToValuePath(pathToExpand);
    for (int i = 1; i < pathSegments.Length; i++)
    {
    pathToExpand = pathToExpand + treeMenu1.PathSeparator + pathSegments[i];
    ExpandToValuePath(pathToExpand);
    }
}
TreeNode nodeToSelect = treeMenu1.FindNode(valuePath);
if (nodeToSelect != null) nodeToSelect.Selected = true;


private void ExpandToValuePath(string valuePath)
{
    if (valuePath.Length > 0)
    {
    TreeNode treeNode;
    treeNode = treeMenu1.FindNode(valuePath);

    if (treeNode != null)
    {
        if (
        (treeNode.Expanded == null) 
        || (treeNode.Expanded.Equals(false))
        )
        {
        treeNode.Expand();
        }
    }
    }

}

In my TreeNodeDataBound event shown below, I’m filtering nodes by role, but more important to this topic is at the bottom of this event, for any SiteMapeNode that has children, I set the node.

PopulateOnDemand = true;
protected void treeMenu1_TreeNodeDataBound(object sender, TreeNodeEventArgs e)
{
TreeView menu = (TreeView)sender;
mojoSiteMapNode mapNode = (mojoSiteMapNode)e.Node.DataItem;if (mapNode.MenuImage.Length > 0)
{
e.Node.ImageUrl = mapNode.MenuImage;
}

if (!(
    (isAdmin)
    || (
    (isContentAdmin)
    && (mapNode.Roles != null)
    && (!(mapNode.Roles.Count == 1)
    && (mapNode.Roles[0].ToString() == "Admins")
       )
    )
    || ((isContentAdmin) && (mapNode.Roles == null))
    || (
    (mapNode.Roles != null)
    && (WebUser.IsInRoles(mapNode.Roles))
    )
))
{
if (e.Node.Depth == 0)
{
    menu.Nodes.Remove(e.Node);
}
else
{
    TreeNode parent = e.Node.Parent;
    if (parent != null)
    {
    parent.ChildNodes.Remove(e.Node);
    }
}

}

if (mapNode.HasChildNodes)
{
e.Node.PopulateOnDemand = true;
}

}

protected void treeMenu1_TreeNodeExpanded(object sender, TreeNodeEventArgs e)
{
if (!e.Node.ValuePath.Contains(treeMenu1.PathSeparator.ToString()))
{
ExpandToValuePath(e.Node.ValuePath);
}
else
{
string[] pathSegments
= e.Node.ValuePath.Split(new char[] { '|' });

string pathToExpand = pathSegments[0];
ExpandToValuePath(pathToExpand);
for (int i = 1; i < pathSegments.Length; i++)
{
    pathToExpand = pathToExpand + treeMenu1.PathSeparator + pathSegments[i];
    ExpandToValuePath(pathToExpand);
}
}

}

protected void treeMenu1_TreeNodePopulate(object sender, TreeNodeEventArgs e)
{
//this never seems to fire

}


Hope these notes help anyone from having to struggle too much with this. The load on demand feature of the TreeView is pretty cool and could be a performance enhancement for a deep page hierarchy compared to rendering all the markup for all the menu items at once.

My implementation is in mojoPortal and can be obtained by checking out branches/2.x from svn for anyone who wants to dig further into the details.