This chapter jumps into the details of Site Maps. First, you learn how to use the SiteMapDataSource
control to represent a Site Map on a page. For example, you learn how to use the SiteMapDataSource
control to display a list of all the pages contained in a folder.
Next, you’ll explore the SiteMap
and SiteMapNode
classes. You learn how to create new Site Map nodes dynamically. You also learn how to programmatically retrieve Site Map nodes and display the properties of a node in a page.
This chapter also examines several advanced features of Site Maps. For example, you learn how to show different Site Maps to different users depending on their roles. You also learn how you can extend Site Maps with custom attributes.
You also learn how to create custom Site Map providers. The first custom Site Map provider—the AutoSiteMapProvider—automatically builds a Site Map based on the folder and page structure of your website. The second custom Site Map provider—the SqlSiteMapProvider—enables you to store a Site Map in a Microsoft SQL Server database table.
Finally, you learn how to generate Google SiteMaps from ASP.NET Site Maps automatically. You can use a Google SiteMap to improve the way that your website is indexed by the Google search engine.
The SiteMapDataSource
control enables you to represent a Site Map declaratively in a page. You can bind navigation controls such as the TreeView
and Menu
controls to a SiteMapDataSource
control. You also can bind other controls such as the GridView
or DropDownList
control to a SiteMapDataSource
control.
Imagine, for example, that your website contains the Web.sitemap
file in Listing 18.1. Because the default SiteMapProvider
is the XmlSiteMapProvider
, the SiteMapDataSource
control automatically represents the contents of this XML file.
The code samples in this section are located in the SiteMaps
application on the CD that accompanies this book.
Example 18.1. Web.sitemap
<?xml version="1.0" encoding="utf-8" ?> <siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" > <siteMapNode url="Default.aspx" title="Home" description="The Home Page"> <siteMapNode url="Products/Default.aspx" title="Our Products" description="Products that we offer"> <siteMapNode url="Products/FirstProduct.aspx" title="First Product" description="The description of the First Product" /> <siteMapNode url="Products/SecondProduct.aspx" title="Second Product" description="The description of the Second Product" /> </siteMapNode> <siteMapNode url="Services/Default.aspx" title="Our Services" description="Services that we offer"> <siteMapNode url="Services/FirstService.aspx" title="First Service" description="The description of the First Service" metaDescription="The first service" /> <siteMapNode url="Services/SecondService.aspx" title="Second Service" description="The description of the Second Service" /> </siteMapNode> </siteMapNode> </siteMap>
The Site Map file in Listing 18.1 represents a website with the following folder and page structure:
Default.aspx Products FirstProduct.aspx SecondProduct.aspx Services FirstService.aspx SecondService.aspx
The page in Listing 18.2 illustrates how you can represent a Site Map by binding a TreeView
control to the SiteMapDataSource
control.
Example 18.2. Default.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Home</title> </head> <body> <form id="form1" runat="server"> <div> <asp:SiteMapPath id="SiteMapPath1" Runat="server" /> <hr /> <asp:TreeView id="TreeView1" DataSourceID="srcSiteMap" Runat="server" /> <asp:SiteMapDataSource id="srcSiteMap" Runat="server" /> </div> </form> </body> </html>
When you open the page in Listing 18.2, all the elements from the Web.sitemap
file are displayed in the TreeView
control with the help of the SiteMapDataSource
control (see Figure 18.1).
The SiteMapDataSource
control includes several valuable properties that you can set to modify the nodes that the control returns:
ShowStartingNode
—. Enables you to hide the starting node.
StartFromCurrentNode
—. Enables you to return all nodes starting from the current node.
StartingNodeOffset
—. Enables you to specify a positive or negative offset from the current node.
StartingNodeUrl
—. Enables you to return all nodes, starting at a node associated with a specified URL.
The most useful of these properties is the ShowStartingNode
property. Normally, when you display a list of nodes with a Menu or TreeView control, you do not want to display the starting node (the link to the home page). The page in Listing 18.3 illustrates how you can bind a Menu control to a SiteMapDataSource
that has the value False
assigned to its ShowStartingNode
property.
Example 18.3. Services/Default.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/ xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> .menuItem { border:solid 1px black; background-color:#eeeeee; padding:4px; margin:1px 0px; } </style> <title>Our Services</title> </head> <body> <form id="form1" runat="server"> <div> <asp:SiteMapPath id="SiteMapPath1" Runat="server" /> <hr /> <asp:Menu id="Menu1" DataSourceID="srcSiteMap" StaticMenuItemStyle-CssClass="menuItem" DynamicMenuItemStyle-CssClass="menuItem" Runat="server" /> <asp:SiteMapDataSource id="srcSiteMap" ShowStartingNode="false" Runat="server" /> </div> </form> </body> </html>
When you open the page in Listing 18.3, only the second-level nodes and descendent nodes are displayed (see Figure 18.2).
The StartFromCurrentNode
property is useful when you want to display a list of all nodes below the current node. For example, the page in Listing 18.4 is the Default.aspx
page contained in the Products folder. It displays a list of all the product pages contained in the folder.
Example 18.4. Products/Default.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/ xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> html { font:16px Georgia,Serif; } .productList li { margin:5px; } </style> <title>Our Products</title> </head> <body> <form id="form1" runat="server"> <div> <h1>Products</h1> <asp:BulletedList id="bltProducts" DisplayMode="HyperLink" DataTextField="Title" DataValueField="Url" DataSourceID="srcSiteMap" CssClass="productList" Runat="server" /> <asp:SiteMapDataSource id="srcSiteMap" ShowStartingNode="false" StartFromCurrentNode="true" Runat="server" /> </div> </form> </body> </html>
The page in Listing 18.4 contains a BulletedList
control bound to a SiteMapDataSource
control. Because the SiteMapDataSource
control has its StartFromCurrentNode
property set to the value True
and its ShowStartingNode
property set to the value False
, all immediate child nodes of the current node are displayed (see Figure 18.3).
Under the covers, the SiteMapDataSource
control represents the contents of the SiteMap
class. The SiteMap
class represents an application’s Site Map regardless of whether the Site Map is stored in an XML file, a database, or some other data source. The class is a memory-resident representation of Site Map data.
All the properties exposed by the SiteMap
class are shared (static) properties:
CurrentNode
—. Enables you to retrieve the SiteMapNode
that corresponds to the current page.
Enabled
—. Enables you to determine whether the Site Map is enabled.
Provider
—. Enables you to retrieve the default SiteMapProvider
.
Providers
—. Enables you to retrieve all the configured SiteMapProvders
.
RootNode
—. Enables you to retrieve the root SiteMapNode
.
The CurrentNode
and RootNode
properties return a SiteMapNode
object. Because a Site Map can contain only one root node, and the root node contains all the other nodes as children, the RootNode
property enables you to iterate through all the nodes in a Site Map.
The Provider
property returns the default SiteMapProvider
. You can use this property to access all the properties and methods of the SiteMapProvider
class, such as the FindSiteMapNode()
and GetParentNode()
methods.
The SiteMap
class also supports a single event:
SiteMapResolve
—. Raised when the current node is accessed.
You can handle this event to modify the node returned when the current node is retrieved. For example, the Global.asax
file in Listing 18.5 automatically adds a new node when the current page does not include a node in the Site Map.
Example 18.5. Global.asax
<%@ Application Language="VB" %> <%@ Import Namespace="System.IO" %> <script runat="server"> Private Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs) AddHandler SiteMap.SiteMapResolve, AddressOf SiteMap_SiteMapResolve End Sub Private Function SiteMap_SiteMapResolve(ByVal sender As Object, ByVal e As SiteMapResolveEventArgs) As SiteMapNode If SiteMap.CurrentNode Is Nothing Then Dim url As String = e.Context.Request.Path Dim title As String = Path.GetFileNameWithoutExtension(url) Dim NewNode As New SiteMapNode(e.Provider, url, url, title) NewNode.ParentNode = SiteMap.RootNode Return NewNode End If Return SiteMap.CurrentNode End Function </script>
The Application_Start()
event handler in Listing 18.5 executes only once when the application first starts. The handler adds a SiteMapResolve
event handler to the SiteMap
class.
Whenever any control retrieves the current node, the SiteMap_SiteMapResolve()
method executes. If there is no node that corresponds to a page, then the method creates a new node and returns it.
The About.aspx
page in Listing 18.6 is not included in the Web.sitemap
file. However, this page includes a SiteMapPath
control. The SiteMapPath
control works correctly because the About.aspx
page is dynamically added to the Site Map when you access the page (see Figure 18.4).
Example 18.6. About.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/ xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>About</title> </head> <body> <form id="form1" runat="server"> <div> <asp:SiteMapPath id="SiteMapPath1" Runat="server" /> <hr /> <h1>About Our Company</h1> </div> </form> </body> </html>
All pages and folders in a Site Map are represented by instances of the SiteMapNode
class. The SiteMapNode
class contains the following properties:
Description
—. Returns the description of the current node.
HasChildNodes
—. Returns True
when the current node has child nodes.
Item
—. Returns a custom attribute (or resource string).
Key
—. Returns a unique identifier for the current node.
NextSibling
—. Returns the next sibling of the current node.
ParentNode
—. Returns the parent node of the current node.
PreviousSibling
—. Returns the previous sibling of the current node.
Provider
—. Returns the SiteMapProvider
associated with the current node.
ReadOnly
—. Returns true when a node is read-only.
ResourceKey
—. Returns the resource key associated with the current node (enables localization).
Roles
—. Returns the user roles associated with the current node.
RootNode
—. Returns the Site Map root node.
Title
—. Returns the title associated with the current node.
Url
—. Returns the URL associated with the current node.
The SiteMapNode
class also supports the following methods:
Clone()
—. Returns a clone of the current node.
GetAllNodes()
—. Returns all descendent nodes of the current node.
GetDataSourceView()
—. Returns a SiteMapDataSourceView
object.
GetHierarchicalDataSourceView()
—. Returns a SiteMapHierarchicalDataSourceView
.
IsAccessibleToUser()
—. Returns True
when the current user has permissions to view the current node.
IsDescendantOf()
—. Returns True
when the current node is a descendant of a particular node.
By taking advantage of the SiteMap
and SiteMapNode
classes, you can work directly with Site Maps in a page. For example, imagine that you want to display the value of the SiteMapNode
title attribute in both the browser’s title bar and in the body of the page. Listing 18.7 demonstrates how you can retrieve the value of the Title property associated with the current page programmatically.
Example 18.7. Products/FirstProduct.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/ xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Private Sub Page_Load() If Not Page.IsPostBack Then Dim currentNode As SiteMapNode = SiteMap.CurrentNode Me.Title = currentNode.Title ltlBodyTitle.Text = currentNode.Title lblDescription.Text = currentNode.Description End If End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>First Product</title> </head> <body> <form id="form1" runat="server"> <div> <h1><asp:Literal ID="ltlBodyTitle" runat="server" /></h1> <asp:Label id="lblDescription" Runat="server" /> </div> </form> </body> </html>
When you open the page in Listing 18.7, the Page_Load()
event handler grabs the current SiteMapNode
and modifies the Page Title
property. The handler also assigns the value of the Title
property to a Literal
control contained in the body of the page. Finally, the value of the SiteMapNode
’s Description
property is assigned to a Label control (see Figure 18.5).
It would make sense to place the code in Listing 18.7 in a Master Page. To learn more about Master Pages, see Chapter 5, “Designing Web Sites with Master Pages.”
This section explores several advanced features of Site Maps. For example, you learn how to display different SiteMap
nodes, depending on the roles associated with the current user. You also learn how to create multiple Site Maps for a single application. Finally, you learn how you can extend Site Maps with custom attributes.
You might want to display different navigation links to different users, depending on their roles. For example, if a user is a member of the Administrators role, you might want to display links to pages for administrating the website. However, you might want to hide these links from other users.
To display different links to different users depending on their roles, you must enable a feature of Site Maps named Security Trimming. This feature is disabled by default. The web configuration file in Listing 18.8 enables Security Trimming.
Example 18.8. Web.Config
<?xml version="1.0"?> <configuration> <system.web> <authentication mode="Windows" /> <roleManager enabled="true" /> <siteMap defaultProvider="MySiteMapProvider"> <providers> <add name="MySiteMapProvider" type="System.Web.XmlSiteMapProvider" securityTrimmingEnabled="true" siteMapFile="Web.sitemap" /> </providers> </siteMap> </system.web> </configuration>
Notice that the configuration file in Listing 18.8 includes a <siteMap>
element that configures a new SiteMapProvider
named MySiteMapProvider
. The new provider enables Security Trimming with its securityTrimmingEnabled
property.
After you enable Security Trimming, any pages a user is not allowed to view are automatically hidden. For example, imagine that your website includes a folder named Admin that contains the web configuration file in Listing 18.9.
The configuration file in Listing 18.9 prevents anyone who is not a member of the WebAdmin role from viewing pages in the same folder (and below) as the configuration file. Even if the Web.sitemap
file includes nodes that represent pages in the Admin folder, the links don’t appear for anyone except members of the WebAdmin role.
Another option is to explicitly associate roles with nodes in a Site Map. This is useful in two situations. First, if your website contains links to another website, then you can hide or display these links based on the user role. Second, if you explicitly associate roles with pages, then you hide page links even when a user has permission to view a page.
The Web.sitemap
file in Listing 18.10 contains links to the Microsoft, Google, and Yahoo! websites. A different set of roles is associated with each link.
Example 18.10. Web.sitemap
<?xml version="1.0" encoding="utf-8" ?> <siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" > <siteMapNode title="External Links" description="Links to external Websites" roles="RoleA,RoleB,RoleC"> <siteMapNode title="Google" url="http://www.Google.com" description="The Google Website" roles="RoleA" /> <siteMapNode title="Microsoft" url="http://www.Microsoft.com" description="The Microsoft Website" roles="RoleB" /> <siteMapNode title="Yahoo" url="http://www.Yahoo.com" description="The Yahoo Website" roles="RoleC" /> </siteMapNode> </siteMap>
The page in Listing 18.11 enables you to add yourself and remove yourself from different roles. Notice that different links appear in the TreeView
control, depending on which roles you select.
Example 18.11. ShowSecurityTrimming.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/ xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Sub Page_Load() If Not Page.IsPostBack Then For Each item As ListItem In cblSelectRoles.Items If Not Roles.RoleExists(item.Text) Then Roles.CreateRole(item.Text) Roles.AddUserToRole(User.Identity.Name, item.Text) End If Next End If End Sub Sub btnSelect_Click(ByVal sender As Object, ByVal e As EventArgs) For Each item As ListItem In cblSelectRoles.Items If item.Selected Then If Not User.IsInRole(item.Text) Then Roles.AddUserToRole(User.Identity.Name, item.Text) End If Else If User.IsInRole(item.Text) Then Roles.RemoveUserFromRole(User.Identity.Name, item.Text) End If End If Next Response.Redirect(Request.Path) End Sub Sub Page_PreRender() For Each item As ListItem In cblSelectRoles.Items item.Selected = User.IsInRole(item.Text) Next End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> html { background-color:silver; } .column { float:left; width:300px; border:Solid 1px black; background-color:white; padding:10px; } </style> <title>Show Security Trimming</title> </head> <body> <form id="form1" runat="server"> <div class="column"> <asp:Label id="lblSelectRoles" Text="Select Roles:" AssociatedControlID="cblSelectRoles" Runat="server" /> <br /> <asp:CheckBoxList id="cblSelectRoles" Runat="server"> <asp:ListItem Text="RoleA" /> <asp:ListItem Text="RoleB" /> <asp:ListItem Text="RoleC" /> </asp:CheckBoxList> <asp:Button id="btnSelect" Text="Select" OnClick="btnSelect_Click" Runat="server" /> </div> <div class="column"> <asp:TreeView id="TreeView1" DataSourceID="srcSiteMap" Runat="server" /> <asp:SiteMapDataSource id="srcSiteMap" Runat="server" /> </div> </form> </body> </html>
When you first open the page in Listing 18.11, the Page_Load()
handler creates three roles—RoleA, RoleB, and RoleC—and adds the current user to each role.
The CheckBoxList
control in the body of the page enables you to select the roles that you want to join. Notice that different links to external websites appear, depending on which roles you select (see Figure 18.6).
To make it easier to manage a large application, you can store Site Maps in more than one location and merge the Site Maps at runtime. For example, if you are using the default SiteMapProvider
—the XmlSiteMapProvider
—then you can create multiple sitemap files that describe the navigation structure of different sections of your website.
For example, the Web.sitemap
file in Listing 18.12 includes a node that points to another sitemap file.
Example 18.12. Web.sitemap
<?xml version="1.0" encoding="utf-8" ?> <siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" > <siteMapNode url="Default.aspx" title="Home" description="The Home Page"> <siteMapNode url="Products/Default.aspx" title="Our Products" description="Products that we offer"> <siteMapNode url="Products/FirstProduct.aspx" title="First Product" description="The description of the First Product" /> <siteMapNode url="Products/SecondProduct.aspx" title="Second Product" description="The description of the Second Product" /> </siteMapNode> <siteMapNode url="Services" title="Our Services" description="Services that we offer"> <siteMapNode url="Services/FirstService.aspx" title="First Service" description="The description of the First Service" metaDescription="The first service" /> <siteMapNode url="Services/SecondService.aspx" title="Second Service" description="The description of the Second Service" /> </siteMapNode> <siteMapNode siteMapFile="Employees/Employees.sitemap" /> </siteMapNode> </siteMap>
The sitemap in Listing 18.12 includes the following node:
<siteMapNode siteMapFile="Employees/Employees.sitemap" />
This node includes a siteMapFile
attribute that points to a sitemap located in the Employees subdirectory of the current application. The contents of the Employees.sitemap
are automatically merged with the default Web.sitemap
.
The Employees.sitemap
is contained in Listing 18.13.
Example 18.13. Employees/Employees.sitemap
<?xml version="1.0" encoding="utf-8" ?> <siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" > <siteMapNode url="Employees/Default.aspx" title="Employees" description="Contains descriptions of employees"> <siteMapNode url="Employees/BillGates.aspx" title="Bill Gates" description="Bill Gates Page" /> <siteMapNode url="Employees/SteveJobs.aspx" title="Steve Jobs" description="Steve Jobs Page" /> </siteMapNode> </siteMap>
Notice that there is nothing special about the sitemap in Listing 18.13. It contains a description of the two pages in the Employees subdirectory.
This is a great feature for working with large websites. Each section of the website can be managed by a different developer. When the website is accessed by a user, the contents of the different sitemaps are seamlessly stitched together.
You can extend a Site Map with your own custom attributes. You can use a custom attribute to represent any type of information that you want.
For example, imagine that you want to associate <meta>
Description tags with each page in your web application to make it easier for search engines to index your website. In that case, you can add a metaDescription
attribute to the nodes in a Web.sitemap
file.
The Web.sitemap
file in Listing 18.14 includes metaDescription
attributes for the two Services pages.
Example 18.14. Web.sitemap
<?xml version="1.0" encoding="utf-8" ?> <siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" > <siteMapNode url="Default.aspx" title="Home" description="The Home Page"> <siteMapNode url="Products/Default.aspx" title="Our Products" description="Products that we offer"> <siteMapNode url="Products/FirstProduct.aspx" title="First Product" description="The description of the First Product" /> <siteMapNode url="Products/SecondProduct.aspx" title="Second Product" description="The description of the Second Product" /> </siteMapNode> <siteMapNode url="Services/Default.aspx" title="Our Services" description="Services that we offer"> <siteMapNode url="Services/FirstService.aspx" title="First Service" description="The description of the First Service" metaDescription="The first service" /> <siteMapNode url="Services/SecondService.aspx" title="Second Service" description="The description of the Second Service" metaDescription="The second service" /> </siteMapNode> </siteMapNode> </siteMap>
Visual Web Developer displays blue squiggles (warning messages) under any custom attributes in a SiteMap file. You can safely ignore these warnings.
Any custom attributes that you add to a Site Map are exposed by instances of the SiteMapNode
class. For example, the page in Listing 18.15 retrieves the value of the metaDescription
attribute from the current node and displays the value in an actual <meta>
tag.
Example 18.15. Services/FirstService.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/ xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Private Sub Page_Load() Dim meta As HtmlMeta = New HtmlMeta() meta.Name = "Description" meta.Content = SiteMap.CurrentNode("metaDescription") head1.Controls.Add(meta) End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="head1" runat="server"> <title>First Service</title> </head> <body> <form id="form1" runat="server"> <div> <h1>The First Service</h1> </div> </form> </body> </html>
After you open the page in Listing 18.15 in a web browser, you can select View, Source to see the <meta>
tag added to the source of the page (see Figure 18.7).
It is important emphasize that you can do anything you want with custom SiteMapNode
attributes. You can represent page titles, section titles, product icons, or anything else with a custom attribute.
Site Maps use the provider model. This means that you can easily modify or extend the way Site Maps work by creating your own Site Map provider.
In this section, we create two custom Site Map providers. First, we create the AutoSiteMapProvider
. This provider automatically builds a Site Map based on the file and folder structure of a website.
Next, we create a SqlSiteMapProvider
. This provider enables you to store a Site Map in a Microsoft SQL Server database table instead of an XML file.
All Site Map providers inherit from the base SiteMapProvider
class. If you want to create your own Site Map provider, then you can override the methods of this base class.
However, in most cases it makes more sense to derive a custom Site Map provider from the base StaticSiteMapProvider
class. This is the base class for the default Site Map provider—the XmlSiteMapProvider
—and this class includes default implementations of many of the SiteMapProvider
methods.
This AutoSiteMapProvider
derives from the StaticSiteMapProvider
class. It overrides two methods of the base class: GetRootNodeCore()
and BuildSiteMap()
.
The GetRootNodeCore()
method returns the root node of the Site Map. The BuildSiteMap()
method is the method that is actually responsible for building the Site Map.
The AutoSiteMapProvider
is contained in Listing 18.16.
Example 18.16. App_Code/AutoSiteMapProvider.vb
Imports System Imports System.Collections.Generic Imports System.IO Imports System.Web Imports System.Web.Caching Namespace AspNetUnleashed Public Class AutoSiteMapProvider Inherits StaticSiteMapProvider Private _rootNode As SiteMapNode Private Shared _excluded As New List(Of String)() Private _dependencies As New List(Of String)() ''' <summary> ''' These folders and pages won't be added ''' to the Site Map ''' </summary> Shared Sub New() _excluded.Add("app_code") _excluded.Add("app_data") _excluded.Add("app_themes") _excluded.Add("bin") End Sub ''' <summary> ''' Return the root node of the Site Map ''' </summary> Protected Overrides Function GetRootNodeCore() As SiteMapNode Return BuildSiteMap() End Function ''' <summary> ''' Where all the work of building the Site Map happens ''' </summary> Public Overrides Function BuildSiteMap() As SiteMapNode ' Allow the Site Map to be created by only a single thread SyncLock Me ' Attempt to get Root Node from Cache Dim context As HttpContext = HttpContext.Current _rootNode = CType(context.Cache("RootNode"), SiteMapNode) If _rootNode Is Nothing Then ' Clear current Site Map Clear() ' Create root node Dim folderUrl As String = HttpRuntime.AppDomainAppVirtualPath Dim defaultUrl As String = folderUrl + "/Default.aspx" _rootNode = New SiteMapNode(Me, folderUrl, defaultUrl, "Home") AddNode(_rootNode) ' Create child nodes AddChildNodes(_rootNode) _dependencies.Add(HttpRuntime.AppDomainAppPath) ' Add root node to cache with file dependencies Dim fileDependency As CacheDependency = New CacheDependency(_dependencies.ToArray()) context.Cache.Insert("RootNode", _rootNode, fileDependency) End If Return _rootNode End SyncLock End Function ''' <summary> ''' Add child folders and pages to the Site Map ''' </summary> Private Sub AddChildNodes(ByVal parentNode As SiteMapNode) AddChildFolders(parentNode) AddChildPages(parentNode) End Sub ''' <summary> ''' Add child folders to the Site Map ''' </summary> Private Sub AddChildFolders(ByVal parentNode As SiteMapNode) Dim context As HttpContext = HttpContext.Current Dim parentFolderPath As String = context.Server.MapPath(parentNode.Key) Dim folderInfo As DirectoryInfo = New DirectoryInfo(parentFolderPath) ' Get sub folders Dim folders() As DirectoryInfo = folderInfo.GetDirectories() For Each folder As DirectoryInfo In folders If Not _excluded.Contains(folder.Name.ToLower()) Then Dim folderUrl As String = parentNode.Key + "/" + folder.Name Dim folderNode As SiteMapNode = New SiteMapNode(Me, folderUrl, Nothing, GetName(folder.Name)) AddNode(folderNode, parentNode) AddChildNodes(folderNode) _dependencies.Add(folder.FullName) End If Next End Sub ''' <summary> ''' Add child pages to the Site Map ''' </summary> Private Sub AddChildPages(ByVal parentNode As SiteMapNode) Dim context As HttpContext = HttpContext.Current Dim parentFolderPath As String = context.Server.MapPath(parentNode.Key) Dim folderInfo As DirectoryInfo = New DirectoryInfo(parentFolderPath) Dim pages() As FileInfo = folderInfo.GetFiles("*.aspx") For Each page As FileInfo In pages If Not _excluded.Contains(page.Name.ToLower()) Then Dim pageUrl As String = parentNode.Key + "/" + page.Name If String.Compare(pageUrl, _rootNode.Url, True) <> 0 Then Dim pageNode As SiteMapNode = New SiteMapNode(Me, pageUrl, pageUrl, GetName(page.Name)) AddNode(pageNode, parentNode) End If End If Next End Sub ''' <summary> ''' Fix the name of the page or folder ''' by removing the extension and replacing ''' underscores with spaces ''' </summary> Private Function GetName(ByVal name As String) As String name = Path.GetFileNameWithoutExtension(name) Return Name.Replace("_", " ") End Function End Class End Namespace
Almost all of the work in Listing 18.16 happens in the BuildSiteMap()
method. This method recursively iterates through all the folders and pages in the current web application creating SiteMapNodes
. When the method completes its work, a Site Map that reflects the folder and page structure of the website is created.
You should notice two special aspects of the code in Listing 18.16. First, file dependencies are created for each folder. If you add a new folder or page to your website, the BuildSiteMap()
method is automatically called the next time you request a page.
Second, notice that the constructor for the AutoSiteMapProvider
class creates a list of excluded files. For example, this list includes the App_Code
and Bin folders. You do not want these files to appear in a Site Map. If there are other special files that you want to hide, then you need to add the filenames to the list of excluded files in the constructor.
After you create the AutoSiteMapProvider
class, you need to configure your application to use the custom Site Map provider. You can use the configuration file in Listing 18.17 to enable the AutoSiteMapProvider
.
Example 18.17. Web.Config
<?xml version="1.0"?> <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0"> <system.web> <siteMap defaultProvider="MyAutoSiteMapProvider"> <providers> <add name="MyAutoSiteMapProvider" type="AspNetUnleashed.AutoSiteMapProvider" /> </providers> </siteMap> </system.web> </configuration>
The configuration file in Listing 18.17 configures the AutoSiteMapProvider
as the application’s default provider.
You can try out the AutoSiteMapProvider
by requesting the Default.aspx
page from the AutoSiteMapProviderApp
Web application contained on the CD that accompanies this book. This application does not include a Web.sitemap
file. The Site Map is automatically generated from the structure of the website.
For certain applications it makes more sense to store a Site Map in a database table than an XML file. In this section, you can see the creation of the SqlSiteMapProvider
, which stores a Site Map in a Microsoft SQL Server database.
To use the SqlSiteMapProvider
class, you must create a SQL database table named SiteMap. Furthermore, the SiteMap database table must look like this:
Id | ParentId | Url | Title | Description |
---|---|---|---|---|
1 | null | Default.aspx | Home | The Home Page |
2 | 1 | Products | Products | |
3 | 2 | Products/FirstProduct.aspx | First Product | The First Product |
4 | 2 | Products/SecondProduct.aspx | Second Product | The Second Product |
6 | 1 | Services | Services | |
7 | 6 | Services/FirstService.aspx | First Service | The First Service |
Each row in the SiteMap table represents a particular Site Map node. The relationship between the nodes is represented by the ParentId column. The row that represents the root node has a ParentId column with the value null
. Every other row is either a child of the root node or the child of some other node.
The code for the SqlSiteMapProvider
is contained in Listing 18.18.
Example 18.18. App_CodeSqlSiteMapProvider.vb
Imports System Imports System.Collections.Specialized Imports System.Web.Configuration Imports System.Data Imports System.Data.SqlClient Imports System.Web Imports System.Web.Caching Namespace AspNetUnleashed ''' <summary> ''' Summary description for SqlSiteMapProvider ''' </summary> Public Class SqlSiteMapProvider Inherits StaticSiteMapProvider Private _isInitialized As Boolean = False Private _connectionString As String Private _rootNode As SiteMapNode ''' <summary> ''' Initialize provider with database ''' connection string ''' </summary> Public Overrides Sub Initialize(ByVal name As String, ByVal attributes As NameValueCollection) If _isInitialized Then Return End If MyBase.Initialize(name, attributes) Dim connectionStringName As String = attributes("connectionStringName") If String.IsNullOrEmpty(connectionStringName) Then Throw New Exception("You must provide a connectionStringName attribute") End If _connectionString = WebConfigurationManager.ConnectionStrings(connectionStringName). ConnectionString If String.IsNullOrEmpty(_connectionString) Then Throw New Exception("Could not find connection String " & [ic;ccc] connectionStringName) End If _isInitialized = True End Sub ''' <summary> ''' Return root node by calling ''' BuildSiteMap ''' </summary> Protected Overrides Function GetRootNodeCore() As SiteMapNode Return BuildSiteMap() End Function ''' <summary> ''' Build the Site Map and ''' create SQL Cache Dependency ''' </summary> ''' <returns></returns> ''' <remarks></remarks> Public Overrides Function BuildSiteMap() As SiteMapNode ' Only allow the Site Map to be created by a single thread SyncLock Me ' Attempt to get Root Node from Cache Dim context As HttpContext = HttpContext.Current _rootNode = CType(context.Cache("RootNode"), SiteMapNode) If _rootNode Is Nothing Then HttpContext.Current.Trace.Warn("Loading from database") ' Clear current Site Map Clear() ' Load the database data Dim tblSiteMap As DataTable = GetSiteMapFromDB() ' Get the root node _rootNode = GetRootNode(tblSiteMap) AddNode(_rootNode) ' Build the child nodes BuildSiteMapRecurse(tblSiteMap, _rootNode) ' Add root node to cache with database dependency Dim sqlDepend As SqlCacheDependency = New SqlCacheDependency("SiteMapDB", "SiteMap") context.Cache.Insert("RootNode", _rootNode, sqlDepend) End If Return _rootNode End SyncLock End Function ''' <summary> ''' Loads Site Map from Database ''' </summary> Private Function GetSiteMapFromDB() As DataTable Dim selectCommand As String = "SELECT Id,ParentId,Url,Title,Description FROM SiteMap" Dim dad As New SqlDataAdapter(selectCommand, _connectionString) Dim tblSiteMap As New DataTable() dad.Fill(tblSiteMap) Return tblSiteMap End Function ''' <summary> ''' Gets the root node by returning row ''' with null ParentId ''' </summary> Private Function GetRootNode(ByVal siteMapTable As DataTable) As SiteMapNode Dim results() As DataRow = siteMapTable.Select("ParentId IS NULL") If results.Length = 0 Then Throw New Exception("No root node in database") End If Dim rootRow As DataRow = results(0) Return New SiteMapNode(Me, rootRow("Id").ToString(), rootRow("url").ToString(), rootRow("title").ToString(), rootRow("description").ToString()) End Function ''' <summary> ''' Recursively builds a Site Map by iterating ParentId ''' </summary> Private Sub BuildSiteMapRecurse(ByVal siteMapTable As DataTable, ByVal parentNode As SiteMapNode) Dim results() As DataRow = siteMapTable.Select("ParentId=" + parentNode.Key) For Each row As DataRow In results Dim node As SiteMapNode = New SiteMapNode(Me, row("Id").ToString(), row("url").ToString(), row("title").ToString(), row("description").ToString()) AddNode(node, parentNode) BuildSiteMapRecurse(siteMapTable, node) Next End Sub End Class End Namespace
Like the custom Site Map provider that was created in the previous section, the SqlSiteMapProvider
derives from the base StaticSiteMapProvider
class. The SqlSiteMapProvider
class overrides three methods of the base class: Initialize()
, GetRootNodeCore()
, and BuildSiteMap()
.
The Initialize()
method retrieves a database connection string from the web configuration file. If a database connection string cannot be retrieved, then the method throws a big, fat exception.
Almost all the work happens in the BuildSiteMap()
method. This method loads the contents of the SiteMap database table into an ADO.NET DataTable. Next, it recursively builds the Site Map nodes from the DataTable.
There is one special aspect of the code in Listing 18.18. It uses a SQL cache dependency to automatically rebuild the Site Map when the contents of the SiteMap database table are changed.
To enable SQL cache dependencies for a database, you must configure the database with either the enableNotifications
tool or the aspnet_regsql
tool. Use the enableNotifications
tool when enabling SQL cache dependencies for a SQL Express database table, and use the aspnet_regsql
tool when enabling SQL cache dependencies for the full version of Microsoft SQL Server.
To learn more about configuring SQL cache dependencies, see Chapter 23, “Caching Application Pages and Data.”
To enable SQL cache dependencies for a SQL Express database named SiteMapDB that contains a table named SiteMap, browse to the folder that contains the SiteMapDB.mdf file and execute the following command from a Command Prompt:
enableNotifications "SiteMapDB.mdf" "SiteMap"
You can configure your website to use the SqlSiteMapProvider
class with the Web configuration file in Listing 18.19.
Example 18.19. Web.Config
<?xml version="1.0"?> <configuration> <connectionStrings> <add name="conSiteMap" connectionString="Data Source=.SQLExpress;Integrated Security=True;AttachDbFileName=|DataDirectory|SiteMapDB.mdf;User Instance=True"/> </connectionStrings> <system.web> <siteMap defaultProvider="myProvider"> <providers> <add name="myProvider" type="AspNetUnleashed.SqlSiteMapProvider" connectionStringName="conSiteMap" /> </providers> </siteMap> <caching> <sqlCacheDependency enabled = "true" pollTime = "5000" > <databases> <add name="SiteMapDB" connectionStringName="conSiteMap" /> </databases> </sqlCacheDependency> </caching> </system.web> </configuration>
The configuration file in Listing 18.19 accomplishes several tasks. First, it configures the SqlSiteMapProvider
as the default Site Map provider. Notice that the provider includes a connectionStringName
attribute that points to the connection string for the local SQL Express database named SiteMapDB.
The configuration file also enables SQL cache dependency polling. The application is configured to poll the SiteMapDB database for changes every five seconds. In other words, if you make a change to the SiteMap database table, the Site Map is updated to reflect the change within five seconds.
You can try out the SqlSiteMapProvider
by opening the Default.aspx
page included in the SqlSiteMapProviderApp
web application on the CD that accompanies this book. If you modify the SiteMap database table, the changes are automatically reflected in the Site Map (see Figure 18.9).
Google provides a free service, named Google SiteMaps, that you can use to monitor and improve the way that Google indexes the pages on your website. For example, you can use Google SiteMaps to discover which Google search queries have returned pages from your website and the ranking of your pages in Google search results. You also can use Google SiteMaps to view any problems that the Google crawler encounters when indexing your site.
You can sign up for Google SiteMaps by visiting the following URL:
http://www.google.com/webmasters/sitemaps
To use Google SiteMaps, you must provide Google with the URL of a Google SiteMap file hosted on your website. The Google SiteMap file is an XML file that contains a list of URLs you want Google to index.
The Google SiteMap XML file has the following format:
<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.google.com/schemas/sitemap/0.84"> <url> <loc>http://www.example.com/</loc> <lastmod>2005-01-01</lastmod> </url> <url> <loc>http://www.example.com/sample.html/</loc> <lastmod>2006-03-11</lastmod> </url> </urlset>
The Google SiteMap file contains a simple list of <url>
elements that contain <loc>
elements representing the location of the URL and <lastmod>
elements representing the last modified date of the URL.
The Google SiteMap file also can contain <changefreq>
and <priority>
elements. The <changefreq>
element indicates how frequently a URL changes, and the <priority>
element represents the priority of a URL relative to other URLs in your site. These elements are optional and are ignored here.
You can generate a Google SiteMap file automatically from an ASP.NET SiteMap. The HTTP Handler in Listing 18.20 generates a Google SiteMap that conforms to Google’s requirements for a valid SiteMap file.
Example 18.20. PublicSiteMap.ashx
<%@ WebHandler Language="VB" Class="PublicSiteMap" %> Imports System Imports System.Web Imports System.Xml Imports System.Text Imports System.IO Public Class PublicSiteMap Implements IHttpHandler Private _xmlWriter As XmlWriter Public Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest context.Response.ContentType = "text/xml" Dim settings As New XmlWriterSettings() settings.Encoding = Encoding.UTF8 settings.Indent = True _xmlWriter = XmlWriter.Create(context.Response.OutputStream, settings) _xmlWriter.WriteStartDocument() _xmlWriter.WriteStartElement("urlset", "http://www.google.com/schemas/sitemap/0.84") ' Add root node AddUrl(SiteMap.RootNode) ' Add all other nodes Dim nodes As SiteMapNodeCollection = SiteMap.RootNode.GetAllNodes() For Each node As SiteMapNode In nodes AddUrl(node) Next _xmlWriter.WriteEndElement() _xmlWriter.WriteEndDocument() _xmlWriter.Flush() End Sub Private Sub AddUrl(ByVal node As SiteMapNode) ' Skip empty Urls If String.IsNullOrEmpty(node.Url) Then Return End If ' Skip remote nodes If node.Url.StartsWith("http",True,Nothing) Then Return End If ' Open url tag _xmlWriter.WriteStartElement("url") ' Write location _xmlWriter.WriteStartElement("loc") _xmlWriter.WriteString(GetFullUrl(node.Url)) _xmlWriter.WriteEndElement() ' Write last modified _xmlWriter.WriteStartElement("lastmod") _xmlWriter.WriteString(GetLastModified(node.Url)) _xmlWriter.WriteEndElement() ' Close url tag _xmlWriter.WriteEndElement() End Sub Private Function GetFullUrl(ByVal url As String) As String Dim context As HttpContext = HttpContext.Current Dim server As String = context.Request.Url.GetComponents( UriComponents.SchemeAndServer,UriFormat.UriEscaped) Return Combine(server,url) End Function Private Function Combine(ByVal baseUrl As String, ByVal url As String) As String baseUrl = baseUrl.TrimEnd(New Char() {"/"c}) url = url.TrimStart(New Char() {"/"c}) Return baseUrl + "/" + url End Function Private Function GetLastModified(ByVal url As String) As String Dim context As HttpContext = HttpContext.Current Dim physicalPath As String = context.Server.MapPath(url) Return File.GetLastWriteTimeUtc(physicalPath).ToString("s") End Function Public ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable Get Return True End Get End Property End Class
The HTTP Handler in Listing 18.20 generates an XML file by iterating through each of the nodes in an ASP.NET Site Map. The XML file is created with the help of the XmlWriter
class. This class is used to generate each of the XML tags.
You can think of an HTTP Handler is a lightweight ASP.NET page. You learn about HTTP Handlers in Chapter 25, “Working with the HTTP Runtime.”
The file in Listing 18.21 contains the XML file returned by the PublicSiteMap.ashx
handler when the Handler is called from the sample application contained on the CD that accompanies this book. (The file has been abridged for reasons of space.)
Example 18.21. PublicSiteMap.ashx
Results
<?xml version="1.0" encoding="utf-8"?> <urlset xmlns="http://www.google.com/schemas/sitemap/0.84"> <url> <loc>http://localhost:2905/SiteMaps/Default.aspx</loc> <lastmod>2005-10-30T03:13:58</lastmod> </url> <url> <loc>http://localhost:2905/SiteMaps/Products/Default.aspx</loc> <lastmod>2005-10-28T21:48:04</lastmod> </url> <url> <loc>http://localhost:2905/SiteMaps/Services</loc> <lastmod>2005-10-30T04:31:57</lastmod> </url> <url> <loc>http://localhost:2905/SiteMaps/Employees/Default.aspx</loc> <lastmod>1601-01-01T00:00:00</lastmod> </url> <url> <loc>http://localhost:2905/SiteMaps/Products/FirstProduct.aspx</loc> <lastmod>2005-10-30T03:43:52</lastmod> </url> </urlset>
When you sign up at the Google SiteMaps website, submit the URL of the PublicSiteMap.ashx
file when you are asked to enter your SiteMap URL. The Google service retrieves your SiteMap from the handler automatically.
In this chapter, you learned how to work with Site Maps. The first section discussed the SiteMapDataSource
control. You learned how to declaratively represent different sets of nodes in a Site Map with this control.
Next, the SiteMap
and SiteMapNode
classes were examined. You learned how to create new Site Map nodes dynamically by handling the SiteMapResolve
event. You also learned how to programmatically retrieve the current Site Map node in a page.
The next section discussed several advanced features of Site Maps. You learned how to display different Site Map nodes to different users depending on their roles. You also learned how to merge SiteMap files located in different subfolders. Finally, you learned how to extend Site Maps with custom attributes.
We also built two custom Site Map providers. We created an AutoSiteMapProvider
that automatically builds a Site Map that reflects the folder and page structure of a website. We also created a SqlSiteMapProvider
that stores a Site Map in a Microsoft SQL Server database table.
Finally, you learned how to use ASP.NET Site Maps with Google SiteMaps. In the final section of this chapter, you learned how to create a custom HTTP Handler that converts an ASP.NET Site Map into a Google SiteMap so that you can improve the way that Google indexes your website’s pages.