Building an ASP.NET MVC sitemap provider with security trimming

Edit on GitHub

Warning!
Warning!
A new version of the source code provided in this post is available here. Use this blog post as reference only. 


Yes, it has been a while since my last post. A nice vacation to Austria, some work to catch up, ... All excuses, I know, but I'll make it up to you with a huge blog post!

If you have been using the ASP.NET MVC framework, you possibly have been searching for something like the classic ASP.NET sitemap. After you've played with it, you even found it useful! But not really flexible and easy to map to routes and controllers. Sounds familiar? Continue reading! Doesn't ring a bell? Well, continue reading, please!

Feel free to download the sample code.
UPDATE: A version for preview 5 can also be downloaded: MvcSitemapProvider.cs (19.46 kb)

The base concept of this class is based on someone else's version which supports dynamic nodes, populated by code instead of XML. Unfortunately, I forgot to write down the URL where I found it. So please, if you do find something like that, let me know so I can thank this person for the base concepts of his class...

kick it on DotNetKicks.com

The concept

What I would like, is having a web.sitemap file which looks like the following:

[code:c#]

<?xml version="1.0" encoding="utf-8" ?>
<siteMap>
  <siteMapNode id="Root" url="~/Index.aspx">
    <mvcSiteMapNode id="Home" title="Home" controller="Home" action="Index">
      <mvcSiteMapNode id="About" title="About Us" controller="Home" action="About" />
    </mvcSiteMapNode>

    <mvcSiteMapNode id="Products" title="Products" controller="Products">
      <mvcSiteMapNode id="Books" title="Books" controller="Products" action="List" category="Books" />
      <mvcSiteMapNode id="DVD" title="DVD's" controller="Products" action="List" category="DVD"/>
    </mvcSiteMapNode>
    <mvcSiteMapNode id="Account" title="Account" controller="Account">
      <mvcSiteMapNode id="Login" title="Login" controller="Account" action="Login" />
      <mvcSiteMapNode id="Register" title="Account Creation" controller="Account" action="Register" />
      <mvcSiteMapNode id="ChangePassword" title="Change Password" controller="Account" action="ChangePassword" />
      <mvcSiteMapNode id="Logout" title="Logout" controller="Account" action="Logout" />
    </mvcSiteMapNode>
  </siteMapNode>
</siteMap>

[/code]

That's right: regular siteMapNodes, but also mvcSiteMapNodes! I want my ASP.NET menu control and sitemap path to use both node types for determining the current locattion on my website. And since the ASP.NET MVC framework uses routing and allows extra parameters to build up a URL, I thought of creating an mvcSiteMapNode.

Each mvcSiteMapNode is structured like this:

idId for the current node. Can only occur once!
titleThe title to show in menu's.
descriptionOptional description.
controllerThe controller to map this node to. Will default to "Home" if it is not specified.
actionThe action on that controller to map this node to. Will default to "Index" if it is not specified.
*Well, any other attribute will be used as route data values. For example, if you add "category='Books'", it will correpond with new { category = "Books" } in your route definitions.
paramidWell, this one maps to new { id = ... }, since I already used id before...

Implementing it

Two options for this one... Option one would be extending the existing XmlSiteMapProvider class, but that seemed like a no-go because... well... I wanted to take the hard way :-) Option two it is! And that's extending StaticSiteMapProvider.

This MvcSiteMapProvider class will have to do some things:

  • Read the web.config settings
  • Cache my sitemap nodes for a specified amount of time
  • Do some mapping of the current HttpContext (which is not IHttpContext, unfortunately...) to the current route
  • Security trimming! The provider should check my controllers for AuthorizeAttribute and follow the directions of that attribute.

If you want to check the full source code, feel free to download it. I'll not go trough it completely in this blog post, but just pick some interesting parts.

MvcSiteMapNode

First things first! If I want to use a custom sitemap node, I'll have to create one! Here's my overloaded version of the SiteMapNode class which now also contains a Controller and Action property:

[code:c#]

/// <summary>
/// MvcSiteMapNode
/// </summary>
public class MvcSiteMapNode : SiteMapNode
{

    #region Properties

    public string Id { get; set; }
    public string Controller { get; set; }
    public string Action { get; set; }

    #endregion

    #region Constructor

    /// <summary>
    /// Creates a new MvcSiteMapNode instance
    /// </summary>
    public MvcSiteMapNode(SiteMapProvider provider, string key)
        : base(provider, key)
    {
        Id = key;
    }

    #endregion

}

[/code]

Reading the mvcSiteMapNode XML

That's actually a nice one! Here's the full snippet:

[code:c#]

/// <summary>
/// Maps an XMLElement from the XML file to a SiteMapNode.
/// </summary>
/// <param name="node">The element to map.</param>
/// <returns>A SiteMapNode which represents the XMLElement.</returns>
protected SiteMapNode GetMvcSiteMapNodeFromXMLElement(XElement node)
{
    // Get the ID attribute, need this so we can get the key.
    string id = GetAttributeValue(node.Attribute("id"));

    // Create a new sitemapnode, setting the key and url
    var smNode = new MvcSiteMapNode(this, id);

    // Create a route data dictionary
    IDictionary<string, object> routeValues = new Dictionary<string, object>();

    // Add each attribute to our attributes collection on the sitemapnode
    // and to a route data dictionary.
    foreach (XAttribute attribute in node.Attributes())
    {
        string attributeName = attribute.Name.ToString();
        string attributeValue = attribute.Value;

        smNode[attributeName] = attributeValue;

        if (attributeName != "title" && attributeName != "description"
            && attributeName != "resourceKey" && attributeName != "id"
            && attributeName != "paramid")
        {
            routeValues.Add(attributeName, attributeValue);
        }
        else if (attributeName == "paramid")

        {
            routeValues.Add("id", attributeValue);
        }
    }

    // Set the other properties on the sitemapnode,
    // these are for title and description, these come
    // from the nodes attrbutes are we populated all attributes
    // from the xml to the node.
    smNode.Title = smNode["title"];
    smNode.Description = smNode["description"];
    smNode.ResourceKey = smNode["resourceKey"];
    smNode.Controller = smNode["controller"];
    smNode.Action = smNode["action"] ?? "Index";

    // Verify route values
    if (!routeValues.ContainsKey("controller")) routeValues.Add("controller", "Home");
    if (!routeValues.ContainsKey("action")) routeValues.Add("action", "Index");

    // Build URL
    MvcHandler handler = HttpContext.Current.Handler as MvcHandler;
    RouteData routeData = handler.RequestContext.RouteData;

    smNode.Url = "~/" + routeData.Route.GetVirtualPath(handler.RequestContext, new RouteValueDictionary(routeValues)).VirtualPath;

    return smNode;
}

[/code]

Interesting part to note are the last 4 lines of code. I'm using the application's route data to map controller, action and values to a virtual path, which will be used by all sitemap controls to link to a URL. Coolness! If I change my routes in Global.asax.cs, my menu will automatically be updated without having to change my web.sitemap file.

Security trimming

Some more code. I told you it would be a long post!

[code:c#]

/// <summary>
/// Determine if a node is accessible for a user
/// </summary>
/// <param name="context">Current HttpContext</param>
/// <param name="node">Sitemap node</param>
/// <returns>True/false if the node is accessible</returns>
public override bool IsAccessibleToUser(HttpContext context, SiteMapNode node)
{
    // Is security trimming enabled?
    if (!this.SecurityTrimmingEnabled)
        return true;

    // Is it a regular node? No need for more things to do!
    MvcSiteMapNode mvcNode = node as MvcSiteMapNode;
    if (mvcNode == null)
        return base.IsAccessibleToUser(context, node);

    // Find current handler
    MvcHandler handler = context.Handler as MvcHandler;

    if (handler != null)
    {
        // It's an MvcSiteMapNode, try to figure out the controller class
        IController controller = ControllerBuilder.Current.GetControllerFactory().CreateController(handler.RequestContext, mvcNode.Controller);

        // Find all AuthorizeAttributes on the controller class and action method
        object[] controllerAttributes = controller.GetType().GetCustomAttributes(typeof(AuthorizeAttribute), true);
        object[] actionAttributes = controller.GetType().GetMethod(mvcNode.Action).GetCustomAttributes(typeof(AuthorizeAttribute), true);

        // Attributes found?
        if (controllerAttributes.Length == 0 && actionAttributes.Length == 0)
            return true;

        // Find out current principal
        IPrincipal principal = handler.RequestContext.HttpContext.User;

        // Find out configuration
        string roles = "";
        string users = "";
        if (controllerAttributes.Length > 0)
        {
            AuthorizeAttribute attribute = controllerAttributes[0] as AuthorizeAttribute;
            roles += attribute.Roles;
            users += attribute.Users;
        }
        if (actionAttributes.Length > 0)
        {
            AuthorizeAttribute attribute = actionAttributes[0] as AuthorizeAttribute;
            roles += attribute.Roles;
            users += attribute.Users;
         }

        // Still need security trimming?
        if (string.IsNullOrEmpty(roles) && string.IsNullOrEmpty(users) && principal.Identity.IsAuthenticated)
            return true;

            // Determine if the current user is allowed to access the current node
            string[] roleArray = roles.Split(',');
            string[] usersArray = users.Split(',');
            foreach (string role in roleArray)
            {
                if (role != "*" && !principal.IsInRole(role)) return false;
            }
            foreach (string user in usersArray)
            {
                if (user != "*" && (principal.Identity.Name == "" || principal.Identity.Name != user)) return false;
            }

            return true;
    }

    return false;
}

[/code]

Now read it again, it might be a bit confusing. What actually happens, is the following:

  1. Security trimming is not enabled? Well duh! Of cource you can access this node!
  2. If the curent node that a menu control or something similar tries to render is a regular sitemap node, simply use the base class to verify security
  3. If it is an MvcSiteMapNode that we're accessing, do some work...
    1. Find out the controller and action method that's being called
    2. Check for security attributes on the controller
    3. Check for security attributes on the action method
    4. Verify if the current IPrincipal complies with all previous stuff
  4. No access granted in the past few lines of code? return false!

I can now actually hide a sitemap node from unauthorized users by simply adding the [Authorize(...)] attribute to a controller action!

Using it

Feel free to download the sample code or check the live demo. It has been configured to use my custom sitemap provider by adding the following in web.config:

[code:xml]

<system.web>
    <!-- ... -->
    <siteMap defaultProvider="MvcSitemapProvider">
        <providers>
            <add name="MvcSitemapProvider"
                 type="MvcSitemapProviderDemo.Core.MvcSitemapProvider"
                 siteMapFile="~/Web.sitemap" securityTrimmingEnabled="true"
                 cacheDuration="10" />
        </providers>
    </siteMap>
    <!-- ... -->
</system.web>

[/code]

In short: I've told ASP.NET to use my sitemap provider in favor of the standard sitemap provider. Don't you just love this provider model!

Known issues

  • The root node should always link to url "~/Index.aspx"
  • A controller + action + values combination can only occur once (but that's the case with regular sitemaps too)

Note: based on ASP.NET MVC preview 4 - A version for preview 5 can also be downloaded: MvcSiteMapProvider.cs (19.90 kb)

kick it on DotNetKicks.com 

This is an imported post. It was imported from my old blog using an automated tool and may contain formatting errors and/or broken images.

Leave a Comment

avatar

59 responses

  1. Avatar for pp
    pp August 30th, 2008

    Its great - that is what i looking for

  2. Avatar for Celik
    Celik September 7th, 2008

    This is what i was looking for, thank you

  3. Avatar for tgmdbm
    tgmdbm September 9th, 2008

    MvcSitemapProvider.cs:line 239-250

    What if there is an AuthoriseAttribute on both the class and the method? Instead of Admin,SuperUsers it would be AdminSuperUsers.

    Also, the logic is incorrect. Lets say there's an [Authorize( Roles = "Admin" )] on the classand a [Authorize( Roles = "SuperUsers" )] on the method, You would only be allowed to call that action on that controller if you were in both roles. Whereas the logic you have assumes you are allowed if you are in either role.

    Otherwise, excellent work.

  4. Avatar for maartenba
    maartenba September 9th, 2008

    That's a good suggestion... Will fix!

  5. Avatar for Matt Bertulli
    Matt Bertulli September 10th, 2008

    Hey Maarten,

    First, great work on this one. It's a straight forward example of implementing a sitemap provider.

    Second, I just wanted to let you know that I have encountered a bit of a problem in using your code with Preview 5, and more specifically the AcceptAction attribute on action methods. When the MvcSiteMapProvider gets to around line 227 and tries this:

    object[] actionAttributes = controller.GetType().GetMethod(mvcNode.Action).GetCustomAttributes(typeof(AuthorizeAttribute), true);

    It borks with the following message: "Ambiguous match found." I'm not familiar enough with mvc yet to fix this, but I am thinking it's becuase I have two actions of the same name in my controller class, each with a different signature and tagged with different AcceptVerbs attributes.

  6. Avatar for maartenba
    maartenba September 10th, 2008

    I've posted an updated version for preview 5.

  7. Avatar for dkarantonis
    dkarantonis September 19th, 2008

    Hi,
    very nice article indeed.

    I have two questions.
    First of all, how could you implement multilanguage support on your site map provider? For a site that uses ..\<Language>-<Locale>\... for url routing, how could this be reflected on the site map?
    Finally, how could you create a sitemap for posting it on Google, Yahoo, etc... search engines?

    thanks in advance

  8. Avatar for Matt Bertulli
    Matt Bertulli September 20th, 2008

    Is anybody else hitting the following "Object reference not set to an instance of an object." exception scenario?

    Line 438: smNode.Url = "/" + routeData.Route.GetVirtualPath(handler.RequestContext, new RouteValueDictionary(routeValues)).VirtualPath;

    - When the application cycles (write to web.config / iis restart / cache expiry)
    - Only happens when I'm NOT in a /Home/{action}/ or /Account/Index/ nodes

    My Sitemap looks like this:

    <siteMap>
    <siteMapNode id="Root" url="~/Index.aspx">
    <mvcSiteMapNode id="Home" title="Home" controller="Home" action="Index">
    <mvcSiteMapNode id="About" title="About Us" controller="Home" action="About" />
    <mvcSiteMapNode id="Contact" title="Contact Us" controller="Home" action="Contact" />
    <mvcSiteMapNode id="GetStarted" title="Getting Started" controller="Home" action="GetStarted" />
    <mvcSiteMapNode id="Support" title="Need Help?" controller="Support" action="Index" />
    </mvcSiteMapNode>

    <mvcSiteMapNode id="Account" title="Your Dashboard" controller="Account" action="Index">
    <mvcSiteMapNode id="Purchases" title="Full Purchase History" controller="Account" action="Purchases">
    <mvcSiteMapNode id="PurchaseDetail" title="Purchase Detail" controller="Account" action="PurchaseDetail" />
    </mvcSiteMapNode>
    <mvcSiteMapNode id="Products" title="Full Product History" controller="Account" action="Products">
    <mvcSiteMapNode id="ProductDetail" title="Product Detail" controller="Account" action="ProductDetail" />
    </mvcSiteMapNode>
    <mvcSiteMapNode id="Alerts" title="Manage E-mail Alerts" controller="Account" action="Alerts">
    <mvcSiteMapNode id="AlertDetail" title="Alert Detail" controller="Account" action="AlertDetail" />
    <mvcSiteMapNode id="NewAlert" title="New Alert" controller="Account" action="NewAlert" />
    </mvcSiteMapNode>
    <mvcSiteMapNode id="Profile" title="Your Profile" controller="User" action="Profile">
    <mvcSiteMapNode id="ChangePassword" title="Change Password" controller="User" action="ChangePassword" />
    </mvcSiteMapNode>
    </mvcSiteMapNode>
    </siteMapNode>
    </siteMap>

  9. Avatar for maartenba
    maartenba September 22nd, 2008

    @dkarantonis: You can simply place a route in global.asax and add the default values in the sitemap file?

    @matt: will send you an updated version by email, it'll be posted on my blog later

  10. Avatar for dkarantonis
    dkarantonis September 22nd, 2008

    [quote]@dkarantonis: You can simply place a route in global.asax and add the default values in the sitemap file?[/quote]

    Hi,
    what do you mean by adding the default values in the sitemap file? Could you provide some more details?

    thanks again!

  11. Avatar for maartenba
    maartenba September 22nd, 2008

    Example:
    <mvcSiteMapNode id="SomeIdForTheNode" title="Home" controller="Home" action="Index" [b]language="en" locale="US"[/b] />

  12. Avatar for robert
    robert September 23rd, 2008

    Maarten,

    This is very cool.

    I'm trying to extend your idea into a "Security Trimmed" ActionLink extension method, but I'm running into trouble with the MvcHandler:
    I posted a question on the Asp.net forums (http://forums.asp.net/t/132... - I guess I should have started here.
    Anyway, your comments/ideas are welcome and thanks for creating such a useful implementation of the SiteMap.

    -Rob

  13. Avatar for maartenba
    maartenba September 24th, 2008

    Hello Robert,

    Make sure to use the latest code (http://blog.maartenballiauw..., this issue has been resolved in there.

    Regards,
    Maarten

  14. Avatar for robert
    robert September 24th, 2008

    Maarten,

    Thanks for the quick response. I did update the code, but I still get a null MvcHandler each time. Not sure how to instantiate it properly I guess.

  15. Avatar for maartenba
    maartenba September 24th, 2008

    Try fetching the principal via HttpContext.Current.User.Identity

  16. Avatar for Rei
    Rei October 2nd, 2008

    Firstly many thanks for a great component!!!

    One thing to note about the provider is that the method [b]IsAccessibleToUser[/b] creates an instance of a controller at:
    [quote]
    IController controller = ControllerBuilder.Current.GetControllerFactory().CreateController(handler.RequestContext, mvcNode.Controller);
    [/quote]

    Now this is all nice and dandy, but if controller creation is expensive (ie: resolved from an ioc container) then you might create a small bottleneck at this point.

    I tried a simple solution which i'm confortable with:

    [quote]

    private readonly object _controllerCache = new object();
    private readonly Hashtable _controllers = new Hashtable();

    private IController GetConntroller(RequestContext context, string controllerName) {
    lock (_controllerCache) {
    IController controller;

    if (!_controllers.ContainsKey(controllerName)) {
    controller = ControllerBuilder.Current.GetControllerFactory().CreateController(context, controllerName);
    _controllers.Add(controllerName, controller);
    } else
    controller = (IController)_controllers[controllerName];

    return controller;
    }
    }

    and modified the the [b]IsAccessibleToUser[/b] method as follows:

    IController controller = GetConntroller(handler.RequestContext, mvcNode.Controller);;

    [/quote]

    Hope this helps anyone.

  17. Avatar for Alvin
    Alvin October 5th, 2008

    Hi Maarten,

    This is great work. Much appreciated.

    -Alvin

  18. Avatar for Jason Plante
    Jason Plante October 21st, 2008

    I am experiencing the same issue that Matt Bertulli brought up.

    Line 438: smNode.Url = "/" + routeData.Route.GetVirtualPath(handler.RequestContext, new RouteValueDictionary(routeValues)).VirtualPath;

    I've tried looking through the source code myself to see if I can fix it or at least find the culprit. From what I can see, after some kind of application cycles (write to web.config / iis restart / cache expiry) requests outside the route generate this error. The root of the error seems to come from this line of code inside of the GetMvcSiteMapNodeFromXMLElement(XElement node) method:

    // slightly modified (broken apart)
    VirtualPathData virtualPathData = routeBase.GetVirtualPath(requestContext, routeValueDictionary);
    string virtualPath = virtualPathData.VirtualPath;

    ...because the return value of GetVirtualPath is null, so the object virtualPathData is a null reference. So something in the routeBase, requestContext or routeValueDictionary must be off. Any help would be very much appreciated. This has been a very very useful framework thus far.

  19. Avatar for maartenba
    maartenba October 21st, 2008

    Have you tried the latest version? (http://blog.maartenballiauw...

  20. Avatar for Jason Plante
    Jason Plante October 21st, 2008

    Perfect. Great product. Slight correction to a typo in the link above:

    http://blog.maartenballiauw...

  21. Avatar for Robert Dean
    Robert Dean October 23rd, 2008

    Hi Maarten,

    I think I got my extension method to work now. I used part of your code to create an extension method that determines if the user is able to see/click on a link based on the Authorize attribute of the Controller or it's methods. Let me know what you think.
    http://inq.me
    -Robert

  22. Avatar for maartenba
    maartenba October 23rd, 2008

    Robert, your code looks nice! Think it's a useful feature to have in an MVC application.

  23. Avatar for Mike
    Mike October 25th, 2008

    Hey! I can't wait to get this working, it sounds very useful. I'm running into the same error message that Robert was having...getting a Null MvcHandler every time.

  24. Avatar for maartenba
    maartenba October 25th, 2008

    Mike, try the code you find linked in the top of the article. This should work smoothly.

  25. Avatar for Andr&#233;
    Andr&#233; October 27th, 2008

    Hi Maarten,

    I also had the problem with the MvcHandler. I think this is caused by the way MVC beta redirects to the Home page in default.aspx.cs:

    [b]HttpContext.Current.RewritePath(Request.ApplicationPath);
    IHttpHandler httpHandler = new MvcHttpHandler();
    httpHandler.ProcessRequest(HttpContext.Current);
    [/b]

    This instantiates a MvcHttpHandler instead of a MvcHandler. Due to the caching the code in BuildSiteMap() is only executed with a MvcHandler after the cache period has expired.

    For the time being I have changed the pageload handler back to

    [b]Response.Redirect("~/Home");[/b]

    and then everything works great again.

    HTH

  26. Avatar for Seb
    Seb October 31st, 2008

    Hello and thanks for your excellent job !

    Do you think it's possible customing the CSS style of each link ? For example, I add an "Image" attribute on the provider, which is the background url of my link.

    Thanks

    Seb

    (sorry for my english, I'm french :))

  27. Avatar for maartenba
    maartenba October 31st, 2008

    Seb, you can style the sitemap controls just the way as you would style them in classic ASP.NET.

  28. Avatar for maartenba
    maartenba October 31st, 2008

    Seb, you can style the sitemap controls just the way as you would style them in classic ASP.NET. Make sure to check http://www.carlj.ca/2007/11...

  29. Avatar for Rob Fearn
    Rob Fearn January 6th, 2009

    Awesome Code just what I was looking for :-)

    Just a quick note with the [Authorize] Action decoration, when the MvcSitemapProvider class processes the decorations it does not handle missing attributes correctly due to a simple split problem:

    [Authorize(Roles="Admin")] does not work - you have to use [Authorize(Roles="Admin",Users="*")]

    If you only add the Roles attribute without the Users attribute the menu will not display correctly as the Users attribute is treated as an empty String "" and the Split function converts this empty string into an Array of length 1 which means it gets processed once by the following loop and consequently fails:

    foreach (string user in usersArray)
    {
    if (user != "*" && (principal.Identity.Name == "" || principal.Identity.Name != user)) return false;
    }

    If you change the split lines to the following it will sort out the problem:

    // Determine if the current user is allowed to access the current node
    char[] delimeter = {','};
    string[] roleArray = roles.Split(delimeter,StringSplitOptions.RemoveEmptyEntries);
    string[] usersArray = users.Split(delimeter,StringSplitOptions.RemoveEmptyEntries);

    Thanks again for the great code :-)

    Cheers
    Rob

  30. Avatar for Thiago Santos
    Thiago Santos January 18th, 2009

    Wow. Thanks for your great code. Very good!!

  31. Avatar for pravin
    pravin January 23rd, 2009

    Hi, I am getting following error

    'System.Web.Mvc.HtmlHelper' does not contain a definition for 'ActionLink' and no extension method 'ActionLink' accepting a first argument of type 'System.Web.Mvc.HtmlHelper' could be found (are you missing a using directive or an assembly reference?)

    Source Error:

    Line 21: if (Request.IsAuthenticated) {
    Line 22: %>
    Line 23: Welcome <b><%= Html.Encode(Page.User.Identity.Name) %></b>! [ <%=Html.ActionLink("Logout", "Logout", "Account") %>
    Line 24: ]
    Line 25: <%

    Please help

  32. Avatar for maartenba
    maartenba January 23rd, 2009

    Are regular ASP.NET MVC sites working? Seems you should add a reference to the System.Web.Mvc.dll

  33. Avatar for Rob Fearn
    Rob Fearn January 24th, 2009

    Typically I find that I don't have to add a reference for the standard htmlHelper extensions (i.e. there is usually one already added to the project) I only need to add an import for my own custom html extensions.

    I have found on the odd occasion that VS08 loses site of the htmlHelper extensions and gives me a similar message, I find that a clean of the entire solution usually helps to rectify this problem. I also find sometimes that intellisense doesn't work with the extensions so I usually manually add one (e.g. type in Html.ActionLink(etc...) then build the project and intellisense goes back to normal.

    Good luck :-)

  34. Avatar for dkarantonis
    dkarantonis February 23rd, 2009

    Hi Maarten,

    i' ve asked you on September 2008 how could i implement your code in a multilingual site scenario in a theoretical basis. Now, i am actually in the phase of implementation and just adding the local and language parameter in the global.asax routing and in the sitemap xml file doesn't solve the problem. I mean, how could i implement localized versions of sitemap menu and path in order to be displayed correspondingly upon user (or browser) language selection?

    My second question has to with the Details page. I don't want the Details page to be displayed on the sitemap menu, since i don't know the product that the user will select and because i don't want to hard code all the product urls to the sitemap xml file by providing the paramid of each product. But i want the Details page to be displayed on the sitemap path, each time a product details link is selected. How could i implement it?

    best regards!

  35. Avatar for maartenba
    maartenba February 23rd, 2009

    Me and Patrice Calve (http://geekswithblogs.net/P... are currently investigating the best solution for solving that kind of issue. Blog post will come once we are out on this.

  36. Avatar for Nick Gieschen
    Nick Gieschen February 24th, 2009

    Your GetSiteMapNodeFromXMLElement is failing to account for a nested provider in the XML. To fix this do something like:

    protected SiteMapNode GetSiteMapNodeFromXMLElement(XElement node)
    {
    var provider = GetAttributeValue(node.Attribute("provider"));
    if (!string.IsNullOrEmpty(provider))
    return GetSiteMapNodesFromProvider(provider);

    ...
    }

    private SiteMapNode GetSiteMapNodesFromProvider(string provider)
    {
    var ds = new SiteMapDataSource { ShowStartingNode = true };
    ds.SiteMapProvider = provider;
    var view = (SiteMapDataSourceView)ds.GetView(string.Empty);
    var nodes = (SiteMapNodeCollection)view.Select(DataSourceSelectArguments.Empty);
    return nodes[0];
    }

  37. Avatar for Nick Gieschen
    Nick Gieschen February 24th, 2009

    I found another problem if there are nested providers: If the current node is in the nested provider it is not reported correctly. So you need:

    public override SiteMapNode FindSiteMapNode(HttpContext context)
    {
    // Node
    SiteMapNode node = null;

    foreach (var provider in providers)
    {
    node = provider.FindSiteMapNode(context);
    if (node != null)
    return node;
    }
    ...
    }

    And in ProcessXMLNodes:

    ...
    if (node.Name == ns + nodeName)
    {
    var providerName = GetAttributeValue(node.Attribute("provider"));
    if (!string.IsNullOrEmpty(providerName))
    {
    var provider = new SiteMapPath { SiteMapProvider = providerName }.Provider;
    if(!providers.Contains(provider))
    providers.Add(provider);
    AddNode(provider.RootNode, rootNode);
    }
    else
    {
    // If this is a normal siteMapNode then map the xml element
    // to a SiteMapNode, and add the node to the current rootNode.
    childNode = this.GetSiteMapNodeFromXMLElement(node);
    AddNode(childNode, rootNode);
    }
    }
    ...

    Thanks, this has saved me a ton of time.

  38. Avatar for Dominique
    Dominique March 11th, 2009

    This project does not work straight out of th zip file in the latest release candidate. Any chance that this will be updated or made part of the official MVC release?

    Thanks.

  39. Avatar for maartenba
    maartenba March 12th, 2009

    This one does: http://blog.maartenballiauw...

    We are currently working on a better implementation of this.

  40. Avatar for Joel D'Souza
    Joel D'Souza March 16th, 2009

    Great work here. An issue I've faced is that the menus aren't rendered properly on a WebKit browser (only tested Chrome/Safari). They need to be treated as uplevel browsers/be handled with updated browsercaps available here : http://owenbrady.net/browse...

  41. Avatar for matt
    matt March 17th, 2009

    Great article but im wondering if it is possible to use a drop down menu i created instead of that asp:menu? I was told to avoid tables unless its a grid or something similar and i really would like to do the menu this way but im not sure on how to create my own menu that takes a datasource like this. Any suggestions?

  42. Avatar for Shay Jacoby
    Shay Jacoby March 19th, 2009

    Hi Maarten,
    Thanks for the solution.
    I adapted this sitemap provider in my project with some small modifications.
    I use the official MVC 1.0 release.

    I have a little problem with the hierarchy, It's not a sitemap bug but I would like to know if You have any idea how to solve it.

    my sitemap:

    <?xml version="1.0" encoding="utf-8" ?>
    <siteMap>
    <siteMapNode id="Root">

    <mvcSiteMapNode id="home" title="main page" controller="Home" action="Index" paramid="">

    <mvcSiteMapNode id="category_empty" title="sub_category_empty" controller="Home" action="MyAction" paramid="">
    <mvcSiteMapNode id="sub_category1" title="sub_category_param1" controller="Home" action="MyAction" paramid="param1" />
    <mvcSiteMapNode id="sub_category2" title="sub_category_param2" controller="Home" action="MyAction" paramid="param2" />
    </mvcSiteMapNode>

    </mvcSiteMapNode>

    </siteMapNode>
    </siteMap>

    The problem: sitemap will never go to "sub_category1" or "sub_category2" because it always matches the empty id (category_empty) node.
    Any ideas?

    Thanks.
    Shay

  43. Avatar for maartenba
    maartenba March 19th, 2009

    I will be posting a new version of this soon, which also tackles this kind of things. Be patient :-)

  44. Avatar for Bilal Al-Smadi
    Bilal Al-Smadi March 19th, 2009

    Many thanx... greate article :)

  45. Avatar for Anis
    Anis May 21st, 2009

    Hi Maartim!

    It's a nice article indeed.

    I just want to know if we could specify the action permissions in sitemap instead of specifying it as [Authorize (Role="Admin")] attribute?

    I think it would be a nice addion.r

    Regards,
    Anis

  46. Avatar for maartenba
    maartenba May 22nd, 2009

    Check http://mvcsitemap.codeplex.com, we've put it in there.

  47. Avatar for Thomas
    Thomas October 1st, 2009

    hi!
    i just want to thank you for your great articel and your good work...the implementing part is exactly what i&#180m looking for...thanks a lot!

  48. Avatar for Tran Chi Khanh
    Tran Chi Khanh October 20th, 2009

    Hi,
    I have a error :
    Server Error in '/' Application.
    Multiple nodes with the same URL '/StoreFont' were found. XmlSiteMapProvider requires that sitemap nodes have unique URLs.
    Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

    Exception Details: System.InvalidOperationException: Multiple nodes with the same URL '/StoreFont' were found. XmlSiteMapProvider requires that sitemap nodes have unique URLs.

    Source Error:

    Line 354: }
    Line 355:
    Line 356: base.AddNode(node, parentNode);
    Line 357: }
    Line 358:

    Source File: D:\Project .Net\MVC\MyShop\MvcSiteMap.Core\MvcSiteMapProvider.cs Line: 356

    When i use this sitemap:
    <?xml version="1.0" encoding="utf-8" ?>
    <siteMap xmlns="http://mvcsitemap.codeplex...." enableLocalization="true">
    <mvcSiteMapNode title="HomeTitle" controller="Home" action="Indexs" isDynamic="true" dynamicParameters="*">

    <mvcSiteMapNode title="Products" controller="Products" visibility="InSiteMapPathOnly">
    <mvcSiteMapNode title="InsertsProduct"
    controller="Products" action="List"
    isDynamic="true" dynamicParameters="id"
    key="ProductsListCategory"/>
    <mvcSiteMapNode title="Manage" controller="Products" action="Index" />
    </mvcSiteMapNode>

    <mvcSiteMapNode title="AccountTitle" controller="Account" visibility="InSiteMapPathOnly">
    <!-- No need to specify controller here if not needed: parent controller name will be used -->
    <mvcSiteMapNode title="LoginTitle" action="LogOn" />
    <mvcSiteMapNode title="AccountCreationTitle" action="Register" />
    <mvcSiteMapNode title="ChangePasswordTitle" action="ChangePassword" />
    <mvcSiteMapNode title="LogoutTitle" action="LogOff" />
    </mvcSiteMapNode>

    <mvcSiteMapNode title="SitemapTitle" controller="Home" action="Sitemap" visibility="InSiteMapPathOnly" />

    <mvcSiteMapNode title="AboutUsTitle" controller="Home" action="About" />

    </mvcSiteMapNode>
    </siteMap>

    --> this error because i use <mvcSiteMapNode title="Manage" controller="Products" action="Index" />. If i change to <mvcSiteMapNode title="Manage" controller="Products" action="Indexs" /> --> no error.
    Please explain for me why i have this error. thanks a lot

  49. Avatar for maartenba
    maartenba October 20th, 2009

    It means that there are multiple nodes in the sitemap which are generating the same URL.

  50. Avatar for Tran Chi Khanh
    Tran Chi Khanh October 21st, 2009

    Thanks maartenba. I know "there are multiple nodes in the sitemap which are generating the same URL."
    But i don't know why i use <mvcSiteMapNode title="Manage" controller="Products" action="Index" /> --> it always generate url is /root instead of /root/controller/action because i declare controller is Products and action is Index. I want to use index in my sitemap with several controller.

  51. Avatar for Maarten
    Maarten October 21st, 2009

    Can you post the routing table?

  52. Avatar for Tran Chi Khanh
    Tran Chi Khanh October 21st, 2009

    This is my routing table in global.asax:
    public static void RegisterRoutes(RouteCollection routes)
    {
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    AreaRegistration.RegisterAllAreas();

    routes.MapRoute(
    "Default", // Route name
    "{controller}/{action}/{id}", // URL with parameters
    new { controller = "Home", action = "Index", id = "" } // Parameter defaults
    );

    }

    protected void Application_Start()
    {
    RegisterRoutes(RouteTable.Routes);
    }

    I use asp.net mvc area for my project. And the code below is my file route.cs for each area:

    public class Routes : AreaRegistration
    {
    public override string AreaName
    {
    get { return "StoreFont"; }
    }

    public override void RegisterArea(AreaRegistrationContext context)
    {
    context.MapRoute(
    "StoreFont_Default",
    "StoreFont/{controller}/{action}/{id}",
    new { controller = "Products", action = "Index", id = "" });
    }
    }
    --> for my Area StoreFont.
    When i use action = Index on every controller in sitemap. the url always is http://localhost:8080/StoreFont.
    I don't know what i do wrong.

  53. Avatar for Maarten
    Maarten October 21st, 2009

    Are you using the latest code from the source code tab on CodePlex? There's a fix for this in it.

  54. Avatar for Smerp
    Smerp December 3rd, 2009

    Hi,

    This entry node works for me (please note i hard coded parameter id = 100):

    <mvcSiteMapNode title="Product Detail"
    controller="Product"
    action="Details"
    isDynamic="true"
    dynamicParameters="id" id="100"/>

    Is there a way to have it assigned @ runtime? So every time I click the link "Details" it will redirect me to the same item i last selected..

    Lets say my breadcrumb is

    A. Home > Products > Details > Price (Product ID = 100)
    B. Home > Products > Details > Price (Product ID = 200)

    If i clicked "Details" in A, it should route me to Product ID 100 or
    If i clicked "Details" in B, it should route me to Product ID 200

    Hope you could help me on this.

    Thank you in advance!

  55. Avatar for Milan
    Milan July 6th, 2010

    MvcSiteMapProvider is working fine, but it is returning results in "https" instead of "http",

    how to i do that?

  56. Avatar for maartenba
    maartenba July 19th, 2010

    Will be fixed in the next release.

  57. Avatar for kevin
    kevin July 26th, 2010

    hi the source code was not able to download can anyone show me where can i download it

  58. Avatar for st
    st August 19th, 2010

    Hello, can anyone send me the source code/sample demo? I am not able to download it. The page hits Runtime Error.

  59. Avatar for Bob Sculley
    Bob Sculley December 15th, 2015

    Doesn't look like this thread is active, but what the heck; is there a version of the MvcSiteMapProvider that is compatible with MVC6? I would like to implement a treeview with security trimming.
    Thanks