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:
- http://www.codeproject.com/Articles/12716/Navigation-using-dynamic-SiteMapPath-file
- http://www.dotnetcurry.com/showarticle.aspx?ID=281
Using the code
Web site requirements:
- Must have a Site.master template file in which all content pages inherit.
- The Site.master must have a Menu control.
- The Site.master must have a SiteMapDataSource.
- The Site.master must have a ScriptManager.
- 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.
- 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.
- 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:
- You will need to create the DynamicSiteMapProvider class. Some adjustment may be necessary for your particular site.
- You will need to add and call the Page_Init() method in the Site.master.cs file.
- You will need to add a SiteMapProvider element to the Web.config file.
- 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;
public class DynamicSiteMapProvider : StaticSiteMapProvider
{
#region Variables
private const string ExcludedFolders = "(App_Data)|(obj)|(Scripts)|(App_Code)|(bin)|(fonts)|(Content)";
private const string ExcludedFiles = "(Default.aspx)";
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
public SiteMapNode ParentNode
{
get { return parentNode; }
set { parentNode = value; }
}
#endregion END Accessors Mutators
#region overridded methods
protected override SiteMapNode GetRootNodeCore()
{
return BuildSiteMap();
}
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");
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
public void RebuildSiteMap()
{
lock (this)
{
HttpContext.Current.Cache.Remove("SiteMap");
}
BuildSiteMap();
}
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);
AddNode(folderNode, parentNode);
AddFiles(folderNode);
}
}
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
public void GenerateWebDotSiteMapXMLFile(string sFileName = "Web.sitemap")
{
SiteMapNodeCollection myCollection;
if (ParentNode == null)
{
ParentNode = GetRootNodeCore();
}
myCollection = ParentNode.ChildNodes;
Encoding enc = Encoding.UTF8;
XmlTextWriter myXmlTextWriter = new XmlTextWriter(HttpRuntime.AppDomainAppPath + sFileName, enc);
myXmlTextWriter.WriteStartDocument(); myXmlTextWriter.WriteStartElement("siteMap");
myXmlTextWriter.WriteAttributeString("xmlns", "http://schemas.microsoft.com/AspNet/SiteMap-File-1.0");
myXmlTextWriter.WriteStartElement("siteMapNode");
myXmlTextWriter.WriteAttributeString("title", "Home");
myXmlTextWriter.WriteAttributeString("description",
"This is home"); myXmlTextWriter.WriteAttributeString("url",
""); foreach (SiteMapNode node in ParentNode.ChildNodes)
{
myXmlTextWriter.WriteStartElement("siteMapNode");
myXmlTextWriter.WriteAttributeString("title",
node.Title);
myXmlTextWriter.WriteAttributeString("description",
node.Description);
myXmlTextWriter.WriteAttributeString("url",
node.Url);
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(); }
myXmlTextWriter.WriteEndElement(); }
myXmlTextWriter.WriteEndDocument();
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)
{
if (!System.IO.File.Exists(Server.MapPath("~") + "Web.sitemap"))
{
DynamicSiteMapProvider myCustomSiteMap = new DynamicSiteMapProvider();
myCustomSiteMap.RebuildSiteMap();
myCustomSiteMap.GenerateWebDotSiteMapXMLFile();
}
var requestCookie = Request.Cookies[AntiXsrfTokenKey];
Guid requestCookieGuidValue;
if (requestCookie != null && Guid.TryParse(requestCookie.Value, out requestCookieGuidValue))
{
_antiXsrfTokenValue = requestCookie.Value;
Page.ViewStateUserKey = _antiXsrfTokenValue;
}
else
{
_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)
{
ViewState[AntiXsrfTokenKey] = Page.ViewStateUserKey;
ViewState[AntiXsrfUserNameKey] = Context.User.Identity.Name ?? String.Empty;
}
else
{
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();
}
}
-->
<system.web>
<siteMap defaultProvider="DefaultProvider"> -->
<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:
- 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.
- 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.
- 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.