Logo

Maarten Balliauw {blog}

ASP.NET, ASP.NET MVC, Windows Azure, PHP, ...

About the author

Maarten Balliauw is currently employed as a Technical Evangelist at JetBrains. His interests are mainly web applications developed in ASP.NET (C#) or PHP and the Windows Azure cloud platform.
More about me More about me
Send mail E-mail me


ASP.NET MVC Quickly Pro NuGet Subscribe to my RSS feed Follow me on Twitter! View Maarten Balliauw's profile on LinkedIn
Maarten Balliauw - MVP - Most Valuable Professional
Maarten Balliauw - ASPInsider

Search

Archive

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

© Copyright Maarten Balliauw 2013


Building an ASP.NET MVC sitemap provider with security trimming

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:

<?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>

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:

id Id for the current node. Can only occur once!
title The title to show in menu's.
description Optional description.
controller The controller to map this node to. Will default to "Home" if it is not specified.
action The 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.
paramid Well, 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:

/// <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

}

Reading the mvcSiteMapNode XML

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

/// <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;
}

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!

/// <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;
}

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:


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

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 


Categories: ASP.NET | C# | General | MVC | Software

Comments (59) -

pp |

Saturday, August 30, 2008 5:03 PM

pp

Its great - that is what i looking for

Celik Turkey |

Sunday, September 07, 2008 1:48 PM

Celik

This is what i was looking for, thank you

tgmdbm United Kingdom |

Tuesday, September 09, 2008 12:59 PM

tgmdbm

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.

maartenba Belgium |

Tuesday, September 09, 2008 1:51 PM

maartenba

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

Matt Bertulli Canada |

Wednesday, September 10, 2008 2:50 PM

Matt Bertulli

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.

maartenba Belgium |

Wednesday, September 10, 2008 3:00 PM

maartenba

I've posted an updated version for preview 5.

dkarantonis Greece |

Friday, September 19, 2008 9:42 AM

dkarantonis

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

Matt Bertulli Canada |

Friday, September 19, 2008 8:37 PM

Matt Bertulli

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>

maartenba |

Monday, September 22, 2008 9:32 AM

maartenba

@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

dkarantonis Greece |

Monday, September 22, 2008 10:12 AM

dkarantonis

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

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

thanks again!

maartenba |

Monday, September 22, 2008 10:20 AM

maartenba

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

robert United States |

Tuesday, September 23, 2008 5:36 PM

robert

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/1324349.aspx) - 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

maartenba Belgium |

Tuesday, September 23, 2008 6:31 PM

maartenba

Hello Robert,

Make sure to use the latest code (blog.maartenballiauw.be/file.axd), this issue has been resolved in there.

Regards,
Maarten

robert United States |

Tuesday, September 23, 2008 7:01 PM

robert

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.

maartenba Belgium |

Tuesday, September 23, 2008 7:31 PM

maartenba

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

Rei Spain |

Thursday, October 02, 2008 1:58 AM

Rei

Firstly many thanks for a great component!!!

One thing to note about the provider is that the method IsAccessibleToUser creates an instance of a controller at:

  IController controller = ControllerBuilder.Current.GetControllerFactory().CreateController(handler.RequestContext, mvcNode.Controller);


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:



        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 IsAccessibleToUser method as follows:

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



Hope this helps anyone.

Alvin Australia |

Sunday, October 05, 2008 4:46 AM

Alvin

Hi Maarten,

This is great work. Much appreciated.

-Alvin

Jason Plante United States |

Tuesday, October 21, 2008 3:24 PM

Jason Plante

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.

maartenba Belgium |

Tuesday, October 21, 2008 3:38 PM

maartenba

Have you tried the latest version? (blog.maartenballiauw.be/file.axd)

Jason Plante United States |

Tuesday, October 21, 2008 3:57 PM

Jason Plante

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

blog.maartenballiauw.be/file.axd

Robert Dean United States |

Thursday, October 23, 2008 1:16 AM

Robert Dean

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

maartenba Belgium |

Thursday, October 23, 2008 8:25 AM

maartenba

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

Mike United States |

Friday, October 24, 2008 6:53 PM

Mike

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.

maartenba Belgium |

Saturday, October 25, 2008 11:10 AM

maartenba

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

André Netherlands |

Monday, October 27, 2008 2:55 PM

Andr&#233;

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:

HttpContext.Current.RewritePath(Request.ApplicationPath);
IHttpHandler httpHandler = new MvcHttpHandler();
httpHandler.ProcessRequest(HttpContext.Current);


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

Response.Redirect("~/Home");

and then everything works great again.

HTH

Seb |

Friday, October 31, 2008 10:24 AM

Seb

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 Smile)

maartenba Belgium |

Friday, October 31, 2008 11:40 AM

maartenba

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

maartenba |

Friday, October 31, 2008 12:00 PM

maartenba

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

Rob Fearn Australia |

Tuesday, January 06, 2009 4:11 AM

Rob Fearn

Awesome Code just what I was looking for Smile

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 Smile

Cheers
Rob

Thiago Santos Canada |

Saturday, January 17, 2009 6:51 PM

Thiago Santos

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

pravin United States |

Thursday, January 22, 2009 8:18 PM

pravin

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

maartenba Belgium |

Friday, January 23, 2009 7:44 AM

maartenba

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

Rob Fearn Australia |

Saturday, January 24, 2009 12:51 AM

Rob Fearn

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 Smile

dkarantonis Greece |

Sunday, February 22, 2009 10:30 PM

dkarantonis

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!

maartenba Belgium |

Monday, February 23, 2009 8:27 AM

maartenba

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

Nick Gieschen United States |

Tuesday, February 24, 2009 5:29 AM

Nick Gieschen

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];
}

Nick Gieschen United States |

Tuesday, February 24, 2009 8:12 AM

Nick Gieschen

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.

Dominique United Kingdom |

Wednesday, March 11, 2009 1:16 PM

Dominique

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.

maartenba Belgium |

Thursday, March 12, 2009 7:27 AM

maartenba

This one does: blog.maartenballiauw.be/file.axd

We are currently working on a better implementation of this.

Joel D'Souza United States |

Monday, March 16, 2009 1:41 PM

Joel D'Souza

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/browsercaps

matt United States |

Monday, March 16, 2009 9:56 PM

matt

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?

Shay Jacoby Israel |

Thursday, March 19, 2009 10:18 AM

Shay Jacoby

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





maartenba Belgium |

Thursday, March 19, 2009 10:52 AM

maartenba

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

Bilal Al-Smadi Jordan |

Thursday, March 19, 2009 4:13 PM

Bilal Al-Smadi

Many thanx... greate article Smile

Anis Saudi Arabia |

Thursday, May 21, 2009 1:15 PM

Anis

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

maartenba Belgium |

Friday, May 22, 2009 7:16 AM

maartenba

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

Thomas Germany |

Thursday, October 01, 2009 11:17 AM

Thomas

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

Tran Chi Khanh Vietnam |

Tuesday, October 20, 2009 2:30 PM

Tran Chi Khanh

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="mvcsitemap.codeplex.com/.../...teMap-File-1.0"; 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


maartenba Belgium |

Tuesday, October 20, 2009 3:28 PM

maartenba

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

Tran Chi Khanh Vietnam |

Wednesday, October 21, 2009 12:33 PM

Tran Chi Khanh

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.

Maarten Belgium |

Wednesday, October 21, 2009 1:07 PM

Maarten

Can you post the routing table?

Tran Chi Khanh Vietnam |

Wednesday, October 21, 2009 3:01 PM

Tran Chi Khanh

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.

Maarten Belgium |

Wednesday, October 21, 2009 3:07 PM

Maarten

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

Tran Chi Khanh Vietnam |

Wednesday, October 21, 2009 4:00 PM

Tran Chi Khanh

I download source code here mvcsitemap.codeplex.com/.../ProjectReleases.aspx
When i download this dll (mvcsitemap.codeplex.com/.../ProjectReleases.aspx),  this error have fixed.
Thanks a lot

Smerp Singapore |

Thursday, December 03, 2009 9:26 AM

Smerp

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!

Milan India |

Tuesday, July 06, 2010 9:53 AM

Milan


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

how to i do that?

maartenba Belgium |

Monday, July 19, 2010 5:55 PM

maartenba

Will be fixed in the next release.

kevin Australia |

Monday, July 26, 2010 9:15 AM

kevin

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

st Malaysia |

Thursday, August 19, 2010 3:35 AM

st

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

Pingbacks and trackbacks (8)+

Comments are closed