devxlogo

Nine ASP.NET Site Navigation Problem Solutions: Part 1

Nine ASP.NET Site Navigation Problem Solutions: Part 1

o far, there’s been plenty of ink expended on ASP.NET 2.0’s site navigation controls, but most of that coverage lacks details on how to use the new navigation features in the real world. For example, some common real-world questions are: How do you integrate database driven content into your site’s navigation? How do you architect your solution to automatically hide pages to which a user lacks permission? What about differentiating the active top level page, or displaying only the children of that active page? Perhaps you need to display navigational elements as images, but aren’t sure how to store the filename or image size.

This two-part series explores nine of the most common site map problems and shows how to solve them with ASP.NET 2.0’s new navigation features. Part 1 demonstrates how to:

  • Set up ASP.NET 2.0 site navigation
  • Display images in navigation
  • Differentiate active pages
  • Navigate site map data
  • Display breadcrumbs (a list of pages between the home page and the current page)
  • Use custom site map attributes
  • Construct a site map page

Part two will delve into more advanced site map solutions, such as:

  • Implementing page security and integrating it with site map data
  • Integrating database driven content by extending the provider model

Initial Setup: Mind-blowing Simplicity
The ASP.NET 2.0 solution to site navigation is extremely easy to set up. Initial configuration takes less than a minute. If you are starting with a blank solution add a few pages such as Default.aspx and SiteMap.aspx. Then, in Visual Studio 2005 add a new item of type “Site Map” named Web.sitemap.

Inside your new Web.sitemap XML file, add siteMapNode elements for each of your pages. Specify the locations and titles for your pages using the url and title attributes. Here’s a sample snippet of a Web.sitemap file:

                            

Next you must tell ASP.NET to use the Web.sitemap file. Before the closing element in your Web.config file, add:

                              

To see how this works add the following to Default.aspx or your master page:

   

<%= SiteMap.CurrentNode.Title %>!

When you run the application, the title of the Default.aspx page displays as “Home!”

#1: Making Buckets Pretty
The most common site navigation problem developers face is displaying a site’s buckets (top level pages). Displaying buckets as images is slightly more interesting than displaying them as plain text, because you must store the image’s filename, and perhaps its size. This first solution shows how to display top-tier images as a set of “tabs”?by binding a Repeater control to a SiteMapDataSource control and using the IsDescendantOf() method of ASP.NET 2.0’s new System.Web.SiteMap class.

Figure 1 shows the final result for a Site Map page where each bucket is represented by an image.

?
Figure 1: Image-oriented Navigation: The “tabs” in the page shown in this figure are images that correspond to the buckets, or top-level pages in the application, as shown by the arrows connecting them to the hierarchical set of links.

The MasterPage.master file in this solution contains two controls: a SiteMapDataSource control and a Repeater control:

              <%#        String.Format("{1},"          ((SiteMapNode)Container.DataItem).Url,          ((SiteMapNode)Container.DataItem).Title,          ((SiteMapNode)Container.DataItem)["imageName"])     %>   

The SiteMapDataSource control provides easy programmatic access to the hierarchical site map information contained in Web.sitemap. Of all SiteMapDataSource’s properties, ShowStartingNode is the most useful. Setting it to false removes the mandatory one-and-only-one top-level siteMapNode in Web.sitemap, and exposes its children (the buckets). StartingNodeOffset is the next most useful property because it lets you move up and down the site map navigational tree. I’ll explain this topic in greater detail in Solution #4. The most common properties of SiteMapDataSource are described in Table 1.

Table 1: Commonly SiteMapDataSource Properties: The table shows the most commonly-used SiteMapDataSource properties and a description of each.

PropertyDescription
ShowStartingNodeTrue by default. Setting this property to false removes the mandatory one-and-only-one top-level siteMapNode in Web.sitemap, and exposes its children (the buckets).
StartFromCurrentNodeFalse by default. Setting this property to true exposes the current page rather than the topmost page. If you were to use this property in conjunction with a StartingNodeOffset of 1 you would expose the children of the current page.
StartingNodeOffsetThis has a value of zero by default. Setting it to a positive number exposes the children of the node that would otherwise be exposed (negative numbers move up the navigation tree). For example, if you were to use a StartingNodeOffset of 1 in conjunction with a ShowStartingNode with a value of false, you would expose the children of the currently active bucket. See Solution #4 for details.
StartingNodeUrlThis property allows you to set a specific page as the starting node that SiteMapDataSource exposes. If you were to use this property in conjunction with a StartingNodeOffset of 1, you could display the children of a particular node. This technique might be particularly useful when used in conjunction with database-driven site map content as described in the second article in this series.

A TreeView is one of the few controls that can display all a SiteMapDataSource’s pages, because it can handle hierarchical information natively. The repeater used above, however, typically does not. What it will do is display the topmost pages exposed by the SiteMapDataSource. This is why SiteMapDataSource’s ShowStartingNode property is so useful.

By binding to the SiteMapDataSource through the DataSourceID property, the repeater can access the siteMapNode elements contained in the SiteMapDataSource. Each SiteMapNode object corresponds to one of the entries in the Web.sitemap file (see earlier example). The repeater may then access the siteMapNode element’s attributes, such as url and title.

But what about storing and retrieving other attributes, such as imageName? What makes ASP.NET 2.0’s approach to site navigation so wonderful is its extensibility. You can add arbitrary attributes to the siteMapNode elements in the Web.sitemap file, and then access them in code. The solution above adds the imageName attribute to the Web.sitemap file as shown below:

   

Then, you can access the value in the RepeaterItemTemplate:

   ((SiteMapNode)Container.DataItem)["imageName"])

You could easily add image height and width attributes as well. That covers the basics of displaying image-oriented navigation. The next sections expand on this approach by showing how to identify active buckets.

#2: Active Buckets: The Plot Thickens
Most sites display active buckets differently from inactive buckets in order to help users identify where they are in the site. But what does active mean? Active pages (the term is not restricted to buckets)contain a direct lineage to the current page. For example, notice that the Home bucket in Figure 1 is white instead of blue. This is because the Home page is a bucket and the parent of Site Map. If the Site Map page resided within some other bucket, then the Home would be blue, and the other bucket would be white.

Determining ancestry required a custom approach in ASP.NET 1.1. ASP.NET 2.0 simplifies the task with the SiteMapNode.IsDescendantOf() method.

Consider the following page snippet:

        <%#        IsBucketActive((SiteMapNode)Container.DataItem) ?       // if the current page belongs to the current bucket       "[display active image]" :       // else if the current page does not belong        // to the current bucket          "[display regular image]"     %>   

The repeater’s ItemTemplate calls the IsBucketActive() method in the code-behind and passes in the current SiteMapNode. The IsBucketActive() function then returns true if CurrentNode.IsDescendentOf(nodeBucket):

   protected bool IsBucketActive(SiteMapNode nodeBucket) {     // pages that don't contain an item in Web.sitemap return null     if (SiteMap.CurrentNode == null)       return false;     else       return SiteMap.CurrentNode.Equals(nodeBucket) ||            SiteMap.CurrentNode.IsDescendantOf(nodeBucket);   }

IsDescendantOf() combined with the SiteMap.CurrentNode property packs a powerful punch. And since the default XmlSiteMapProvider holds everything in memory, it’s fast too. The next solution demonstrates a vertically-oriented navigation, which requires nesting Repeaters.

#3: Putting Pails inside Buckets
Vertical navigations often display buckets (top-level pages), then pails (second-level pages) under the active bucket, and then sometimes tertiary pages under active pails (see Figure 2). But Repeater’s don’t display hierarchical data, right? Actually, they can if you nest Repeaters and use the data binding syntax (see my article Nested DataGrids Simplify Hierarchical Data Display for more details).

?
Figure 2: Vertically-oriented Navigation: The navigation bar on the left side of the page in the solution that accompanies this article demonstrates a vertically-oriented navigation.

The code for this solution might look like:

               <%# IsBucketActive((SiteMapNode)Container.DataItem) ?           String.Format("{1}
," ((SiteMapNode)Container.DataItem).Url, ((SiteMapNode)Container.DataItem).Title) : String.Format("{1}
," ((SiteMapNode)Container.DataItem).Url, ((SiteMapNode)Container.DataItem).Title) %> DataSource='<%# ((SiteMapNode)Container.DataItem).ChildNodes %>' Visible='<%# IsBucketActive( (SiteMapNode)Container.DataItem)%>'> <%# String.Format("  {1}
," ((SiteMapNode)Container.DataItem).Url, ((SiteMapNode)Container.DataItem).Title) %>

With this approach the nested repeater retrieves the current SiteMapNode of the outer repeater and binds to its children via the ChildNodes() function. The nested repeater’s Visible property combined with the IsBucketActive() function allows only active buckets to display their pails. The next solution shows another common variation for displaying pails.

#4: What’s Under Your Bucket?
The previous solution showed how to display pails in between the HTML code for buckets. Displaying children of the active bucket after all buckets have been rendered is also a common requirement of site navigation, but requires a different approach. The second-level navigation in Figure 1 is a perfect example.

The approach is simple. Add a second SiteMapDataSource that exposes only the children of the active bucket:

   

By setting StartingNodeOffset to 1, the example forces the starting point down one level from the bucket level, thus exposing the children of the currently active bucket. Setting the StartingNodeOffset to 2 works for third-tier pages under the active second-tier page, and so on.

#5: Lost in a Sea of Breadcrumbs
Displaying breadcrumbs in ASP.NET 2.0 is so ridiculously simple with the SiteMapPath control that this solution goes right to a more interesting problem?hiding certain breadcrumbs.

Hiding pages from the list of breadcrumbs is very useful in some circumstances, but its implementation is non-trivial. For example, suppose you want a set of pages to act as if they are children of the home page, yet look like third-level pages rather than buckets. Your sitemap might look like this:

  • Home (root version)
    • Home (bucket version)
      • Privacy Policy
      • Site Map
    • Articles
      • Article #1
      • Article #1
    • Solutions

The “Privacy Policy” page and the “Article #1” page must use different approaches to display their breadcrumbs. For example, Article #1 should display its breadcrumbs as Home > Articles > Article #1. It uses the root version of Home and displays its parent bucket. In contrast, the Privacy policy page should display its breadcrumbs as Home > Privacy Policy. But if it displays the root version of Home AND its bucket it will display home twice. So the problem is how to hide one of those pages from the breadcrumbs display.

You can solve the problem in three steps. First, by using the SiteMapPath templates in the page:

               Home                 <%# (IsSiteMapNodeVisible(Container.SiteMapNode) ?            Eval("Url," " // ") +            Eval("Title," "{0}") :            "") %>                 <%# Eval("Title," " // {0}")%>        

Second, by adding a visible=”false” siteMapNode attribute to Web.sitemap as follows:

           ...

And third, by adding an IsSiteMapNodeVisible() function that uses the visible property in the code-behind:

   protected bool IsSiteMapNodeVisible(SiteMapNode node) {     // pages that don't contain an item in Web.sitemap return null     if (node == null)       return true;     // most pages won't contain the visible attribute     else if (node["visible"] == null)       return true;     else       // return true unless visible is set to "false"       return !node["Visible"].Equals("false");   }

You should note two things about the preceding solution. First, Web.sitemap uses two different URL names to refer to Default.aspx. This is important because without it the .NET Framework will return the error:

   Multiple nodes with the same URL 'Default.aspx' were found.    XmlSiteMapProvider requires that sitemap nodes have unique URLs.

The .NET Framework requires URL’s to be unique because behind the scenes it stores SiteMapNodes in a Dictionary object in memory. You could also get around this requirement by appending parameters to your URLs (e.g. Default.aspx?1, and Default.aspx?2).

Second, note that you cannot use PathSeparator if you ever plan to hide breadcrumbs. If you do set a path separator, either through the PathSeparator attribute or the PathSeparatorTemplate properties, the system displays the separator for your non-visible breadcrumbs?resulting in double separators.

The solution above approaches the problem by manually writing the “//” path separator in the NodeTemplate and CurrentNodeTemplate. You should note that doing this invalidates any NodeStyle-Font-Bold type attributes, because those will now apply to the separator as well as to the breadcrumb content. Consequently the NodeTemplate rolls its own formatting.

Even with the added twist of hiding certain breadcrumbs the ASP.NET 2.0 breadcrumb solution is remarkably simple compared to a custom solution. And what’s truly impressive is how easy it is to extend the approach using SiteMapPath templates and custom attributes.

#6 Centering Yin & Yang by Centralizing Meta-Data
ASP.NET 2.0’s ability to store arbitrary page information centrally can solve a great number of problems. For instance, consider the simplicity of maintaining meta-keywords and meta-descriptions in the Web.sitemap rather than distributed across every page. The new approach makes it simple to insert such metadata in the rendered HTML.

        <font style="background-color: yellow"><%</font># mstrTitle <font style="background-color: yellow">%></font>     <%# mstrMetaDescription %>" />     <%# mstrMetaKeywords %>" />

Then simply declare and set a couple of protected properties in the code-behind:

   protected string mstrMetaKeywords = "";   protected string mstrMetaDescription = "";   protected string mstrTitle = "";      protected void Page_Load(object sender, EventArgs e) {     if (SiteMap.CurrentNode != null) {       if (SiteMap.CurrentNode["metaKeywords"] != null)         mstrMetaKeywords = SiteMap.CurrentNode["metaKeywords"];       mstrMetaDescription = SiteMap.CurrentNode.Description;       mstrTitle = SiteMap.CurrentNode.Title;     }     ...     if (!Page.IsPostBack)       Page.DataBind();   }

Don’t forget to check whether the CurrentNode property or any custom-defined SiteMapNode attributes are null. Lastly add metaKeywords and description attributes to your Web.sitemap file:

?
Figure 3: Displaying TreeView Navigation: The Maintenance page in the solution accompanying this article contains a TreeView that links to a SiteMapDataSource and acts as a site map.
   

This approach vastly simplifies maintaining page-level metadata used by internal or external search engines. By combining the technique with a Master Page, you may just write your Web administrator out of a job!

#7: Sitemap Simplicity
Adding a functional site map to your site is now as easy as adding a SiteMapDataSource and a TreeView to your Web form, and then setting the Treeview.DataSourceID property to the SiteMapDataSource. To hide the root node just set the SiteMapDataSource.ShowStartingNode Property to false. You can make the TreeView a little prettier by setting the ShowExpandCollapse property to false and ShowLines to true. See Figure 3.

Don’t Miss the Next Installment!
This article demonstrated seven problems and their solutions using basic ASP.NET 2.0 site navigation techniques in the context of scenarios that developers are most likely to encounter. The second installment in this article series provides two more problems and solutions, and introduces more advanced techniques, including: hiding unauthorized pages, extending the site map provider model to include dynamic content, and caching dynamic content using ASP.NET 2.0’s new SqlCacheDependency class.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist