Examining ASP.NET 2.0's Site Navigation - Part 4
By Scott Mitchell
A Multipart Series on ASP.NET 2.0's Site Navigation
This article is one in a series of articles on ASP.NET 2.0's site navigation functionality.
Part 1 - shows how to create a simple site map
using the default XML-based site map provider and how to display a
TreeView and SiteMapPath (breadcrumb) based on the site map data.
Part 2 - explores programmatically accessing
site map data through the SiteMap class;
includes a thorough discussion of the SiteMapPath (breadcrumb) control.
Part 3 - examines how to use base the site map's
contents on the currently logged in user and the authorization rules
defined for the pages in the site map.
Part 4 - delves into creating a custom site
map provider, specifically one that bases the site map on the website's physical, file system structure.
Part 5 - see how to customize the markup
displayed by the navigation controls, and how to create your own custom navigation UI.
Introduction
The goal of ASP.NET's site navigation feature is to allow a developer to specify a site map that describes his website's
logical structure. A site map is constructed of an arbitrary number of hierarchically-related site map nodes, which
typical contain a name and URL. The site navigation API, which is available in the .NET Framework via the SiteMap
class, has properties for accessing the root node in the site map as well as the "current" node (where the "current" node is
the node whose URL matches the URL the visitor is currently on). As discussed in
Part 2 of this article series, the data from the site
map can be accessed programmatically or through the navigation Web controls (the SiteMapPath, TreeView, and Menu controls).
The site navigation features are implemented using the provider
model, which provides a standard API (the SiteMap class) but allows developers to plug in their own
implementation of the API at runtime. ASP.NET 2.0 ships with a single default implementation,
XmlSiteMapProvider,
with which the developer can define the site map through an XML file (Web.sitemap); Part 1
of this article series looked at defining this XML file. However, our site's structure might already be specified by existing
database data, or perhaps by the folders and files that makeup our website. Rather than having to mirror the database or
file system structure in a Web.sitemap file, we can create a custom provider that exposes the database
or file system information as a site map.
Thanks to the provider model we can provide a custom implementation of the site navigation subsystem, but one that still
is accessible through the SiteMap class. In essence, with a custom provider the SiteMap class
and navigation Web controls will work exactly as they did with the XmlSiteMapProvider. The only difference will
be that the site map information will be culled from our own custom logic, be it from a database, a Web service, the file system,
or from whatever data store our application may require. In this article we'll look at how to create a custom site
navigation provider and build a file system-based custom provider from the ground-up. Read on to learn more!
The Job of a Custom Site Map Provider
In English, the responsibility of a site map provider is to return the site map upon request, where a site map is
a collection of hierarchically-related site map nodes. More concretely, each site map node is implemented in the .NET Framework
as an instance of the SiteMapNode class. A custom site map provider, then, will need to perform the following:
Get the site navigation information (this may be in a database, based on the file system, etc.)
Iterate through the site navigation information, creating a SiteMapNode for each logical section
Adding nodes to the site map, forming a hierarchy of site map nodes, keeping in mind that there can be only
one root node.
In addition to actually creating the site map, the custom site map provider might also need some initialization code.
When the site map is accessed for the first time after a custom provider has been registered in the Web.config
file, the custom site map provider's Initialize() method is called and the attribute names and value specified
for the provider in Web.config are passed along. The custom provider's Initialize() method can
read these attribute names and values and save them as needed. For example, a site map provider that accessed
site structure information from a database would need a connection string specified in the provider markup in the
Web.config file. In the Initialize() method this connection string value could be saved to
a member variable in the class, so later, when constructing the site map, it could be used to connect to the database.
Extending the StaticSiteMapProvider Class
The .NET Framework offers a StaticSiteMapProvider class that contains the core functionality needed for a custom
site map provider. To build our own site map provider, then, we merely need to create a class that inherits the StaticSiteMapProvider class
and provides an implementation for the following two methods:
GetRootNodeCore() - returns the root of the site map.
BuildSiteMap() - constructs the site map and returns the root node.
In addition to these two methods, there are a number of other methods that can be overridden, if needed. For example, we'll need to
override the Initialize() method for any custom providers that need to read custom settings from Web.config.
Building a File System-Based Custom Site Map Provider
For simple websites that are only composed of a handful of static pages, web designers typically structure the file system
to mimic the navigational structure of the website. For example, the demo website examined in the previous three parts of
this article series used the following navigational structure:
The files and folder structure in the website mimicked this logical structure. There was a Books folder with
pages named Novels.aspx, Romance.aspx, History.aspx and so on. Similarly, there were
folders named Electronics, DVDs, and Computers. Rather than having to mirror this
file system information in the Web.sitemap file (and remember to update it when adding new pages, renaming directories
or files, or removing pages altogether), we could create a custom site map provider that based the site map's content
on the file system structure.
I've created such a custom site map provider and named it FileSystemSiteMapProvider. This class, along with an
ASP.NET 2.0 website that uses the custom provider, is available for download at the end of this article.
The Practicality of a File System-Based Site Map Provider
Does a file system-based site map provider make sense for real-world web applications? It depends. For small websites
that have a tight affinity between the site's file system layout and its navigational structure, such a site map provider
makes sense. For data-driven sites that have pages whose content is dynamically generated based on parameters like querystring
values, or for sites where there's no connection between the file system layout and navigational structure, the file system
site map provider is ill-fitted.
For data-driven sites you'll likely want to use a custom site map provider that builds the site map based on the contents
from a database. While we won't look at building such a provider in this article, refer to Jeff Prosise's article
The SQL Site Map Provider You've Been Waiting For
for information on building a site map provider that hits against a SQL Server database. I also have an implementation of
a SQL site map provider available in this example.
The FileSystemSiteMapProvider Provider's Properties
By default the FileSystemSiteMapProvider provider enumerates every ASP.NET page and folder in the web application,
excluding only the Bin folder and folders that being with the App_ prefix. However, there may be some
ASP.NET pages or folders that you do not want to appear in the site map. The FileSystemSiteMapProvider
provider offers two optional properties that can be specified to achieve this aim:
ExcludeFileList - a list of files that should not be included in the site map. Each file must
be fully qualified. For example, in the diagram shown above, imagine that we wanted to exclude the Novels.aspx
page from the site map. We'd need to add ~/Books/Novels.aspx to ExcludeFileList.
ExcludeFoldeerList - a list of folders that should not be included in the site map. If a file
is excluded, all of its files and subfolders are excluded as well. Likewise, the folder path must be fully specified,
like ~/DVDs/.
Both the ExcludeFileList and ExcludeFoldeerList properties are StringDictionary objects;
furthermore, they are both Protected, meaning that you can't work with this property directly from your ASP.NET page.
Instead, you'll need to use the appropriate helper methods for adding, removing, and enumerating these properties. (See
the SiteMap_Info.aspx page in the download for an illustration of using these helper methods.) However, you can
set these properties to their initial values through the Web.config file, as we'll see shortly.
In addition to these two exclusion properties, FileSystemSiteMapProvider contains four other properties:
RootUrl - the fully qualified path to the page that should serve as the root node in the
site map. If this value isn't specified, the default value is ~/Default.aspx.
RootTitle - the title to display for the root SiteMapNode. Defaults to "Home"
UseDefaultPageAsFolderUrl - a Boolean property that indicates whether or not to use the DefaultPage
as the folder URL. If this property is True (the default) then each SiteMapNode created in the site map for
a folder has its Url property set to ~/FOLDER/DefaultPage.aspx (if the file exists).
Furthermore, the file ~/FOLDER/DefaultPage.aspx (if it exists) is not added as a child of the
folder.
To understand this property's implications, refer to the diagram shown earlier of the site's logical structure. Imagine
that the site has a ~/Books/Default.aspx page. If you want the Books node in the site map to be clickable and
take the user to ~/Books/Default.aspx, leave this property set to True. If, however, you don't want the Books
node to be clickable, and instead want to add a fourth child of the Books node that sends the user to ~/Books/Default.aspx,
then set this property to False.
DefaultPageName - the file name for the page that is the default document. Defaults to Default.aspx.
Building the Site Map
A site map provider that extends StaticSiteMapProvider (like ours does) must implement the BuildSiteMap()
method, whose purpose is to construct the site map (if needed) and return the root SiteMapNode. Since this method
can be called multiple times per page request (since a page might have multiple navigation Web controls), it behooves us to
utilize caching as much as possible. That is, we don't want to have to hit the file system and rebuild the site map every single
time this method is invoked. Rather, we want to build it once and cache this tree until there's some file system-level change.
Caching the tree is simple enough - we just create a _rootSiteMapNode variable at the class level and
assign this variable to the root of the SiteMap. This approach builds the site map just once, and then caches it until
the web application is restarted or the FileSystemSiteMapProvider class is edited. This caching is too aggressive,
however, because if new ASP.NET pages are added to the file system, or existing ones deleted, the site map will not pick up
these changes.
To alleviate this problem I utilize the CacheDependency object, which can be used to monitor a set of files and/or
folders. Specifically, I point it to the root folder (~/) and whenever BuildSiteMap() is called I
check to see if the file system has been changed since BuildSiteMap() was last called. If not, then I simply
return the _root reference, since there's no need to rebuild the site map. If, however, there has been a change,
I recreate the dependency, clear out the site map, and rebuild it.
The BuildSiteMap() method and the recursive BuildSiteMapFromFileSystem() shown below are the workhorses
for creating the site map. I've left off a couple of the helper methods (CreateFileNode() and CreateFolderNode(),
for example), for brevity. These methods can be explored by downloading the complete code at the end of this article.
Public Overrides Function BuildSiteMap() As System.Web.SiteMapNode
'Need to lock to ensure thread safety, since multiple pages in the app
'might be trying to call this method concurrently
SyncLock Me
'See if a root has been defined
If _root IsNot Nothing Then
'We have a root - but has the underlying file system been changed?
If Not _fsMonitor.HasChanged Then
'No change to FS, returned the cached root
Return _root
End If
'The file system has been changed since we've last called
'BuildSiteMap - we need to rebuild the sitemap
End If
'If we reach here, either we don't have a root or the file system has
'been changed must build up the Site Map. Clear out the site map, if
'it already exists...
Refresh()
'Establish the cache dependency
_fsMonitor = New CacheDependency(HttpContext.Current.Server.MapPath("~/"))
AddNode(_root) 'Add the root to the site map
'Recurse through the file system, adding nodes
BuildSiteMapFromFileSystem(_root, "~/")
Return _root
End SyncLock
End Function
Protected Sub BuildSiteMapFromFileSystem(ByVal parentNode As SiteMapNode, ByVal folderPath As String)
'Determine the folder path for currentNode
Dim folder As String = HttpContext.Current.Server.MapPath(folderPath)
'Get directory information for the folder
Dim dirInfo As New DirectoryInfo(folder)
'Add files to the tree with currentNode as the parent
For Each fi As FileInfo In dirInfo.GetFiles("*.aspx")
Dim fileNode As SiteMapNode = CreateFileNode(fi.FullName, parentNode, folderPath)
If fileNode IsNot Nothing Then AddNode(fileNode, parentNode)
Next
'Add nodes for each subfolder
For Each di As DirectoryInfo In dirInfo.GetDirectories()
Dim folderNode As SiteMapNode = CreateFolderNode(di.FullName, String.Concat(folderPath, di.Name, "/") & DefaultPageName)
'Add the node
If folderNode IsNot Nothing Then
AddNode(folderNode, parentNode)
BuildSiteMapFromFileSystem(folderNode, String.Concat(folderPath, di.Name, "/"))
End If
Next
End Sub
Extending the FileSystemSiteMapProvider Provider
The FileSystemSiteMapProvider implementation provided here uses a very simply algorithm for determining the title
to display for the SiteMapNodes for the files and folders - it just uses the file or folder name, replacing
underscores (_) with spaces. However, you may want to base the file name on the value in the <title>
element (if it exists), or based on some custom <meta> tag or some other criteria. You can easily add this
functionality by extending the FileSystemSiteMapProvider class and overridding the GetFileTitle()
and GetFolderTitle() methods. These two methods return the title used by the SiteMapNode for a specified
file or folder path.
Plugging the FileSystemSiteMapProvider Provider Into Your Website
With the FileSystemSiteMapProvider custom provider complete, the last step is to plug it into your website and
to start using it in place of the default XmlSiteMapProvider. To accomplish this, add the following markup to
your application's Web.config file:
<configuration>
<system.web>
<!-- SiteMap Provider Configuration -->
<siteMap enabled="true" defaultProvider="FileSystemSiteMapProvider">
<providers>
<add name="FileSystemSiteMapProvider"
type="FileSystemSiteMapProvider"
excludeFiles="comma-delimited list of files"
excludeFolders="comma-delimited list of folders"
rootUrl="rootUrl"
rootTitle="rootTitle"
useDefaultPageAsFolderUrl="true|false"
defaultPageName="defaultPageName.aspx"
/>
</providers>
</siteMap>
...
</system.web>
</configuration>
All of the attributes in the <add> element are optional (except for name and type).
Refer to the "The FileSystemSiteMapProvider Provider's Properties" section earlier in this article for a rundown
on this custom provider's properties and their default values.
Conclusion
One of the key benefits of ASP.NET 2.0 is its extensibility. With its myriad of subsystems built atop the provider model,
ASP.NET 2.0 provides a standardized API that permits custom implementation. In this article we saw how to utilize the provide
model and site navigation, creating a custom site map provider that is based on the file system structure. Such a site map
provider will likely prove useful for those developing small, relatively static websites whose file system structure closely
matches the site's navigational structure.
A Multipart Series on ASP.NET 2.0's Site Navigation
This article is one in a series of articles on ASP.NET 2.0's site navigation functionality.
Part 1 - shows how to create a simple site map
using the default XML-based site map provider and how to display a
TreeView and SiteMapPath (breadcrumb) based on the site map data.
Part 2 - explores programmatically accessing
site map data through the SiteMap class;
includes a thorough discussion of the SiteMapPath (breadcrumb) control.
Part 3 - examines how to use base the site map's
contents on the currently logged in user and the authorization rules
defined for the pages in the site map.
Part 4 - delves into creating a custom site
map provider, specifically one that bases the site map on the website's physical, file system structure.
Part 5 - see how to customize the markup
displayed by the navigation controls, and how to create your own custom navigation UI.