Practical ASP.NET
Drive Your Menus from a Database Table
If you'd rather keep your menu structure in a table in your database instead of a file in your Web site, here's all the code you need to implement a database-driven menuing system.
One of the questions that I frequently get from people attending my ASP.NET course is "How can I drive my menus from a table in the database instead of from a file in my Website?" (Gratuitous plug: that's
course 512 from Learning Tree International). This column provides a problem to that question.
In my last column (Managing Menus with a Custom Menu Provider), I briefly described the ASP.NET provider architecture and how to create your own class to manage the connection between the Web.sitemap and your site's menus. As I also said in that column, creating a provider is easy to do. In fact, you'll probably spend more time creating the page that updates your menu table than you will spend writing your SiteMapProvider.
The first step is to add a class to your App_Code folder and have it inherit from the the StaticSiteMapProvider (the StaticSiteMapProvide includes implementations of most of the functionality required by a SiteMapProvider):
Public Class PHVDBSiteMap
Inherits StaticSiteMapProvider
End Class
The next step is to override the class's Initialize method to extract the connection string to the database that contains your menu table. My Initialize method looks like this:
Private _connectionStringName As String
Public Overrides Sub Initialize(ByVal name As String, _
ByVal attributes As System.Collections.Specialized.NameValueCollection)
MyBase.Initialize(name, attributes)
_connectionStringName = attributes("connectionStringName")
End Sub
The attributes collection that's passed to the Initialize method contains any custom attributes that you've added to the SiteMap's configuration in your site's web.config file. To use this SiteMapProvider provider, then, you'd need this in the site's config file:
<siteMap defaultProvider="CustomDBSiteMap">
<providers>
<add name="CustomDBSiteMap"
type="PHVDBSiteMap"
connectionStringName="MyConnection"/>
</providers>
</siteMap>
The only other method in your SiteMapProvider that you need to put any real code in is the BuildSiteMap method. In that method, you should first call the base Clear method to eliminate any changes made to the SiteMap in the base Initialize method that you called in your Initialize method. It's also critical that you don't touch the RootNode or the ChildNodes collection used in the SiteMap -- that will trigger a call to the BuildSiteMap method and lead to a stack overflow error.
Since the BuildSiteMap method can be called from multiple pages simultaneously, I put the code inside a SyncLock to make sure the code isn't being simultaneously executed by multiple pages (I may be worrying unnecessarily here). Also, once you've built the SiteMapNode collection, the SiteMapProvider hangs onto it -- you don't need to build it again. The simplest way if you actually need to build the SiteMap is to check whether or not the root item for your menu is created.
If you do need to build the SiteMap, you should create the root node for your SiteMapNode and use the Base AddNode method to append the node to the SiteMap. The collection has to return that root node so the initial form of the method looks like this:
Dim smnRoot As SiteMapNode
Dim lockObject As Object = New Object
Public Overrides Function BuildSiteMap() As System.Web.SiteMapNode
Dim smnItem As SiteMapNode
Dim smnMenuHeader As SiteMapNode
SyncLock lockObject
If smnRoot Is Nothing Then
MyBase.Clear()
smnRoot = New SiteMapNode(Me, "Home", "~/Default.aspx", "Home")
End If
End SyncLock
Return smnRoot
End Function
While I've hard-coded values for the menu's name, title and URL, you'd probably read them out of your database table. As you read additional menu items from the database you use the AddNode collection to add new items to your root item or, to create a submenu, to some already added node. This example adds another menu item to the root item:
smnMenuHeader = New SiteMapNode(Me, "Customers", Nothing, "Customers")
AddNode(smnMenuHeader, smnRoot)
You must also implement the GetRootNodeCode method, but it just needs to return your root node:
Protected Overrides Function GetRootNodeCore() _
As System.Web.SiteMapNode
Return smnRoot
End Function
But, if you update the data in your menu table, how do you get the site map to rebuild itself? The best solution is to keep variables in the Site's Application object. Your provider should check that variable every time the BuildSiteMap method is called and build the site map depending on its value. This allows you to set up your menuing table in advance of adding some pages to your site and then, when the pages are finally added, trigger the new menu by setting the Application variable.
I had thought, by the way, that I was done with menus with this topic. However, I've had another question come up, so my next column will tackle one final menu issue: How to add a querystring to a menu item that includes content from the current page. Then I'm done. I promise.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.