Dynamically changing the title of a SiteMapNode

.net, asp.net, sitemap, sitemapprovider, vb.net

Solution

In our Custom SiteMapProvider we override the BuildSiteMap Method and construct the SiteMapNodes manually. To change and/or add custom properties we add custom attributes to the SiteMapNodes by create a NameValueCollection and add pass this to the SiteMapNode Constructor.

Problem

We have a website that uses a bog-standard default sitemap with security trimming as follows: ``` <siteMap defaultProvider="default" enabled="true"> <providers> <add siteMapFile="~/Web.sitemap" securityTrimmingEnabled="true" name="default" type="System.Web.XmlSiteMapProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" /> </providers> </siteMap> ``` All very well, but a request has come in to change the `Title` of one node based on some back-end criteria. Sounds like a simple thing, but apparently not. Attempt 1 - Handling the `SiteMapResolve` event. It doesn't appear to matter where this event is handled, I have shown it in `Global.asax` merely because that was one of the places I tried it and it worked. ``` Public Class Global_asax Inherits System.Web.HttpApplication Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs) AddHandler SiteMap.SiteMapResolve, AddressOf SiteMapResolve End Sub Sub Application_EndRequest(ByVal sender As Object, ByVal e As EventArgs) RemoveHandler SiteMap.SiteMapResolve, AddressOf SiteMapResolve End Sub Private Shared Function SiteMapResolve(ByVal sender As Object, ByVal e As SiteMapResolveEventArgs) As SiteMapNode Dim node As SiteMapNode = SiteMap.CurrentNode If IsThisTheNodeToChange(node) Then node = node.Clone() node.Title = GetNodeTitle() End If Return node End Function End Class ``` This worked fine when the relevant page was navigated to, but unfortunately part of the site navigation involves a combo box that is data-bound to the site map like this: ``` <asp:SiteMapDataSource ID="siteMapDataSource" runat="Server" ShowStartingNode="false" StartFromCurrentNode="false" StartingNodeOffset="1" /> <asp:DropDownList ID="pageMenu" runat="Server" AutoPostBack="True" DataSourceID="siteMapDataSource" DataTextField="Title" DataValueField="Url" /> ``` When this menu is rendered, the `SiteMapResolve` event does not fire for any of the contents because the current node is the page on which the menu is defined. As a result, the menu shows the nonsense placeholder title from the physical sitemap file rather than the correct title. Attempt 2 - Writing my own sitemap provider. I didn't want to duplicate all the default behaviour, so I tried deriving from the default provider as follows. ``` Public Class DynamicXmlSiteMapProvider Inherits XmlSiteMapProvider Private _dataFixedUp As Boolean = False Public Overrides Function GetChildNodes(ByVal node As SiteMapNode) As SiteMapNodeCollection Dim result As SiteMapNodeCollection = MyBase.GetChildNodes(node) If Not _dataFixedUp Then For Each childNode As SiteMapNode In result FixUpNode(childNode) Next End If Return result End Function Private Sub FixUpNode(ByVal node As SiteMapNode) If IsThisTheNodeToChange(node) Then node.ReadOnly = False node.Title = GetNodeTitle() node.ReadOnly = True _dataFixedUp = True End If End Sub End Class ``` This doesn't work because `GetChildNodes` doesn't appear to be called very often when navigating around the site. Attempt 3 - Try to fix the data immediately after it's loaded into memory, rather than when it's accessed. ``` Public Class DynamicXmlSiteMapProvider Inherits XmlSiteMapProvider Private _dataFixInProgress As Boolean = False Private _dataFixDone As Boolean = False Public Overrides Function BuildSiteMap() As SiteMapNode Dim result As SiteMapNode = MyBase.BuildSiteMap() If Not _dataFixInProgress AndAlso Not _dataFixDone Then _dataFixInProgress = True For Each childNode As SiteMapNode In result.GetAllNodes() FixUpNode(childNode) Next _dataFixInProgress = False _dataFixDone = True End If Return result End Function Private Sub FixUpNode(ByVal node As SiteMapNode) If IsThisTheNodeToChange(node) Then node.ReadOnly = False node.Title = GetNodeTitle() node.ReadOnly = True End If End Sub End Class ``` This appears to work. However, I'm worried about the call to `GetAllNodes` in the `BuildSiteMap` method. It just seems wrong to me to recursively pull all data into memory just to fix up one value. Also, I have no control over when `BuildSiteMap` is called. I would prefer something more like Attempt 1, that is called on demand when the node data is first required. Attempt 4 (NEW) - Like Attempt 2, but overriding all virtual members that are to do with reading data (`CurrentNode`, `FindSiteMapNode`, `FindSiteMapNodeFromKey`, `GetChildNodes`, `GetCurrentNodeAndHintAncestorNodes`, `GetCurrentNodeAndHintNeighborhoodNodes`, `GetParentNode`, `GetParentNodeRelativeToCurrentNodeAndHintDownFromParent`, `GetParentNodeRelativeToNodeAndHintDownFromParent`, `HintAncestorNodes`, `HintNeighborhoodNodes`), to try to intercept the reading of the dynamic node somewhere. This did not work. I put debug statements in all the overridden members, and it seems that none of them at all are called when data binding to the dropdown list. The only explanation I can think of is that the nodes are all read into memory in one go during the `BuildSiteMap` call, so that the `SiteMapNode` is not hitting the provider class when enumerating child nodes. Does anyone have any better suggestions?

Original source