Dynamically changing the title of a SiteMapNode
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开发者_StackOverflow中文版 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?
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.
You're pretty close with attempt #2 - you just also need to override GetParentNode and FindSiteMapNode as well.
Thanks to Mark and Rex for the suggestions. What I ended up doing was leaving the sitemap provider alone and just fixing up the one node in the master page, thus:
Protected Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Init
' Do this as early as possible in the page lifecycle so that it happens before any
' automatic data binding or control initialisation is done.
Dim node As SiteMapNode = GetNodeToEdit()
Dim nodeReadOnly As Boolean = node.ReadOnly
node.ReadOnly = False
node.Title = GetNodeTitle()
node.ReadOnly = nodeReadOnly
End Sub
However, I have accepted Mark's answer, because that's how I will do it if it turns out that more extensive modifications need to be done in future.
Thanks Christian for your research. Using your results, I've come up with the following that may help others:
' Dynamically update some menu items.
'
' Since the correct Medical Center ID is not known until runtime, need to
' append "&MEDICAL_CENTER_ID=xxx" to all of the report's URLs in Web.sitemap.
Public Shared Sub UpdateMenu(ByVal MedicalCenterID As String)
Dim CurrentNodeTitle As String = ""
Dim NodeReadOnlyProperty As Boolean
' Home menu item
CurrentNodeTitle = SiteMap.CurrentNode.Title
' Determines if the current node has child nodes.
If (SiteMap.CurrentNode.HasChildNodes) Then
' Loop through top level 1 menu items (looking for Reports)
For Each ChildNodesEnumerator1 As SiteMapNode In SiteMap.CurrentNode.ChildNodes
CurrentNodeTitle = ChildNodesEnumerator1.Title
If CurrentNodeTitle = "Reports" Then
' Loop through level 2 menu items (looking for specfic reports)
For Each ChildNodesEnumerator2 As SiteMapNode In ChildNodesEnumerator1.ChildNodes
CurrentNodeTitle = ChildNodesEnumerator2.Title
If CurrentNodeTitle = "Multi-Day Vehicle Requests" Or _
CurrentNodeTitle = "XXXXXXXXXXXXXXXXX" Then
' First check if the URL has not been modified already
If InStr(ChildNodesEnumerator2.Url, "MEDICAL_CENTER_ID") = 0 Then
NodeReadOnlyProperty = ChildNodesEnumerator2.ReadOnly
ChildNodesEnumerator2.ReadOnly = False
ChildNodesEnumerator2.Url = ChildNodesEnumerator2.Url & "&MEDICAL_CENTER_ID=" & MedicalCenterID
ChildNodesEnumerator2.ReadOnly = NodeReadOnlyProperty
End If
End If
Next
End If
Next
End If
End Sub
精彩评论