harriyott.com

Friday, March 23, 2007

Adding dynamic nodes to ASP.NET site maps at runtime by deriving from StaticSiteMapProvider

Adding a static sitemap to an ASP.NET website is straightforward. Creating a dynamic sitemap is harder, but there are several articles that Google finds describing how. Adding dynamic items to an existing sitemap seems harder still: you can't add items by deriving from XmlSiteMapProvider, as the list of SiteMapNodes is read only.

The method for adding items to a sitemap has a few steps:
  1. Create your sitemap file in the usual way.
  2. Derive a class from StaticSiteMapProvider.
  3. Add your sitemap to the Web.Config file.
  4. Check that this works by adding a tree view connected to a sitemap data source.
  5. Implement the overridden base-class methods.
  6. Read the sitemap file into an XmlDocument.
  7. Dynamically add new elements to the XmlDocument.
  8. Recurse through the XmlDocument creating a tree of SiteMapNodes.


In step 1, your sitemap file may look something like this:

<?xml version="1.0" encoding="utf-8" ?>

<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >

    <siteMapNode url="~/default.aspx" title="Home"  description="Home page">

        <siteMapNode url="~/contact.aspx" title="Contact"  description="Contact us" />

        <siteMapNode url="~/products.aspx" title="Products"  description="Our products" />

    </siteMapNode>

</siteMap>



For step 2, just create the class at the moment:

namespace Harriyott.Web

{

    public class DynamicSiteMapProvider : StaticSiteMapProvider

...

You'll be prompted (by a tiny rectangle) to implement the abstract class, which you should do, and replace the exceptions with return null; for now.

For step 3, add the site map provider in the usual way, but change the type to your new class name, including the namespace:

<system.web>

    <siteMap defaultProvider="main">

        <providers>

            <add siteMapFile="Web.sitemap"  name="main" type="Harriyott.Web.DynamicSiteMapProvider"/>

        </providers>

    </siteMap>



To see what we have so far, step 4 is to add a new .aspx page with a tree view displaying the site map.

<asp:TreeView ID="treeSiteMap" runat="server" DataSourceID="smdsHarriyott" />

<asp:SiteMapDataSource ID="smdsHarriyott" runat="server" />



Although this should run ok, no items are displayed yet. This is because the StaticSiteMapProvider class doesn't actually process the sitemap XML, because we're returning null. To see the static site map items, switch the type in the Web.Config back to the default:

type="System.Web.XmlSiteMapProvider"



OK, so step 5 is to implement the overridden methods and properties properly. Or properlies property.

private String _siteMapFileName;

private SiteMapNode _rootNode = null;

 

public override SiteMapNode RootNode

{

    get { return BuildSiteMap(); }

}

 

public override void Initialize(string name, NameValueCollection attributes)

{

    base.Initialize(name, attributes);

    _siteMapFileName = attributes["siteMapFile"];

}

 

protected override SiteMapNode GetRootNodeCore()

{

    return RootNode;

}

 

protected override void Clear()

{

    lock (this)

    {

        _rootNode = null;

        base.Clear();

    }

}



The first bit is quite straightforward. There's a root node to add sitemap nodes to, and we're saving the filename of the sitemap file. The interesting bit is to create the nodes. This is done in BuildSiteMap():

private const String SiteMapNodeName = "siteMapNode";

 

public override SiteMapNode BuildSiteMap()

{

    lock (this)

    {

        if (null == _rootNode)

        {

            Clear();

            // Load the sitemap's xml from the file.

            XmlDocument siteMapXml = LoadSiteMapXml();

            // Create the first site map item from the top node in the xml.

            XmlElement rootElement =

                (XmlElement)siteMapXml.GetElementsByTagName(

                SiteMapNodeName)[0];

            // This is the key method - add the dynamic nodes to the xml

            AddDynamicNodes(rootElement);

            // Now build up the site map structure from the xml

            GenerateSiteMapNodes(rootElement);

        }

    }

    return _rootNode;

}



Four main things going on here. (Four things. Please don't be confused by me saying things like "And fourthly, step 8", as I'm still trying to keep track of the list at the beginning.) Firstly, in step 6, the XML file is loaded:

private XmlDocument LoadSiteMapXml()

{

    XmlDocument siteMapXml = new XmlDocument();

    siteMapXml.Load(AppDomain.CurrentDomain.BaseDirectory + _siteMapFileName);

    return siteMapXml;

}



Secondly, we're selecting the top siteMapNode from the loaded XML. Thirdly, and this is the important step 7, we're going to add our dynamic nodes:

private void AddDynamicNodes(XmlElement rootElement)

{

    // Add some football teams

    XmlElement teams = AddDynamicChildElement(rootElement, "", "Football Teams", "List of football teams created dynamically");

    AddDynamicChildElement(teams, "~/teams.aspx?name=Watford", "Watford", "Watford's team details");

    AddDynamicChildElement(teams, "~/teams.aspx?name=Reading", "Reading", "Reading's team details");

    AddDynamicChildElement(teams, "~/teams.aspx?name=Liverpool", "Liverpool", "Liverpool's team details");

 

    XmlElement sheffield = AddDynamicChildElement(teams, "", "Sheffield", "There is more than one team in Sheffield");

    AddDynamicChildElement(sheffield, "~/teams.aspx?name=SheffieldUnited", "Sheffield United", "Sheffield United's team details");

    AddDynamicChildElement(sheffield, "~/teams.aspx?name=SheffieldWednesday", "Sheffield Wednesday", "Sheffield Wednesday's team details");

 

    XmlElement manchester = AddDynamicChildElement(teams, "", "Manchester", "There is more than one team in Manchester");

    AddDynamicChildElement(manchester, "~/teams.aspx?name=ManchesterUnited", "Manchester United", "Manchester United's team details");

    AddDynamicChildElement(manchester, "~/teams.aspx?name=ManchesterCity", "Manchester City", "Manchester City's team details");

}



I'm just doing this in memory to keep the example short, but you could generate stuff from your database. The AddDynamicChildElement returns a new child that's been added to the current node:

private static XmlElement AddDynamicChildElement(XmlElement parentElement, String url, String title, String description)

{

    // Create new element from the parameters

    XmlElement childElement = parentElement.OwnerDocument.CreateElement(SiteMapNodeName);

    childElement.SetAttribute("url", url);

    childElement.SetAttribute("title", title);

    childElement.SetAttribute("description", description);

 

    // Add it to the parent

    parentElement.AppendChild(childElement);

    return childElement;

}



And fourthly, step 8 is to generate the site map nodes in, er, GenerateSiteMapNodes():

private void GenerateSiteMapNodes(XmlElement rootElement)

{

    _rootNode = GetSiteMapNodeFromElement(rootElement);

    AddNode(_rootNode);

    CreateChildNodes(rootElement, _rootNode);

}

 

private void CreateChildNodes(XmlElement parentElement, SiteMapNode parentNode)

{

    foreach (XmlNode xmlElement in parentElement.ChildNodes)

    {

        if (xmlElement.Name == SiteMapNodeName)

        {

            SiteMapNode childNode = GetSiteMapNodeFromElement((XmlElement)xmlElement);

            AddNode(childNode, parentNode);

            CreateChildNodes((XmlElement)xmlElement, childNode);

        }

    }

}



What's going on here is that we're creating a root sitemap node from the root element in the XML. Then we're recursively finding the XML element's children and adding coresponding sitemap nodes to match the hierarchy. Here's the method that creates a sitemap node from the XML element:

private SiteMapNode GetSiteMapNodeFromElement(XmlElement rootElement)

{

    SiteMapNode newSiteMapNode;

    String url = rootElement.GetAttribute("url");

    String title = rootElement.GetAttribute("title");

    String description = rootElement.GetAttribute("description");

 

    // The key needs to be unique, so hash the url and title.

    newSiteMapNode = new SiteMapNode(this,

        (url + title).GetHashCode().ToString(), url, title, description);

 

    return newSiteMapNode;

}



So that's it. We now have a static site map with items dynamically added to it. Just to prove it, here's what mine looks like on an Angel Delight coloured background:

Site map screen shot

To save you piecing this all together in a new class, you can download the full source file.

[Tags: ]

26 Comments:

Blogger Thomas said...

Nice, but you REALLY need some CODE formatter...
Check out the one I use (LGPL licensed) at e.g. http://ajaxwidgets.com/Blogs/thomas/javascript_ajax_intellisense_i.bb
But I got some tips about the Sitemap functionality of ASP.NET 2.0 that I found amusing here :)

Cheers!

March 28, 2007 9:18 PM  
Anonymous Simon said...

Hi Thomas,

Glad the sitemap stuff was amusing. You're absolutely right about the formatting. I use the CopySourceAsHtml plugin, which is fine, but my template isn't wide enough for it, and the line spacing is too big. The one on your site looks much better, unless javascript is disabled. I'll take your comment on board though, and see if I can find something better.

March 28, 2007 11:01 PM  
Anonymous Anonymous said...

You R's!

March 29, 2007 9:46 AM  
Anonymous Frank said...

Thanks for an excellent article! I'm putting this to use right now with a site but I wanted to be able to dynamically add siteMapNode elements at a specified location in the XML ... took me far too long to figure this bit out
XmlNamespaceManager nsmgr = new XmlNamespaceManager(siteMapXml.NameTable);

string docNsURIName = siteMapXml.DocumentElement.NamespaceURI;

nsmgr.AddNamespace("map", docNsURIName );

XmlElement targetElement = (XmlElement)siteMapXml.SelectSingleNode("/map:siteMap/map:siteMapNode/map:siteMapNode/map:siteMapNode[@title='Dynamically added content']", nsmgr);


// This is the key method - add the dynamic nodes to the xml

//AddDynamicNodes(rootElement);
AddDynamicNodes(targetElement);

April 07, 2007 12:01 AM  
Anonymous Simon said...

Hi Frank,

Glad you're using this, I was hoping somebody would find it useful. Thanks for the additional code - that makes it more useful.

April 07, 2007 1:11 PM  
Anonymous Anonymous said...

Hi, thanks for the article it is just what i was looking for. but i have a noob problem. I want to change the nodes when the page loads (in the page load event). how do i do this with your code?

thanks
daniel

April 09, 2007 10:47 AM  
Anonymous Anonymous said...

and another noobie question. if i dynamically change the sitemap like this, wouldn't it effect the other users?, i.e someone else using the site, will everybody see there own custom dynamically generated sitemap?

thanks again
daniel

April 09, 2007 11:01 AM  
Anonymous Simon said...

Hi Daniel,

I don't think you can actually change this in the page load event, which is why I had to go through all this in the first place!

As far as affecting the other users, this will only be a problem if you are generating user-specific links in the sitemap. If not, then everyone will see the same dynamic sitemap.

April 11, 2007 11:20 AM  
Blogger Christoph said...

This rocks. Exactly what I was looking for. Now if I can just figure out how to dynamically set images for my treeview nodes at runtime...

April 19, 2007 5:42 PM  
Anonymous Pushkar Joshi said...

How it write in web.sitemap file?

May 08, 2007 8:49 AM  
Anonymous Pushkar Joshi said...

There is one function AddNode(), where the AddNode Function is written? and what id does?

May 08, 2007 9:43 AM  
Anonymous Simon said...

Hi Pushkar,

Firstly, it doesn't actually write into the web.sitemap file, it loads the file, and adds new nodes to the structure in memory. It doesn't write the changes back to the web.sitemap file.

Secondly, AddNode is in the .NET framework base class, StaticSiteMapProvider, which my class derives from. The full details are here: http://tinyurl.com/3afh3c

May 08, 2007 6:24 PM  
Anonymous Anonymous said...

hi

can u tel me how can i create a dynamic tree view in ASP 2.0 by reading an XML file

like i read one XML file and according to that it create a Tree view

let me kn if anybody kn in C# e-mail me ashi_jain2001@yahoo.com

thanks ashish

June 26, 2007 12:43 AM  
Blogger Sam said...

This is a pretty handy class. Building on Franks work, I made the namespace manager a global property and created a new method to get elements by url.

First the property:

private XmlNamespaceManager _NamespaceManager;
public XmlNamespaceManager NamespaceManager
{
get { return _NamespaceManager; }
}

Then in BuildSiteMap() add this after LoadSiteMapXml():

_NamespaceManager = new XmlNamespaceManager(siteMapXml.NameTable);
_NamespaceManager.AddNamespace("map", siteMapXml.DocumentElement.NamespaceURI);

Finally, the method itself:

private XmlElement GetElementByUrl(XmlElement rootElement, string url)
{
return rootElement.SelectSingleNode(String.Format("//map:{0}[@url='{1}']", SiteMapNodeName, url), NamespaceManager) as XmlElement;
}

Then in AddDynamicNodes you can do:

XmlElement aboutElement = GetElementByUrl(rootElement, "~/about/default.aspx");
if (aboutElement != null)
{
AddDynamicChildElement(aboutElement, "~/about/foo.aspx", "About Foo", "More about Foo");
}

However, I do have one question. How can I rebuild the sitemap when something changes (e.g. I'm working on document management and when the documents title is changed, or the document is deleted, it needs to be changed in the site map). Making Clear a public method may help, but if you have a lot of nodes, or if documents frequently get added or removed, this could slow the site down. It may be better to have 'add / edit node' public methods that just changed the single node.

August 23, 2007 11:43 AM  
Anonymous Anonymous said...

THANKS A LOT FOR THE CODE. IT WORKS GREAT FOR ME.

October 16, 2007 11:25 PM  
Blogger Jason said...

I set it up so that it adds child elements from a database table. Works great only thing is I have to recompile the DynamicSiteMap.cs file every time I add a new database record for the menu. Otherwise it doesn't show my new record in the menu. How do I get this to work without recompiling?

October 24, 2007 2:11 PM  
Anonymous a different Simon said...

excellent - just what I wanted :) thanks Simon, and thanks to Frank and Sam

Sam, to answer your question, just stick boolean member variable and a method to set it to true, and check it in BuildSiteMap... IE

private bool m_RefreshSitemap;

public void RefreshSitemap()
{
m_RefreshSitemap = true;
}

public override SiteMapNode BuildSiteMap()
{
lock (this)
{
if (null == m_RootNode || m_RefreshSitemap)
{
// clipped //
m_RefreshSitemap = false;
}
}
return m_RootNode;
}

then when you want a page to refresh the site map, like on update of your documents, do this:

DynamicSiteMapProvider aProvider = (DynamicSiteMapProvider)SiteMap.Provider;
aProvider.RefreshSitemap();

December 19, 2007 7:25 AM  
Blogger usha said...

Simon! Good Article!!!
But I am facing some problem here.
Actually in my sitemap file I am using Javascript functions.
In the url I am calling a javascript function then
It is giving me this error-

'javascript: void fnOpenReport('CustSat')' is not a valid virtual path.

To the existing sitemap file,to this file I wanted to add some filenames(reading from a particular directory) using your code.But I am getting the above error.
Can you please help me out


-Usha.

May 06, 2008 6:31 AM  
Blogger Simon said...

Hi Usha,

I would suggest inserting a placeholder link in the sitemap, and putting the javascript in a separate file. You could navigate the DOM (or something like jQuery) to find the right menu item, and add an event handler for the click.

May 06, 2008 9:13 AM  
Blogger usha said...

Simon,
I took Javascript function in a separate aspx page which calls report viewer control to display some reports.
I wanted to post my sitemap file:

"siteMapNode title="Select a Report"
siteMapNode url="" title="Generated Reports" description=""
siteMapNode url="javascript: void fnOpenReport('ReadyToRelease');" title="Ready To Release" description=""
siteMapNode url="javascript: void fnOpenReport('TopPriority')" title="Top Priority" description=""
siteMapNode url="javascript: void fnOpenReport('On Hold')" title="Hold" description=""


I removed the < and > symbols as it is giving message saying html tags are not acceptible

May 06, 2008 11:02 AM  
Blogger usha said...

Simon,
This is the hierarchy of the menu items:
Select a Report- Generated Reports-Ready To Release
Top Priority
On Hold
This is static sitemap file
I wanted to add one more menu item to "Select a Report" dynamically which consists of some menu items.

-Usha

May 06, 2008 11:06 AM  
Blogger Simon said...

OK, try taking out the javascript from the url in the dynamic site map, and giving it a normal url.

In your separate javascript file, write a function that runs when the page loads, which finds the report links and adds the javascript function call to the onclick event.

May 06, 2008 11:14 AM  
Blogger Mr. Millionaire said...

Hello...

mmmm Frank... where will i put your code....

on which node to add???

mmmm

noob here....

thanks Simon

May 23, 2008 8:17 PM  
Blogger Mr. Millionaire said...

Oh.. I found it out already...

thanks man...

you rocks!!!

weeeee....

You dont have any idea how happy I am ...

The force created you to answer this this.... dammmnnn....

frank,simon , sam.... you rule!!!

May 23, 2008 8:42 PM  
Blogger Mr. Millionaire said...

hi simon...
Im happing a problem when i refresh the site..
it doesnt reflect the database changes

May 24, 2008 12:33 AM  
Anonymous LIUFA said...

Hello Simon is there a way to add nodes without first Parent Node in Web.sitemap? in your example that would be without "Home". I use same Data Provider in Sitemap and Navigation bar. Navigation bar shows only 2 levels so items in deeper levels are hidden. Thanks in advice.

June 06, 2008 11:34 AM  

Post a Comment

Links to this post:

Create a Link

<< Home