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. Creating the AutoSiteMapProvider 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 providerthe XmlSiteMapProviderand 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. Listing 18.16. App_Code/AutoSiteMapProvider.vb [View full width] 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. Listing 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. Figure 18.8. Displaying an automatically generated Site Map. 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: 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. Listing 18.18. App_Code\SqlSiteMapProvider.vb [View full width] 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").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. Note 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. Listing 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). Figure 18.9. Displaying a Site Map from a Microsoft SQL database. |