harriyott.com

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, I've put it in this gist.

23 March 2007