I have been running into this problem and, frankly, not finding any solutions that made me happy... so I borrow ideas from here and there. My solution is multi-part: a) have the SiteMapProvider find the actual page handling the request and use it's node and b) use standard techniques for updating the sitemapnode from there.
A) The problem I kept running into is that if I didn't have the correct virtual path, the SiteMap.CurrentNode would be null and the fire the SiteMapResolve function. To solve this I subclassed XmlSiteMapProvider and overrode CurrentNode:
namespace WebFormTools
{
class RouteBaseSitemapProvider : XmlSiteMapProvider
{
public override SiteMapNode CurrentNode
{
get
{
var node = base.CurrentNode;
if (node == null)
{
// we don't have a node, see if this is from a route
var page = HttpContext.Current.CurrentHandler as System.Web.UI.Page;
if (page != null && page.RouteData != null)
{
// try and get the Virtual path associated with this route
var handler = page.RouteData.RouteHandler as PageRouteHandler;
if (handler != null) {
// try and find that path instead.
node = FindSiteMapNode(handler.VirtualPath);
}
}
}
return node;
}
}
}
}
Basically if the default implementation doesn't find anything, look up the route (if any) and try to find the node using the handler's virtual path.
For reference here is part of my Web.Config, Global.asax and SiteMap files:
Adding the provider
<siteMap defaultProvider="RouteBaseSitemapProvider">
<providers>
<add name="RouteBaseSitemapProvider" type="WebFormTools.RouteBaseSitemapProvider" siteMapFile="Web.sitemap" />
</providers>
</siteMap>
The route:
routes.MapPageRoute("EvalRoutes",
"Evals/{type}/New.aspx",
"~/Evals/New.aspx");
And the SiteMap:
<siteMapNode url="~/Evals/New.aspx" title="New Eval - {type}" description="" />
B) I subclass System.Web.UI.Page, aptly named BaseClass, which adds a method to register handlers for the SiteMapResolve event:
public System.Web.SiteMapNode Process(System.Web.SiteMapNode currentNode)
{
if (currentNode == null) return currentNode;
var page = HttpContext.Current.CurrentHandler as System.Web.UI.Page;
if (page != null && page.RouteData != null)
{
Dictionary<Regex, string> replacements = new Dictionary<Regex, string>();
// build a list of RegEx to aid in converstion, using RegEx so I can ignore class. Technically I could also
foreach (var key in page.RouteData.Values.Keys)
{
replacements.Add(new Regex(string.Format("\\{{{0}\\}}", key), RegexOptions.IgnoreCase), page.RouteData.Values[key].ToString());
}
// navigate up the nodes
var activeNode = currentNode;
while (activeNode != null)
{
// to the replacements
foreach(var replacement in replacements)
{
activeNode.Title = replacement.Key.Replace(activeNode.Title, replacement.Value);
}
activeNode = activeNode.ParentNode;
}
}
return currentNode;
}
I still need to have the URLs map appropriately (the would use the URL of the page receiving the route), which is sans routing information. I will probably use a custom Attribute in the sitemap to tell the node how to render the URL.