Adding dynamic nodes to ASP.NET site maps at runtime by deriving from StaticSiteMapProvider
XmlSiteMapProvider, as the list of SiteMapNodes is read only.The method for adding items to a sitemap has a few steps:
- Create your sitemap file in the usual way.
- Derive a class from
StaticSiteMapProvider. - Add your sitemap to the Web.Config file.
- Check that this works by adding a tree view connected to a sitemap data source.
- Implement the overridden base-class methods.
- Read the sitemap file into an
XmlDocument. - Dynamically add new elements to the
XmlDocument. - Recurse through the
XmlDocumentcreating a tree ofSiteMapNodes.
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:

To save you piecing this all together in a new class, you can download the full source file.
[Tags: StaticSiteMapProvider SiteMap dynamic XmlSiteMapProvider]
38 Comments:
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!
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.
You R's!
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);
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.
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
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
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.
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...
How it write in web.sitemap file?
There is one function AddNode(), where the AddNode Function is written? and what id does?
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
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
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.
THANKS A LOT FOR THE CODE. IT WORKS GREAT FOR ME.
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?
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();
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.
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.
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
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
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.
Hello...
mmmm Frank... where will i put your code....
on which node to add???
mmmm
noob here....
thanks Simon
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!!!
hi simon...
Im happing a problem when i refresh the site..
it doesnt reflect the database changes
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.
hi,
ur article is so nice.
I have the problem plz give me
sum suggestions
In my site there are 3 domains
1.admin
2.user
3.associate
In my site 60 to 70 pages are there
for admin all pages allowes to him
and then goes to user here users are countryspecific
if the user belongs to india he allows only 25 pages,if the user belongs to us he allows 35 pages related to his country............
like that there are 29 countries
users are there how to allow the
users to specific pages ??????????????
plz send ur views
Thnx & Regards
radi
You don't want to use your own made up hash when creating a new sitemapnode. External sources need to know how to compare these to know if the current node is selected. E.g if you have want to highlight the selected node.
This works with the .NET better
newSiteMapNode = new SiteMapNode(this,url.ToString(), url, title, description);
Hi Simmon,
My requirement is something like
I check the user after he logs in against the database and get the user type. Based on the user type I need to display the menu items (top menu) and for each menu item there is a different left nav. Also can you please tell me how I can use your code to achieve the same.
Thanks,
anonymous
You need to change step 7 to add the nodes based on the user, and set the menu's data source to be the site map.
Hi Simon,
I have a Master Page Which Users the Dynamic SiteMap to show URLs, but now i want to add different set of nodes based on User Click from
out side the DynamicSiteMapProvider Class, and again bind the site map to my SiteMapDataSource
Thanks,
Favaz
Thank you very much, great help!
I have a StringCollection and wan't to add a Child Element to every item in the StringCollection.
But how is tis possible to make to a reality?
I cant use:
XmlElement farrSplitedString[1] = AddDynamicChildElement(rootSection, "http://", arrSplitedString[1], "");
"arrSplitedString[1]" dosen't work which is quite understanding.
How do I do?
Simon -
I was reading Franks solution to adding nodes to specific locations. The question I have is where to put his code and how to specify the location I wish to add nodes?
Thanks
I have one table contain childno,Childname,ParentNo,ParentName.
Now A parent can have many child, and even a child can have many child, and the same in repetitive manner.
Now I want to populate this in the tree view,
like this
+ Parent 1
+ Child 1
+ Child 2
+ Child 3
+ Child 5
+ Child 8
+ Child 9
+ Child 6
+ Child 7
+ Parent 2
+ Child 4
Please provide me solution.
Excellent Article!
But can I write this nodes in my web.sitemap file? Is it possible?
How difficult would it be to allow you to have URL's without the default.aspx? Kind of like how this works: Stop editing 'web.sitemap'--Let unknown pages dynamically INHERIT from a parent! And more...
I would like to merge the feature set of both of these providers
I use UrlRewritingNet, so as a result have to make an additional change to the provider to take into account reqwritten URL's.
On Initialize add:
base.SiteMapResolve += new SiteMapResolveEventHandler(SiteMap_SiteMapResolve);
Then create a new method:
SiteMapNode SiteMap_SiteMapResolve(object sender, SiteMapResolveEventArgs e)
{
SiteMapNode n = SiteMap.CurrentNode;
if (n == null && e.Context.Items["UrlRewritingNet.UrlRewriter.CachedPathAfterRewrite"] != null)
{
string actualUrl = e.Context.Items["UrlRewritingNet.UrlRewriter.CachedPathAfterRewrite"].ToString();
if (actualUrl.IndexOf('?') != -1)
{
actualUrl = actualUrl.Substring(0, actualUrl.IndexOf('?'));
}
n = SiteMap.RootNode;
n = FindSiteMapNode(n, "~" + actualUrl);
}
if (n == null) n = SiteMap.RootNode;
return n;
}
Then the method FindSiteMapNode:
public SiteMapNode FindSiteMapNode(SiteMapNode n, string actualUrl)
{
if (n.HasChildNodes)
{
SiteMapNodeCollection all = n.GetAllNodes();
foreach (SiteMapNode cn in all)
{
if (cn.Url.ToLower() == actualUrl.ToLower()) return cn;
}
}
return n;
}
Post a Comment