ptg 1104 CHAPTER 23 Using Site Maps </siteMapNode> </siteMapNode> </siteMap> Any custom attributes that you add to a Site Map are exposed by instances of the SiteMapNode class. For example, the page in Listing 23.15 retrieves the value of the metaDescription attribute from the current node and displays the value in an actual <meta> tag. LISTING 23.15 Services/FirstService.aspx <%@ Page Language=”C#” %> <!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.1//EN” “http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd”> <script runat=”server”> void Page_Load() { HtmlMeta meta = new HtmlMeta(); meta.Name = “Description”; meta.Content = SiteMap.CurrentNode[“metaDescription”]; head1.Controls.Add(meta); } </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> From the Library of Wow! eBook ptg 1105 Creating Custom Site Map Providers 23 FIGURE 23.7 Extending a Site Map with a <meta> tag. After you open the page in Listing 23.15 in a web browser, you can select View, Source to see the <meta> tag added to the source of the page (see Figure 23.7). It is important to 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. Creating Custom Site Map Providers 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. From the Library of Wow! eBook ptg 1106 CHAPTER 23 Using Site Maps Creating the AutoSiteMapProvider All Site Map providers inherit from the base SiteMapProvider class. If you want to create your own Site Map provider, 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 actually responsible for building the Site Map. The AutoSiteMapProvider is contained in Listing 23.16. LISTING 23.16 App_Code/AutoSiteMapProvider.cs using System; using System.Collections.Generic; using System.IO; using System.Web; using System.Web.Caching; namespace AspNetUnleashed { public class AutoSiteMapProvider : StaticSiteMapProvider { private SiteMapNode _rootNode; private static List<string> _excluded = new List<string>(); private List<string> _dependencies = new List<string>(); /// <summary> /// These folder and pages won’t be added /// to the Site Map /// </summary> static AutoSiteMapProvider() { _excluded.Add(“app_code”); _excluded.Add(“app_data”); _excluded.Add(“app_themes”); _excluded.Add(“bin”); } From the Library of Wow! eBook ptg 1107 Creating Custom Site Map Providers 23 /// <summary> /// Return the root node of the Site Map /// </summary> protected override SiteMapNode GetRootNodeCore() { return BuildSiteMap(); } /// <summary> /// Where all of the work of building the Site Map happens /// </summary> public override SiteMapNode BuildSiteMap() { // Only allow the Site Map to be created by a single thread lock (this) { // Attempt to get Root Node from Cache HttpContext context = HttpContext.Current; _rootNode = (SiteMapNode)context.Cache[“RootNode”]; if (_rootNode == null) { // Clear current Site Map Clear(); // Create root node string folderUrl = HttpRuntime.AppDomainAppVirtualPath; string defaultUrl = folderUrl + “/Default.aspx”; _rootNode = new SiteMapNode(this, folderUrl, defaultUrl, “Home”); AddNode(_rootNode); // Create child nodes AddChildNodes(_rootNode); _dependencies.Add(HttpRuntime.AppDomainAppPath); // Add root node to cache with file dependencies CacheDependency fileDependency = new CacheDependency(_dependencies.ToArray()); context.Cache.Insert(“RootNode”, _rootNode, fileDependency); } return _rootNode; } } /// <summary> /// Add child folders and pages to the Site Map From the Library of Wow! eBook ptg 1108 CHAPTER 23 Using Site Maps /// </summary> private void AddChildNodes(SiteMapNode parentNode) { AddChildFolders(parentNode); AddChildPages(parentNode); } /// <summary> /// Add child folders to the Site Map /// </summary> /// <param name=”parentNode”></param> private void AddChildFolders(SiteMapNode parentNode) { HttpContext context = HttpContext.Current; string parentFolderPath = context.Server.MapPath(parentNode.Key); DirectoryInfo folderInfo = new DirectoryInfo(parentFolderPath); // Get sub folders DirectoryInfo[] folders = folderInfo.GetDirectories(); foreach (DirectoryInfo folder in folders) { if (!_excluded.Contains(folder.Name.ToLower())) { string folderUrl = parentNode.Key + “/” + folder.Name; SiteMapNode folderNode = new SiteMapNode(this, folderUrl, null, GetName(folder.Name)); AddNode(folderNode, parentNode); AddChildNodes(folderNode); _dependencies.Add(folder.FullName); } } } /// <summary> /// Add child pages to the Site Map /// </summary> private void AddChildPages(SiteMapNode parentNode) { HttpContext context = HttpContext.Current; string parentFolderPath = context.Server.MapPath(parentNode.Key); DirectoryInfo folderInfo = new DirectoryInfo(parentFolderPath); FileInfo[] pages = folderInfo.GetFiles(“*.aspx”); foreach (FileInfo page in pages) { From the Library of Wow! eBook ptg 1109 Creating Custom Site Map Providers 23 if (!_excluded.Contains(page.Name.ToLower())) { string pageUrl = parentNode.Key + “/” + page.Name; if (String.Compare(pageUrl, _rootNode.Url, true) !=0) { SiteMapNode pageNode = new SiteMapNode(this, pageUrl, pageUrl, ➥ GetName(page.Name)); AddNode(pageNode, parentNode); } } } } /// <summary> /// Fix the name of the page or folder /// by removing the extension and replacing /// underscores with spaces /// </summary> private string GetName(string name) { name = Path.GetFileNameWithoutExtension(name); return name.Replace(“_”, “ “); } } } Almost all the work in Listing 23.16 happens in the BuildSiteMap() method. This method recursively iterates through all the folders and pages in the current web application creat- ing 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 23.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, 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, 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 23.17 to enable the AutoSiteMapProvider. From the Library of Wow! eBook ptg 1110 CHAPTER 23 Using Site Maps FIGURE 23.8 Displaying an automatically generated Site Map. LISTING 23.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 23.17 configures the AutoSiteMapProvider as the applica- tion’s default provider. You can try out the AutoSiteMapProvider by requesting the Default.aspx page from the AutoSiteMapProviderApp Web application contained in the source code on the book’s website. This application does not include a Web.sitemap file. The Site Map is automati- cally generated from the structure of the website (see Figure 23.8). From the Library of Wow! eBook ptg 1111 Creating Custom Site Map Providers 23 Id ParentId Url Title Description 1 null Default.aspx Home Home Page 2 1 Products Products 3 2 Products/FirstProduct.aspx First Product First Product 4 2 Products/SecondProduct.aspx Second Product Second Product 6 1 Services Services 7 6 Services/FirstService.aspx First Service First Service Creating the SqlSiteMapProvider 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: 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 23.18. LISTING 23.18 App_Code\SqlSiteMapProvider.cs using System; using System.Collections.Specialized; using System.Web.Configuration; using System.Data; using System.Data.SqlClient; using System.Web; using System.Web.Caching; namespace AspNetUnleashed { /// <summary> /// Summary description for SqlSiteMapProvider /// </summary> public class SqlSiteMapProvider : StaticSiteMapProvider { private bool _isInitialized = false; From the Library of Wow! eBook ptg 1112 CHAPTER 23 Using Site Maps private string _connectionString; private SiteMapNode _rootNode; public override void Initialize(string name, NameValueCollection attributes) { if (_isInitialized) return; base.Initialize(name, attributes); string connectionStringName = attributes[“connectionStringName”]; if (String.IsNullOrEmpty(connectionStringName)) throw new Exception(“You must provide a connectionStringName ➥ attribute”); _connectionString = WebConfigurationManager.ConnectionStrings[connectionStringName]. ConnectionString; if (String.IsNullOrEmpty(_connectionString)) throw new Exception(“Could not find connection string “ + ➥ connectionStringName); _isInitialized = true; } protected override SiteMapNode GetRootNodeCore() { return BuildSiteMap(); } public override SiteMapNode BuildSiteMap() { // Only allow the Site Map to be created by a single thread lock (this) { // Attempt to get Root Node from Cache HttpContext context = HttpContext.Current; _rootNode = (SiteMapNode)context.Cache[“RootNode”]; if (_rootNode == null) { HttpContext.Current.Trace.Warn(“Loading from database”); // Clear current Site Map Clear(); From the Library of Wow! eBook ptg 1113 Creating Custom Site Map Providers 23 // Load the database data DataTable tblSiteMap = 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 SqlCacheDependency sqlDepend = new SqlCacheDependency(“SiteMapDB”, “SiteMap”); context.Cache.Insert(“RootNode”, _rootNode, sqlDepend); } return _rootNode; } } private DataTable GetSiteMapFromDB() { string selectCommand = “SELECT Id,ParentId,Url,Title,Description FROM ➥ SiteMap”; SqlDataAdapter dad = new SqlDataAdapter(selectCommand, ➥ _connectionString); DataTable tblSiteMap = new DataTable(); dad.Fill(tblSiteMap); return tblSiteMap; } private SiteMapNode GetRootNode(DataTable siteMapTable) { DataRow[] results = siteMapTable.Select(“ParentId IS NULL”); if (results.Length == 0) throw new Exception(“No root node in database”); DataRow rootRow = results[0]; return new SiteMapNode(this, rootRow[“Id”].ToString(), rootRow[“url”].ToString(), rootRow[“title”].ToString(), rootRow[“description”].ToString()); } private void BuildSiteMapRecurse(DataTable siteMapTable, SiteMapNode ➥ parentNode) { From the Library of Wow! eBook . Site Map Providers 23 Id ParentId Url Title Description 1 null Default.aspx Home Home Page 2 1 Products Products 3 2 Products/FirstProduct.aspx First Product First Product 4 2 Products/SecondProduct.aspx. System.Web.Caching; namespace AspNetUnleashed { public class AutoSiteMapProvider : StaticSiteMapProvider { private SiteMapNode _rootNode; private static List<string> _excluded = new List<string>(); private. System.Web.Caching; namespace AspNetUnleashed { /// <summary> /// Summary description for SqlSiteMapProvider /// </summary> public class SqlSiteMapProvider : StaticSiteMapProvider { private bool