Building a Custom SiteMap Provider (PART I)
Earlier I posted an article about creating a custom sitemap provider, loosely based on Jeff Procise's Wicked Code Article and a post on the main MSDN site. I really wanted to make that a much longer post and talk about some of the things I discovered in the experience with the sitemap. First, the new ASP.NET 2.0 menu control is pretty good, and fairly easy to work with. The concept of a site map provider is good too. Unfortunately the provider has some flaws in the design that will ever keep it from being used on any of our production sites.
The site map provider has the limitation that a URL can only be entered into the site map once. This is going to eliminate it from many of our client's web sites because they like to have a page in the site navigation multiple times, especially as a site grows. So this alone forced me to create a customized system to display the navigation.
In the previous article, I offered a sitemap table structure. I will include an updated version and the corresponding stored procedures in a file attachment. I will not cover that in this article, but basically the table provides a definition we will use for each node.
I have really come to love Master Pages and generally will place a menu control on the master page now. I have decided its ID will be MainMenu, which is important for reference issues. I also like to define base classes to inherit my pages from, I am doing the same thing with my Master Page structure too. So I have created an ExMasterPage class which Inherits from System.Web.UI.MasterPage. I have defined a few items to help with building my menu, see code insert.
Finally I am leveraging the Cache Application Block (Patterns and Practices) to store my menu in cache. I will cover that in another entry.
Protected ReadOnly Property MasterMenu() As Menu
If Not IsNothing(Me.FindControl("MainMenu")) Then
Return CType(Me.FindControl("MainMenu"), Menu)
Protected Sub BindMenu(ByVal _Menu As Menu, Optional ByVal mt As MenuType = MenuType.Main)
If IsNothing(MasterMenu) Then
Dim sm As SiteMapCollection = DirectCast(Me.primitivesCache.GetData("SiteMap"), SiteMapCollection)
If sm Is Nothing Then
Dim smc As New SiteMapController
sm = smc.BuildSiteMap
primitivesCache.Add("SiteMap", sm, CacheItemPriority.Normal, New ExCacheRefreshAction, _
New AbsoluteTime(DateAdd(DateInterval.Second, 15, Now())))
For Each smi As SiteMapInfo In sm
If smi.IsVisible = True Then
Dim mi As New MenuItem(smi.Title, smi.Title, smi.Image, smi.Url)
For Each smci As SiteMapInfo In smi.ChildNodes
If smci.IsVisible = True Then
mi.ChildItems.Add(New MenuItem(smci.Title, smci.Title, smci.Image, smci.Url))
You will notice a Read Only Property, MasterMenu, which is a Menu Control. This uses reflection to find the Menu Control of the master page. We can then use it to bind the MenuItem that will comprise the menu we need for a user. This is why I defined the Menu Control ID to 'MainMenu', it makes for a consistant reference point.
Next I defined the sub routine (void for you C# folks) that will actualy build our menu for us. The first thing I do is check to see if we have a menu to use, if not then I exit the sub because we do not have anything else to perform.
If you have been following my entries you will know that I have a basic Business Logic structure, with a Controller class that interacts with the database and a data item class that will hold the values. Our menu system is no different, I have added the BuildSiteMap method to build a collection of SiteMapInfo classes. The SiteMapCollection Class inherits from CollectionBase, so we get all those great features. I will probably discuss the collection classes and generics in another post. The SiteMapInfo Class also has a ChildNodes collection, which is a SiteMapCollection of the nodes that are children of the node.
The Info class has a BuildChildNodes method, that allows for a bit of recursion for our main loop that builds the menu for us. Back to our ExMasterPage class, the BindMenu method is a little overloaded, because it lets us define what menu we are binding, the default is the Main menu. For this pass I am not going to do anything with that, but in a later version (I am thinking ahead already). I call the BindMenu method from the page.load event handler, this ensures it gets called each time the page is loaded the first time.
The BindMenu method consists of two important loops, For Each smi As SiteMapInfo In sm and then another loop to build the child nodes. For this version I am only digging one level deep and will break this into a recursive function in the next version. These loops build the actual menu nodes that are then added to the menu control. Notice I check to see if the Menu node is visible before I add it to the list. This allows us to have more pages that will actually be displayed in the menu. Once this is done, we will have our main menu.
Back to the collection and the SiteMapInfo class. You might think, why not just use the menu node to define a SiteMapInfo class? Well, there are not enough properties to a MenuItem to define the item I want for my sites. It might be exactly what you want to have for your core web site, and if so just use that. I suppose I could inherit the SiteMapInfo class from the MenuItem class, but I think this will prove easier to maintain and extend for now.
So let me recap, we have defined a base for our site map definition in the datbase with our SQL script. We have build a useful Business Layer with our SiteMapController, SiteMapInfo and SiteMapCollection classes to define our menus and sitemaps for our web sites. Finally, we build a base page for our Master Pages and defined a naming standard for our main menu. All of this give us the base for a strong SiteMap Navigation system.
In next part we will examine how to apply security roles to our navigation.