When you think ASP, think...
Recent Articles
All Articles
ASP.NET Articles
ASPFAQs.com
Message Board
Related Web Technologies
User Tips!
Coding Tips
Search

Sections:
Book Reviews
Sample Chapters
Commonly Asked Message Board Questions
JavaScript Tutorials
MSDN Communities Hub
Official Docs
Security
Stump the SQL Guru!
Web Hosts
XML
Information:
Advertise
Feedback
Author an Article

ASP ASP.NET ASP FAQs Message Board Feedback
 
Print this Page!
Published: Wednesday, February 1, 2006

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.
  • (Subscribe to this Article Series! )

    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!

    - continued -

    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:
    1. Get the site navigation information (this may be in a database, based on the file system, etc.)
    2. Iterate through the site navigation information, creating a SiteMapNode for each logical section
    3. 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 _root SiteMapNode 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()

            'Create a root node
            _root = CreateFolderNode(HttpContext.Current.Server.MapPath(RootUrl), RootUrl)
            _root.Title = RootTitle

            '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.

    Happy Programming!

  • By Scott Mitchell


    Attachments


  • Download the code used in this article

    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.
  • (Subscribe to this Article Series! )



  • ASP.NET [1.x] [2.0] | ASPMessageboard.com | ASPFAQs.com | Advertise | Feedback | Author an Article