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
The diagram below shows the relationship between the provider and the controls.
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: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. 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. 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. Figure 4. SiteMapPath control (click on the graphic for a larger image)
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 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
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>
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.
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 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.
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 . . . // . . . }
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); } } }
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>
No comments:
Post a Comment