DotNetCurry Logo

Creating Dynamic ASP.NET SiteMap using LINQ

Posted by: Malcolm Sheridan , on 3/4/2009, in Category ASP.NET
Views: 102190
Abstract: The following article demonstrates how to use LINQ to dynamically generate a SiteMap to show a selection of files and folders for a website.
Creating Dynamic ASP.NET SiteMap using LINQ
 
One of the main files I could not program without, is the web.SiteMap file. The web.SiteMap file is a convenient way to centrally store navigation information and other properties for any ASP.NET web site. The web.SiteMap works perfectly with controls such as the Menu or Treeview control. One little known class you can use to create your own SiteMap Provider is the StaticSiteMapProvider class. This is a base class for the XmlSiteMapProvider class, so by inheriting from this class, you can implement your own SiteMap Provider model. This article will generate a custom SiteMap Provider to create a menu structure outlining the files and folders in the website.
Open Visual Studio 2008 and choose File > New > Web > ASP.NET Web Application.
 
Add a new class to the website and name it MyCustomSiteMap. Add the following code to the class:
 
C#
public class MyCustomSiteMap : StaticSiteMapProvider
    {
        private SiteMapNode parentNode
        {
            get;
            set;
        }
 
        private string ExcludedFolders
        {
            get
            {
                return "(App_Data)|(obj)";
            }
        }
 
        private string ExcludedFiles
        {
            get
            {
                return "";
            }
        }
 
        public override SiteMapNode BuildSiteMap()
        {
            lock (this)
            {
                parentNode = HttpContext.Current.Cache["SiteMap"] as SiteMapNode;
                if (parentNode == null)
                {                   
                    base.Clear();                   
                    parentNode = new SiteMapNode(this,
                                            HttpRuntime.AppDomainAppVirtualPath,
                                            HttpRuntime.AppDomainAppVirtualPath + "Default.aspx",
                                            "Home");
                   
                    AddNode(parentNode);
                    AddFiles(parentNode);
                    AddFolders(parentNode);
                   
                    HttpContext.Current.Cache.Insert("SiteMap", parentNode);
                }
                return parentNode;
            }
        }
 
        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)                       
                        select new
                        {
                            FileName = fileName.Name                            
                        };
           
            foreach (var item in files)
            {
                SiteMapNode fileNode = new SiteMapNode(this,
                                    item.FileName,
                                    folderNode.Key + "/" + item.FileName,
                                    item.FileName);
                AddNode(fileNode, folderNode);
            }
        }
 
        protected override SiteMapNode GetRootNodeCore()
        {
            return BuildSiteMap();
        }
    }
 
VB.NET
Public Class MyCustomSiteMap
      Inherits StaticSiteMapProvider
            Private privateparentNode As SiteMapNode
            Private Property parentNode() As SiteMapNode
                  Get
                        Return privateparentNode
                  End Get
                  Set(ByVal value As SiteMapNode)
                        privateparentNode = value
                  End Set
            End Property
 
            Private ReadOnly Property ExcludedFolders() As String
                  Get
                        Return "(App_Data)|(obj)"
                  End Get
            End Property
 
            Private ReadOnly Property ExcludedFiles() As String
                  Get
                        Return ""
                  End Get
            End Property
 
            Public Overrides Function BuildSiteMap() As SiteMapNode
                  SyncLock Me
                        parentNode = TryCast(HttpContext.Current.Cache("SiteMap"), SiteMapNode)
                        If parentNode Is Nothing Then
                              MyBase.Clear()
                              parentNode = New SiteMapNode(Me, HttpRuntime.AppDomainAppVirtualPath, HttpRuntime.AppDomainAppVirtualPath & "Default.aspx", "Home")
 
                              AddNode(parentNode)
                              AddFiles(parentNode)
                              AddFolders(parentNode)
 
                              HttpContext.Current.Cache.Insert("SiteMap", parentNode)
                        End If
                        Return parentNode
                  End SyncLock
            End Function
 
            Private Sub AddFolders(ByVal parentNode As SiteMapNode)
                  Dim folders = _
                        From o In Directory.GetDirectories(HttpContext.Current.Server.MapPath(parentNode.Key)) _
                        Let dir = New DirectoryInfo(o) _
                        Where (Not Regex.Match(dir.Name, ExcludedFolders).Success) _
                        Select New With {Key .DirectoryName = dir.Name}
 
                  For Each item In folders
                        Dim folderUrl As String = parentNode.Key + item.DirectoryName
                        Dim folderNode As New SiteMapNode(Me, folderUrl, Nothing, item.DirectoryName, item.DirectoryName)
 
                        AddNode(folderNode, parentNode)
                        AddFiles(folderNode)
                  Next item
            End Sub
Private Sub AddFolders(ByVal parentNode As SiteMapNode)
                  Dim folders = _
                        From o In Directory.GetDirectories(HttpContext.Current.Server.MapPath(parentNode.Key)) _
                        Let dir = New DirectoryInfo(o) _
                        Where (Not Regex.Match(dir.Name, ExcludedFolders).Success) _
                        Select New With {Key .DirectoryName = dir.Name}
 
                  For Each item In folders
                        Dim folderUrl As String = parentNode.Key + item.DirectoryName
                        Dim folderNode As New SiteMapNode(Me, folderUrl, Nothing, item.DirectoryName, item.DirectoryName)
 
                        AddNode(folderNode, parentNode)
                        AddFiles(folderNode)
                  Next item
End Sub
 
            Private Sub AddFiles(ByVal folderNode As SiteMapNode)
                  Dim files = _
                        From o In Directory.GetFiles(HttpContext.Current.Server.MapPath(folderNode.Key)) _
                        Let fileName = New FileInfo(o) _
                        Select New With {Key .FileName = fileName.Name}
 
                  For Each item In files
                        Dim fileNode As New SiteMapNode(Me, item.FileName, folderNode.Key & "/" & item.FileName, item.FileName)
                        AddNode(fileNode, folderNode)
                  Next item
            End Sub
 
            Protected Overrides Function GetRootNodeCore() As SiteMapNode
                  Return BuildSiteMap()
            End Function
 
End Class
 
The class inherits the StaticSiteMapProvider class. The two methods we must implement are GetRootNodeCore and BuildSiteMap. BuildSiteMap is where we most of the work lives, so let’s look at that code a now.
To ensure only one thread create an instance of this class, we use the lock keyword. Once we have a lock, we proceed to check the Cache to see if the SiteMap has already been created. If the Cache is empty, we use LINQ to enumerate through the folders and files of the website. To exclude folders you may not want to appear in the list, a property named ExcludedFolders returns a string which is used in a regular expression in the AddFolders method. You could create a separate property to exclude particular files, but for this example we’ll stick to just folders.
The AddFiles method utilises LINQ to enumerate through the files in each folder that is not in the ExcludedFolders string. After a file is found, a new SiteMapNode is created and added to the parent SiteMapNode object, which will be the folder the file is in.
To use this in your website, you need to add the following code to the web.config file:
<siteMap enabled="true" defaultProvider="MyCustomSiteMap">
                  <providers>
                        <clear/>
                        <add name="MyCustomSiteMap" type="CustomSiteMap.MyCustomSiteMap"/>
                  </providers>
            </siteMap>
 
The code above informs ASP.NET to use our CustomSiteMap.MyCustomSiteMap type as the default SiteMap provider. 
 
The hard work is done. To view the results add a new web form to the solution.
 
DS LINQ 
 
In the new web form, drag and drop a Menu and SiteMapDataSource control to the page.
 
<asp:Menu ID="Menu1" runat="server" DataSourceID="SiteMapDataSource1">
</asp:Menu>
<asp:SiteMapDataSource ID="SiteMapDataSource1" runat="server" />
 
By using the SiteMapDataSource control with the Menu, ASP.NET will now use the custom SiteMap class that we created earlier.
Add a new folder to the project and name it Data. Next add some text files to the folder. This is purely to display some more data for this example. Everything is complete. If you run the project, the menu will display the folders and files in the website.
DS LINQ
The example above will hopefully get you thinking of ways to automatically make things happen when you’re developing a website. Instead of having to manually add a web.SiteMap file and update it each time a new page or folder is added, why not create a process to add it automatically? This will allow you to spend more time doing the things you want to, namely writing code! The entire source code of this article can be downloaded from here
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+
Further Reading - Articles You May Like!
Author
Malcolm Sheridan is a Microsoft awarded MVP in ASP.NET, a Telerik Insider and a regular presenter at conferences and user groups throughout Australia and New Zealand. Being an ASP.NET guy, his focus is on web technologies and has been for the past 10 years. He loves working with ASP.NET MVC these days and also loves getting his hands dirty with jQuery and JavaScript. He also writes technical articles on ASP.NET for SitePoint and other various websites. Follow him on twitter @malcolmsheridan


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!
Comment posted by Joel WZ on Wednesday, March 4, 2009 2:40 PM
I am getting an error:
Parser Error Message: Could not load type 'CustomSiteMap.MyCustomSiteMap'.
Any ideas?
Comment posted by Malcolm Sheridan on Wednesday, March 4, 2009 9:10 PM
@Joel WZ
In the example above the namespace has been omitted.  The CustomSiteMap type is referring to the namespace, so you'll need to add that namespace to your code.  The code you can download with this example has that already, so download it and take a look at it.
Comment posted by anup on Wednesday, March 4, 2009 11:29 PM
nice post..
Comment posted by Tinyiko on Monday, May 25, 2009 7:17 AM
please send me a code of how to Create an ASP.net page in VB using a button and a code
Comment posted by Rashed on Tuesday, August 4, 2009 1:23 AM
Multiple nodes with the same key 'Default.aspx' were found. XmlSiteMapProvider requires that sitemap nodes have unique keys
Comment posted by mikew909 on Wednesday, September 30, 2009 9:27 AM
great work! - im trying to do something similar here, do you happen to know if a asp:menu connected to a sitemap can have its navigation overridden - replacing it with dynamic content?
Comment posted by Malcolm Sheridan on Saturday, October 31, 2009 4:59 AM
@mikew909
Off the top of my head I don't know.  If you find out email me with the results.
Comment posted by erdinç on Monday, July 12, 2010 11:10 AM
hi,
thx for this lovely article.but when i try to implement this code to my project i get some errors.and error is about folders.you excluded appdata folder but i need to exclude some other folders to like app_code and bin can you give a tip how can do this?
Comment posted by narinder kehar on Sunday, October 3, 2010 11:36 AM
it is nice article. but i need sitemap path through database not tree view
Comment posted by Martín Tobón on Wednesday, November 17, 2010 8:20 AM
Your post was really helpful... for my application I needed to build the sitemap according to the role of the current user online... I had a problem with the caching part of your code, so I use another overload of the Insert method to add a listener to the cache expiration and remove that item... it's something like this:

//In the BuildSitemap Method when inserting object to Cache
HttpContext.Current.Cache.Insert("SiteMap", parentNode, null, Cache.NoAbsoluteExpiration, new TimeSpan(0, 0, 5), CacheItemPriority.Normal, new CacheItemRemovedCallback((OnSitemapExpirated)));

//The calback to Cache Expiration
public void OnSitemapExpirated(string key, object value, CacheItemRemovedReason reason)
{
    var siteMap = HttpContext.Current.Cache["SiteMap"] as SiteMapNode;

    if (siteMap != null)
    {
            HttpContext.Current.Cache.Remove("SiteMap");
    }
}
Comment posted by anonymous on Saturday, May 14, 2011 1:03 AM
hi
i m gettin dis error while tryin to load the class file
Could not load type 'CustomSiteMap.MyCustomSiteMap'

please help
Comment posted by Soheil on Monday, November 28, 2011 6:12 AM
God bless you with reading comments about "SiteMap" from people how know nothing about .Net lol
Comment posted by prasiddha on Thursday, January 5, 2012 5:14 AM
If you want to have a real dynamic menu in aspx then probably i have presented the idea
Comment posted by prasiddha on Thursday, January 5, 2012 5:15 AM
Under the topic Dynamic Menu In ASP.NET Using MSSQL, and XML in debughere.blogspot.com
Comment posted by Malcolm Sheridan on Tuesday, March 6, 2012 5:01 AM
@prasiddha

Interesting you say that.  Where's your idea?
Comment posted by Safa on Tuesday, January 22, 2013 12:58 AM
this code gives failed to map path in line 79 of MyCustomSiteMap.cs
Comment posted by awgtek on Saturday, March 16, 2013 8:44 PM
I found it helpful to replace the portion of AddFiles to read: SiteMapNode(this,

                                item.FileName, url: (folderNode.Key == "/" ? "" : folderNode.Key + "/") + item.FileName,
                                title: item.FileName);

This is so that root forms are linked to.
Comment posted by arman abi on Wednesday, July 10, 2013 6:00 AM
this error :
Multiple nodes with the same URL '/Default.aspx' were found. XmlSiteMapProvider requires that sitemap nodes have unique URLs.
Comment posted by arman abi on Wednesday, July 10, 2013 6:17 AM
this error :
Multiple nodes with the same URL '/Default.aspx' were found. XmlSiteMapProvider requires that sitemap nodes have unique URLs.
Comment posted by Mạnh Viết Đặng on Tuesday, September 10, 2013 10:50 PM
Could not load type 'CustomSiteMapMyCustomSiteMap'. Please feedback manhdv3000@gmail.com. Thank you.
Comment posted by Mạnh Viết Đặng on Tuesday, September 10, 2013 10:58 PM
Could not load type 'CustomSiteMapMyCustomSiteMap'. Please feedback manhdv3000@gmail.com. Thank you.
Comment posted by Cesar Daniel on Thursday, November 21, 2013 12:25 PM
Thanks, this works very well. Just one question about the "SyncLock Me" statement.

I found this on MSDN:

"Programming Practices
...
You should not use the Me keyword to provide a lock object for instance data. If code external to your class has a reference to an instance of your class, it could use that reference as a lock object for a SyncLock block completely different from yours, protecting different data. In this way, your class and the other class could block each other from executing their unrelated SyncLock blocks. Similarly locking on a string can be problematic since any other code in the process using the same string will share the same lock."

Is it safe to use "SynckLock Me"?
Comment posted by Tanuja on Wednesday, January 14, 2015 1:41 AM
how can i add multiple Parent SitemapNode??
there is an error while return multiple sitemapnode instead of single SiteMapNode(i.e. Single Parent node)
Comment posted by Tanuja on Wednesday, January 14, 2015 1:47 AM
how can i add multiple Parent SitemapNode??
there is an error while return multiple sitemapnode instead of single SiteMapNode(i.e. Single Parent node)
Comment posted by Sam on Friday, May 29, 2015 2:41 PM
I am getting a Parser Error Message: Could not load type 'CustomSiteMap.MyCustomSiteMap'.

Line 13:  <add name="MyCustomSiteMap" type="CustomSiteMap.MyCustomSiteMap"/>