Wednesday, October 13, 2010

Understanding and Extending the Site Navigation System in ASP.NET 2.0

Introduction

Most web sites employ some form of visual navigation to help users move around the site easily, and find the information and Web pages they require. Though the look and feel can vary widely from site to site, the same basic elements are usually present, in the form of navigation bars, or menu lists, that direct users to specific parts of the web site.
ASP.NET 1.x out of the box provided little in the way of support for site navigation, leading many developers and web designers to either build their own navigation system, or buy third-party controls to meet their requirements. All this changes with ASP.NET 2.0, which introduces a navigation system that uses a pluggable framework for exposing the site hierarchy, and controls that plug into this new model, making it easy to build a high quality menu and navigation system.
This paper describes how the navigation system in ASP.NET 2.0 works, and shows how it can be extended beyond simple XML files, which is the default mechanism used in Visual Studio 2005.

Understanding the Navigation System in ASP.NET 2.0

In addition to creating a compelling navigation model that would appeal to both developers and web site designers, one of the other goals for the ASP.NET 2.0 navigation system was to create an architecture that provided an extensibility capability that was flexible enough to address a wide range of needs. It is based on a provider model that is used throughout the ASP.NET 2.0 framework, which provides a standard mechanism for plugging in different data sources.
The ASP.NET 2.0 navigation framework can be broken down into a couple of areas:
  • The web navigation controls (Menu, TreeView, and SiteMapPath) that developers use on the actual web pages. These can be customized to change their look and feel.
  • The SiteMapDataSource control, which the TreeView and Menu navigation controls bind to, providing an abstract layer between the Web navigation controls and the underlying provider of the navigation information.
  • The Site Map Provider, which is the pluggable provider that exposes the actual information that describes the layout of the web site. ASP.NET ships with one provider, the XmlSiteMapProvider, which uses an XML file with a specific schema as its data store
This layered architecture creates a looser coupling between the underlying site hierarchy and the controls on the web site, provides greater flexibility, and makes architectural and design changes easier as sites evolve.
The diagram below shows the relationship between the provider and the controls.
Aa479338.filesystemnav_fig01(en-us,MSDN.10).gif
Figure 1. Navigation architecture
In the case of the navigation system, the data source describes the hierarchy of the web site pages that users can navigate to, and how this information should be displayed to the users. It is referred to as a site map. The layout for a simple web site might be:
Home
    Products
        Product A
        Product B 
Product C
    Latest Offers
    Contact Us
        Email
        Visit us
    

The Navigation Controls

Before delving down into the inner working of the navigation system, it is important to understand how developers interact with it. The most common way is through the three new navigation controls in ASP.NET 2.0. Multiple Web navigation controls can exist on a single site or page—it's not uncommon to have the main menu control on the left-hand side of a page and another menu across, say, the top of the page, and it's possible to have one navigation control programmatically control another on the page, or to have them operate independently.
The navigation system is commonly used in conjunction with the Master Pages capabilities that are also new in ASP.NET 2.0. By placing the navigation controls on the site's master page, it ensures a consistent look and feel across the entire site. However, the navigation and master page capabilities are orthogonal and independent of each other.
The three new web controls are:
  • Menu Control—This provides a traditional navigational interface, typically down the side, or across the top, of a web site. It can render an arbitrary number of nested submenus, and optional "pops-out" submenus when a user hovers over an item.
    Aa479338.filesystemnav_fig02(en-us,MSDN.10).gif
    Figure 2. Menu control
  • TreeView Control—This provides a vertical tree-like user interface that can be expanded and collapsed by selecting the individual nodes. It also provides check box functionality that allows items to be selected.
    Aa479338.filesystemnav_fig03(en-us,MSDN.10).gif
    Figure 3. TreeView control
  • SiteMapPath Control—This is often referred to as a "breadcrumb" control, because it keeps track of where a user is within the site hierarchy. It displays the current location as a trail, typically from the home page to the current location, making it easier for the user to see where he or she is, and to navigate back to other pages on the trail.
        Click here for larger image.
    Figure 4. SiteMapPath control (click on the graphic for a larger image)
From an architectural perspective, the Menu and TreeView controls are somewhat similar, the main differences being in the way they are programmed and render themselves, each having its own distinctive look and feel. For a more detailed look at these two controls, see the MSDN article Introducing the ASP.NET 2.0 TreeView and Menu Controls. It is worth noting that, although the most common scenario for the Menu and TreeView controls centers around site navigation, they can also be used in non-navigation scenarios, letting users make selections.
The SiteMapPath (or "breadcrumb") control is a little different. It works directly with the SiteMapProvider, not through the SiteMapDataSource control that the Menu and TreeView controls use. This reflects its more focused role within the navigation system, so there is less rationale to try to expand its functionality to non-navigation scenarios.

Working with the Navigation System

One of the easiest ways to gain an understanding of how site navigation works is to access it directly from within an application, rather than through a Web control. The following sample shows how to interact with the SiteMap object model to display part of the hierarchical site information.
<%@ Page Language="C#" %>
<script runat="server">
    private void Page_Load(object sender, System.EventArgs e)
    {
        this.Label1.Text = "Current Page Title : " + 
          SiteMap.CurrentNode.Title;
        if(SiteMap.CurrentNode.ChildNodes > 0) {
            this.HyperLink1.NavigateUrl = 
              SiteMap.CurrentNode.ChildNodes[0].Url;
            this.HyperLink1.Text = 
              SiteMap.CurrentNode.ChildNodes[0].Title;
        }
    }
</script>

<html>
<head>
</head>
<body>
    <form id="Form1" runat="server">
        <asp:Label ID="Label1" runat="server" 
          Text="Label"></asp:Label><br />
          First Child Node: <asp:HyperLink id="HyperLink1" 
          runat="server">HyperLink</asp:HyperLink>
</form>
</body>
</html>
The navigation system models the site map as a series of nodes, known as SiteMapNodes, within a tree-like structure, each of which typically represents a page on the web site that a user can navigate to (it is possible to have nodes that are simply place holders for subpages and that, as such, have no Web page themselves).
The SiteMapNode class has several properties and methods, but the most important ones are:
  • Title—The text that will be rendered when this node is displayed by a web control
  • Url—The URL of the actual page that the SiteMapNode represents within the web site
  • Description—The ToolTip to be displayed when the mouse hovers over the node on the HTML page
The SiteMapNode class also has a number of properties that hold the links between the individual SiteMapNodes that describe the site map structure (ChildNodes, NextSibling, PreviousSibling, ParentNode, and so on).

Implementing Your Own Site Map Provider

As mentioned, ASP.NET 2.0 ships with a navigation data store provider called the XmlSiteMapProvider, and this is the default provider that most developers are familiar with. This provider consumes data from an XML file that represent the SiteMapNodes. When using Visual Studio 2005, if you add a new site map to a Web project, by default, it will create a Web.sitemap file within the project and populate it with the following template.
<siteMap 
  xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
    <siteMapNode url="" title=""  description="">
        <siteMapNode url="" title=""  description="" />
        <siteMapNode url="" title=""  description="" />
    </siteMapNode>
</siteMap>
You can immediately see that the core SiteMapNodes properties covered earlier (title, url, and description) are attributes in the Web.sitemap XML schema, and that the tree structure (parent, child, and so on) is expressed by nesting SiteMapNodes.
This is a simple and elegant way of handling the site map information, and, for many Web designers, it more than suffices. However, the extensibility of the ASP.NET provider model does mean you can write your own provider. There are perhaps three primary reasons for considering creating a custom site map provider:
  • The site map information is in a data source that is not an XML file, such as a database or directory service.
  • The site map information is available as XML, but uses a schema that is different to the schema used for Web.sitemap.
  • A dynamic site map structure is required, which needs to be constructed at runtime, or which requires a customized view that cannot be handled by security trimmings.
To implement your own site map provider, you need to derive a custom provider class from the SiteMapProvider abstract class in the System.Web namespace. Although the SiteMapProvider class has some twenty or so abstract or virtual methods, only a small number need to be overridden and implemented in your custom site map provider.
If your custom site map provider uses a data store that is similar to the default XmlSiteMapProvider's Web.sitemap XML schema, you could choose to derive from the StaticSiteMapProvider class, which provides a partial implementation of the SiteMapProvider class, and which is, in turn, the base class for the actual XmlSiteMapProvider class.
As a minimum, the following methods must be implemented by your class:
  • FindSiteMapNode—Returns a SiteMapNode that corresponds to the a specific URL.
  • GetChildNodes—Returns the collection of child nodes for a specific SiteMapNode.
  • GetParentNode—Returns the parent node for a specific SiteMapNode.
  • GetRootNodeCore—Returns the root node of all the nodes currently managed by the current provider.
You should also override the SiteMapProvider Initialize method and perform your own initialization there, after calling the base class Initialize.
You can optionally override some of the properties that relate to the nodes, if required, to enable you to build more sophisticated models to represent the site map:
  • SiteMapNode—Return the node that maps to the current HTTPContext URL.
  • RootNode—Return the node at the root of the store.
  • RootProvider—Return the provider for the root, to allow chaining of providers.
These properties and methods represent the contract between the SiteMapProvider and the Menu / TreeView controls (through the SiteMapDataSource) and the SiteMapPath control. This interaction occurs as the controls make requests for information that is needed to populate the display and, as the user navigates through the site, to track the current position within the site navigation. It's an event-driven model, with the navigation system calling into the provider throughout the interactions.
If you write your own provider, you need to decide whether the SiteMapNodes are read-only or whether they are writeable—if they are writeable, you need to take into account thread safety and ensure that the implementation is thread safe, locking update code where appropriate, because ASP.NET dispatches requests across a thread pool, so a singleton will be accessed by multiple threads.
A more subtle issue is around performance and scalability. It's not unusual for a web site to use a couple of navigation controls, or for the underlying navigation data store to be quite large; therefore, you need to ensure your provider is responsive, which will impact design issues around the best algorithms to store and retrieve the data, and the potential role of caching.
There is also the issue of security to take into account. A common requirement for web sites is to allow only some members or other authenticated users to see certain pages, and ASP.NET 2.0 role management provides a well defined way to restrict access to Web files based on security roles. This extends through to the site navigation system by means of a mechanism known as security trimming. Security trimming enforces the Url and File Authorization rules that also apply when attempting to access a page. Defining a roles attribute on a node serves to widen the access to the node—if you are in one of the roles defined in the roles attribute, then the node is returned; if you are not in one of these roles, then the Url and File Authoriation checks are performed. In the case of the XmlSiteMapProvider, adding a roles="managers" style attribute to a SiteMapNode entry will determine whether the user sees that node in the site map. When writing your own site map provider, this is surfaced throught the IsAccessableToUser method, which returns a Boolean to indicate whether that node is available to the user, based on his or her role. There is a default implementation in the base SiteMapProvider class that derived providers can use. If security is to be supported, the site map must provide some way to store this information.

A Folder Site Map Provider

The sample in this article exposes the subdirectories within a web site as the actual site map, enabling the web site navigation to map directly onto the folder structure, a not uncommon model for a web site structure. The focus for this sample is to show how to design and build a site map provider, and, as such, it doesn't aim to provide industrial-strength coverage of all the possible "edge conditions." These trade-offs are highlighted within the article, where appropriate.
The sample provider recursively enumerates through the subdirectories of the web site and, for each folder (apart from the App_* and bin directories, which are special directories used by ASP.NET 2.0), checks for the existence of a default.aspx file. If this file is present, the sample provider adds it to the site map, using the name of the containing folder as the menu description. It would be relatively easy to extend this sample to support more than the one default.aspx entry for each directory, or to store a ToolTip date in a text file within the directory, if required.
The core code for the provider is as follows.
[AspNetHostingPermission(SecurityAction.Demand, Level = 
  AspNetHostingPermissionLevel.Minimal)]
public class FolderSiteMapProvider : SiteMapProvider
{
    private SiteMapNode rootNode = null;
        // root of tree
    private Hashtable urlHash = null;
            // hash table based on URL
    private Hashtable keyHash = null;
            // hash table based on key
        
    string defaultPageName = "Default.aspx";
    // only look for this file in each subdirectory
    string defaultTitle = "Home";
                // description of root node

    // override SiteMapProvider Initialize
    public override void Initialize(string name, 
      NameValueCollection attributes)
    {
        // do base class initialization first
        base.Initialize(name, attributes);

        // our custom initialization
        urlHash = new Hashtable();
        keyHash = new Hashtable();

        // get web site info
        string startFolder = HttpRuntime.AppDomainAppPath;
        string startUri = 
          Uri.EscapeUriString(HttpRuntime.AppDomainAppVirtualPath);
          // want canonical format of URI

        // Create root node
        string key = startFolder;
        string url = startUri + @"/" + defaultPageName;

        rootNode = new SiteMapNode(this, key, url, defaultTitle);
        RootNode.ParentNode = null;

        urlHash.Add(url, rootNode);
        keyHash.Add(key, rootNode);

        // populate entire site
        EnumerateFolders(rootNode);
    }

    // Retrieves a SiteMapNode that represents a page
    public override SiteMapNode FindSiteMapNode(string rawUrl)
    {
        if (urlHash.ContainsKey(rawUrl))
        {
            SiteMapNode n = (SiteMapNode)urlHash[rawUrl];
            return n;
        }
        else
        {
            return null;
        }
    }


    // Retrieves the root node of all the nodes 
    //currently managed by the current provider. 
    // This method must return a non-null node
    protected override SiteMapNode GetRootNodeCore()
    {
        return rootNode;
    }


    // Retrieves the child nodes of a specific SiteMapNode
    public override SiteMapNodeCollection 
      GetChildNodes(SiteMapNode node)
    {
        SiteMapNode n = (SiteMapNode)keyHash[node.Key];
          // look up our entry, based on key
        return n.ChildNodes;
    }


    // Retrieves the parent node of a specific SiteMapNode.
    public override SiteMapNode GetParentNode(SiteMapNode node)
    {
        SiteMapNode n = (SiteMapNode)keyHash[node.Key];
         // look up our entry, based on key
        return n.ParentNode;
    }

    // helper functions . . .
    //  . . .
}
The sample provider builds up a list of folders when the provider is called through its Initialize method, and doesn't refresh that list, so it won't detect new folders added on the fly, which is not unreasonable for a production web site.
It doesn't implement security trimming, so all subdirectories are visible, but it would be simple to implement this by adding a call to the base class IsNodeAccessible method inside of the FindSiteMapNode, GetChildNodes, and GetParentNode methods, which would automatically get the benefit of any file authorization rules configured for the web site.
Internally, a provider can represent the underlying store in any number of ways; however, because it interacts with the navigation system through SiteMapNode classes, in this case it makes sense to uses these internally, especially because a directory hierarchy so closely matches the node hierarchy. It also maintains two hash tables, one of which is keyed on the node URL, and the other of which is keyed on the node key (which is the folder name), to allow rapid lookup of nodes. For this sample, holding all this information in memory makes sense, because it keeps the programming model simple and the lookup very fast; however, for a very large site, with thousands of folders, it may be worth investigating a mechanism that doesn't hold them all in memory.
In order to keep the code simple, the nodes are not handled as read-only. In a true industrial-strength provider, when the application is not allowed to add or amend the internal list of site map nodes, mark the individual nodes and the collection as read-only.
Private helper functions are used to build up the folder hierarchy, as shown in the following code sample.
private void EnumerateFolders(SiteMapNode parentNode)
{
    // create a node collection for this directory
    SiteMapNodeCollection nodes = new SiteMapNodeCollection();
    parentNode.ChildNodes = nodes;

    // get list of subdirectories within this directory
    string targetDirectory = parentNode.Key;
      // we use the key to hold the file directory
    string[] subdirectoryEntries = 
      Directory.GetDirectories(targetDirectory);
            
    foreach (string subdirectory in subdirectoryEntries)
    {
        // search for any sub folders in this directory
        string[] s = subdirectory.Split('\\');
        string folder = s[s.Length-1];

        string tmp = String.Copy(folder);
        tmp = tmp.ToLower();
           // avoid any case sensitive matching issues

        // check for App_ and bin directories, and don't add them
        if (tmp.StartsWith("app_"))
            continue;
        if (tmp == "bin")
            continue;

        string testFileName = subdirectory + @"\" + defaultPageName;
        if (File.Exists(testFileName))
        {
            // create new node
            string key = subdirectory;
            string url = CreateChildUrl(parentNode.Url, folder);
            string title = folder;

            SiteMapNode n = new SiteMapNode(this, key, url, title);
            n.ParentNode = parentNode;

            // add it into node collection and table
            nodes.Add(n);
            urlHash.Add(url, n);
            keyHash.Add(key, n);

            // and enummerate through this folder now
            EnumerateFolders(n);
        }
    }
}
As the provider enumerates over the subdirectories, it creates a SiteMapNode class for each valid subdirectory, setting its key, url, and title properties. It also populates the ChildNodes property, which is a SiteMapNodeCollection, with each of the child nodes, and the ParentNode property points back up to the parent node. It also adds each of the nodes to the hash tables, for rapid lookup.
Once complete, the provider needs to be configured using the web.config file. The following entry needs to be added under <system.web>.
<system.web>
    . . . 
    <siteMap defaultProvider="SimpleProvider">
        <providers>
            <add name="SimpleProvider" 
                    type="Test.SimpleProvider"/>
        </providers>
    </siteMap>
    . . .
</system.web>

Conclusion

The goal of this article was to give you an insight into, and appreciation of, the ASP.NET 2.0 site navigation system and how the different elements work together. The code sample shows how it is possible to extend the architecture by building your own site map provider that uses an arbitrary source to define the site hierarchy.

No comments:

Post a Comment