Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Create a Dynamic ASP.Net Web Site Navigation Menu using Linq that also generates the static Web.sitemap file for you automatically.

0.00/5 (No votes)
20 Jul 2016 1  
Avoid creating extra redundant work by implementing this code in all your websites.

Introduction

Create a Dynamic ASP.Net Web Site Navigation Menu using Linq that also generates the static Web.sitemap file for you automatically.

Problem: When you are programming an ASP.net website the tasks of adding a new content page include adding the navigation information to the web.sitmap xml file. This task is repeated several times over just one web site and can get complicated because the web.sitemap file does not use IntelliSense and many runtime errors can occur. Avoid creating extra redundant work by implementing this code in all your websites. I searched the internet and I found two parts to the puzzle. The references are listed below.

Background

To come up with this example I borrowed code from two sites:

  1. http://www.codeproject.com/Articles/12716/Navigation-using-dynamic-SiteMapPath-file
  2. http://www.dotnetcurry.com/showarticle.aspx?ID=281

Using the code

Web site requirements:

  1. Must have a Site.master template file in which all content pages inherit.
  2. The Site.master must have a Menu control.
  3. The Site.master must have a SiteMapDataSource.
  4. The Site.master must have a ScriptManager.
  5. The Site.master should have a method that creates the Web.sitemap xml file it doesn’t exist. This enables the system to create an updated Web.sitmap xml file while creating a dynamic SiteMapProvider to make sure your web site functions correctly without first having a Web.sitemap xml file. One is created for you and referenced on-the-fly.
  6. Requires a Web.config file with SiteMapProvider configuration element. This element is set to the DefaultProvider , but can be switched to use the DynamicSiteMapProvider that you will create so you can test it directly. After testing the DynamicSiteMapProvider you may remove the provider element.
  7. Your web site should have well organized folders that contain the content pages you want included in the top menu node. The top menu nodes will be the folder names.

What you add to your site:

  1. You will need to create the DynamicSiteMapProvider class. Some adjustment may be necessary for your particular site.
  2. You will need to add and call the Page_Init() method in the Site.master.cs file.
  3. You will need to add a SiteMapProvider element to the Web.config file.
  4. You will need to create a test content page that is inherited form the Site.master to add a button and button event so you can instantiate an object of the DynamicSiteMapProvider class and call the RebuildSiteMap(), and GenerateWebDotSiteMapXMLFile() methods. You will need to FindControl the Site.master Menu and the SiteMapDataSource controls so you can databind the menu. This will make the system use the updated SiteMapProvider nodes right away.

Down load zip for more complete implementation.

using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Xml;
using System.Security.Permissions;

/// <summary>
/// Summary description for DynamicSiteMapProvider
/// Creates a dynamic SiteMapProvider
/// Dynamically creates the Web.sitemap xml file
/// </summary>
public class DynamicSiteMapProvider : StaticSiteMapProvider
{
    #region Variables

    /// <summary>
    /// Add or remove Excluded folders as needed
    /// </summary>
    private const string ExcludedFolders = "(App_Data)|(obj)|(Scripts)|(App_Code)|(bin)|(fonts)|(Content)";

    /// <summary>
    /// Add or remove Excluded files as needed.
    /// Warning: Do not have but one default.aspx file in your root. All others should be excluded as to not have duplicates and throw a duplicate exception.
    /// </summary>
    private const string ExcludedFiles = "(Default.aspx)";

    /// <summary>
    /// Add or remove Excluded files by file extension.
    /// Use this to exclude all files that are not aspx content pages.
    /// </summary>
    private const string ExcludedFilesByExtension = "(.cs)|(.exclude)|(.config)|(.master)|(master.cs)|(.rdlc)|(.ico)|(.ascx)|(.asax)|(.webinfo)|(.sitemap)";

    private SiteMapNode parentNode = null;

    #endregion END Variables

    #region Accessors Mutators

    /// <summary>
    /// The parent node
    /// Use the parent node to access the child nodes.
    /// </summary>
    public SiteMapNode ParentNode
    {
        get { return parentNode; }
        set { parentNode = value; }
    }

    #endregion END Accessors Mutators

    #region overridded methods

    /// <summary>
    /// Returns the ParentNode
    /// Microsoft says: When you inherit from the SiteMapProvider class, you must override the following members: GetRootNodeCore, FindSiteMapNode, GetChildNodes, and GetParentNode. Apparently I didn't need to.
    /// </summary>
    /// <returns>SiteMapNode</returns>
    protected override SiteMapNode GetRootNodeCore()
    {
        return BuildSiteMap();
    }

    /// <summary>
    /// Builds the sitemap and saves it in cache
    /// </summary>
    /// <returns>SiteMapNode</returns>
    public override SiteMapNode BuildSiteMap()
    {
        lock (this)
        {
            parentNode = HttpContext.Current.Cache["SiteMap"] as SiteMapNode;
            if (parentNode == null)
            {
                base.Clear();
                parentNode = new SiteMapNode(this,
                                        HttpRuntime.AppDomainAppVirtualPath,
                                        "Home");
                // Make sure that you have a Default.aspx file in your root directory. This serves as your main Home page.
                SiteMapNode fileNode = new SiteMapNode(this, HttpRuntime.AppDomainAppVirtualPath, HttpRuntime.AppDomainAppVirtualPath + "Default.aspx", "Home", "Home");
                AddNode(fileNode, parentNode);
                AddFiles(parentNode);
                AddFolders(parentNode);

                HttpContext.Current.Cache.Insert("SiteMap", parentNode);
            }
            return parentNode;
        }
    }
    #endregion END Overridden Methods

    #region Methods

    /// <summary>
    /// Rebuilds the SiteMap
    /// Call this method when you want to update the sitemap.
    /// You need to update the sitemap when you add a content page.
    /// Clears the cache first
    /// </summary>
    public void RebuildSiteMap()
    {
        lock (this)
        {
            HttpContext.Current.Cache.Remove("SiteMap");
        }
        BuildSiteMap();
    }

    /// <summary>
    /// Finds all the web site folders, parses out the folders you don't want included and creates top menu nodes under the parent node and child nodes under the top menu nodes.
    /// </summary>
    /// <param name="parentNode">SiteMapNode</param>
    private void AddFolders(SiteMapNode parentNode)
    {
        var folders = from o in Directory.GetDirectories(HttpContext.Current.Server.MapPath(parentNode.Key))
                      let dir = new DirectoryInfo(o)
                      where !Regex.Match(dir.Name, ExcludedFolders).Success
                      select new
                      {
                          DirectoryName = dir.Name
                      };

        foreach (var item in folders)
        {
            string folderUrl = parentNode.Key + item.DirectoryName;
            SiteMapNode folderNode = new SiteMapNode(this,
                                folderUrl,
                                null,
                                item.DirectoryName,
                                item.DirectoryName);
            // call base class method to add nodes
            AddNode(folderNode, parentNode);
            AddFiles(folderNode);
        }
    }

    /// <summary>
    /// Takes in a top menu node, parses out the files you do not want included then adds the children (content pages) as SiteMapNodes.
    /// </summary>
    /// <param name="folderNode">SiteMapNode</param>
    private void AddFiles(SiteMapNode folderNode)
    {
        var files = from o in Directory.GetFiles(HttpContext.Current.Server.MapPath(folderNode.Key))
                    let fileName = new FileInfo(o)
                    where ((!Regex.Match(fileName.Name, ExcludedFiles).Success) && (!Regex.Match(fileName.Extension, ExcludedFilesByExtension).Success))
                    select new
                    {
                        FileName = fileName.Name
                    };

        foreach (var item in files)
        {
            SiteMapNode fileNode = new SiteMapNode(this,
                                item.FileName,
                                folderNode.Key + "/" + item.FileName,
                                item.FileName.Replace(".aspx", ""));
            AddNode(fileNode, folderNode);
        }
    }
    #endregion END methods

    #region Create Web.Sitemap file locally

    /// <summary>
    /// Guess what this method does? Here is an example of the use of the Microsoft's C# codeing naming standard.
    /// </summary>
    /// <param name="sFileName">string</param>
    public void GenerateWebDotSiteMapXMLFile(string sFileName = "Web.sitemap")
    {
        SiteMapNodeCollection myCollection;
        if (ParentNode == null)
        {
            ParentNode = GetRootNodeCore();
        }
        myCollection = ParentNode.ChildNodes;

        //Create the Web.sitemap XML file
        Encoding enc = Encoding.UTF8;
        XmlTextWriter myXmlTextWriter = new XmlTextWriter(HttpRuntime.AppDomainAppPath + sFileName, enc);

        myXmlTextWriter.WriteStartDocument();//xml document open
        // Create the Top level (Parent element)
        myXmlTextWriter.WriteStartElement("siteMap");
        myXmlTextWriter.WriteAttributeString("xmlns", "http://schemas.microsoft.com/AspNet/SiteMap-File-1.0");

        // Create the first Node of the Menu
        myXmlTextWriter.WriteStartElement("siteMapNode");
        //Title attribute set
        myXmlTextWriter.WriteAttributeString("title", "Home");
        myXmlTextWriter.WriteAttributeString("description",
                 "This is home");//Description attribute set
        myXmlTextWriter.WriteAttributeString("url",
                 "");//URL attribute set
        //Loop and create the main Menu nodes that are represented by folders that were included
        foreach (SiteMapNode node in ParentNode.ChildNodes)
        {
            myXmlTextWriter.WriteStartElement("siteMapNode");
            myXmlTextWriter.WriteAttributeString("title",
                            node.Title);
            myXmlTextWriter.WriteAttributeString("description",
                                  node.Description);
            myXmlTextWriter.WriteAttributeString("url",
                          node.Url);
            //Loop and create child nodes for the Main Menu nodes
            foreach (SiteMapNode childNode in node.ChildNodes)
            {
                myXmlTextWriter.WriteStartElement("siteMapNode");
                myXmlTextWriter.WriteAttributeString("title",
                                childNode.Title);
                myXmlTextWriter.WriteAttributeString("description",
                                      childNode.Description);
                myXmlTextWriter.WriteAttributeString("url",
                              childNode.Url);
                myXmlTextWriter.WriteEndElement();//Close the first siteMapNode
            }
            myXmlTextWriter.WriteEndElement();//Close the siteMapNode
        }
        myXmlTextWriter.WriteEndDocument();//xml document closed

        // clean up and release xml resources
        myXmlTextWriter.Flush();
        myXmlTextWriter.Close();
        return;
    }

    #endregion END Create Web.Sitemap file locally
}

Site.Master.cs

using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Security.Principal;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;

public partial class SiteMaster : MasterPage
{
    private const string AntiXsrfTokenKey = "__AntiXsrfToken";
    private const string AntiXsrfUserNameKey = "__AntiXsrfUserName";
    private string _antiXsrfTokenValue;

    protected void Page_Init(object sender, EventArgs e)
    {
        // rem this code if you change the System.Web.XmlSiteMapProvider defaultProvider in the Web.config file to use the DynamicSiteMapProvider
        if (!System.IO.File.Exists(Server.MapPath("~") + "Web.sitemap"))
        {
            DynamicSiteMapProvider myCustomSiteMap = new DynamicSiteMapProvider();
            myCustomSiteMap.RebuildSiteMap();
            myCustomSiteMap.GenerateWebDotSiteMapXMLFile();
        }

        // The code below helps to protect against XSRF attacks
        var requestCookie = Request.Cookies[AntiXsrfTokenKey];
        Guid requestCookieGuidValue;
        if (requestCookie != null && Guid.TryParse(requestCookie.Value, out requestCookieGuidValue))
        {
            // Use the Anti-XSRF token from the cookie
            _antiXsrfTokenValue = requestCookie.Value;
            Page.ViewStateUserKey = _antiXsrfTokenValue;
        }
        else
        {
            // Generate a new Anti-XSRF token and save to the cookie
            _antiXsrfTokenValue = Guid.NewGuid().ToString("N");
            Page.ViewStateUserKey = _antiXsrfTokenValue;

            var responseCookie = new HttpCookie(AntiXsrfTokenKey)
            {
                HttpOnly = true,
                Value = _antiXsrfTokenValue
            };
            if (FormsAuthentication.RequireSSL && Request.IsSecureConnection)
            {
                responseCookie.Secure = true;
            }
            Response.Cookies.Set(responseCookie);
        }

        Page.PreLoad += master_Page_PreLoad;
    }

    protected void master_Page_PreLoad(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            // Set Anti-XSRF token
            ViewState[AntiXsrfTokenKey] = Page.ViewStateUserKey;
            ViewState[AntiXsrfUserNameKey] = Context.User.Identity.Name ?? String.Empty;
        }
        else
        {
            // Validate the Anti-XSRF token
            if ((string)ViewState[AntiXsrfTokenKey] != _antiXsrfTokenValue
                || (string)ViewState[AntiXsrfUserNameKey] != (Context.User.Identity.Name ?? String.Empty))
            {
                throw new InvalidOperationException("Validation of Anti-XSRF token failed.");
            }
        }
    }

    protected void Page_Load(object sender, EventArgs e)
    {

    }

    protected void Unnamed_LoggingOut(object sender, LoginCancelEventArgs e)
    {
        Context.GetOwinContext().Authentication.SignOut();
    }
}
<!--Web.config-->
 <system.web>
   <siteMap defaultProvider="DefaultProvider"> <!--change the defaultProvider attribute to main to switch-->
     <providers>
      <clear />
       <add name="DefaultProvider" type="System.Web.XmlSiteMapProvider" siteMapFile="Web.sitemap" />
      <add name="DynamicSiteMapProvider" type="DynamicSiteMapProvider" siteMapFile="Web.sitemap" />
     </providers>
   </siteMap>

Points of Interest

I combined the two ideas and some code from the references into one very useful complete package.

How to test and run:

  1. You may delete the Web.sitemap xml file. The system will generate an updated Web.sitemap file for you and use it based on the protected void Page_Init(object sender, eventArgs e) method in the Site.master that you will add. This may be the best way to add content pages. So anytime you add a content page to a folder just delete the Web.sitemap xml file and run the site.
  2. You may add a button to any content page (admin page?) that calls the DynamicSiteMapProvider class methods directly and it will update the existing Web.sitemap xml file.
  3. Remove or delete the Web.sitemap file and set the defaultProvider =” DynamicSiteMapProvider” in the Web.config file to not need the Web.sitemap file and use the DynamicSiteMapProvider class exclusively.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here