19
votes

I am using MvcSiteMapProvider 2.2.1 (http://mvcsitemap.codeplex.com), and am having a problem with creating children under a dynamic node (using a dynamicNodeProvider) when those children have a dynamic parameter (id).

I am losing breadcrumbs for the following route:

Stores/5/Products/Edit/23

where the url pattern is:

Stores/{storeID}/{controller}/{action}/{id}

It works fine when the ID is left out (ie the "New" action). But when the ID is specified, it doesn't match the route, and my breadcrumbs (using the SiteMapPath helper) is blank.

My Sitemap: (abreviated)

<?xml version="1.0" encoding="utf-8" ?>
<mvcSiteMap xmlns="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-2.0">
    <mvcSiteMapNode title="Home" controller="Dashboard" action="Index" changeFrequency="Always" updatePriority="Normal">
        <mvcSiteMapNode title="My Account" controller="Account" action="Index" key="Account" />
        <mvcSiteMapNode title="My Stores" area="Stores" controller="Home" action="Index" visibilityProvider="ControlPanel.Areas.Stores.StoreAreaVisibilityProvider, ControlPanel"  >
            <mvcSiteMapNode title="Store" action="Index" dynamicNodeProvider="ControlPanel.Areas.Stores.StoreAreaNodeProvider, ControlPanel" />
        </mvcSiteMapNode>
    </mvcSiteMapNode>
</mvcSiteMap>

Area Registration:

public override void RegisterArea(AreaRegistrationContext context)
{
        context.MapRoute(
            "Store_Index",
            "Stores",
            new { action = "Index", controller = "Home" },
            new string[] { "ControlPanel.Areas.Stores.Controllers" }
            );

        context.MapRoute(
            "Store_default",
            "Stores/{storeID}/{controller}/{action}/{id}",
            new { action = "Index", controller = "Manage", id = UrlParameter.Optional },
            new { storeID = @"\d+" },
            new string[] { "ControlPanel.Areas.Stores.Controllers" }
        );
    }

First Attempt:

The first thing I tried was to create the child nodes right in the sitemap xml as children of the dynamic node. This didn't work at all, and these ended up being children of "Home". I would put a ParentKey attribute in there, except these will be repeated per store and thus there will be multiple parentkeys

<?xml version="1.0" encoding="utf-8" ?>
<mvcSiteMap xmlns="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-2.0">
  <mvcSiteMapNode title="Home" controller="Dashboard" action="Index" changeFrequency="Always" updatePriority="Normal">
    <mvcSiteMapNode title="My Account" controller="Account" action="Index" key="Account" />
    <mvcSiteMapNode title="My Stores" area="Stores" controller="Home" action="Index" visibilityProvider="ControlPanel.Areas.Stores.StoreAreaVisibilityProvider, ControlPanel"  >
      <mvcSiteMapNode title="Store" action="Index" dynamicNodeProvider="ControlPanel.Areas.Stores.StoreAreaNodeProvider, ControlPanel">
        <mvcSiteMapNode title="Products" area="Stores" controller="Products" action="Index">
          <mvcSiteMapNode title="Edit" area="Stores" controller="Products" action="Edit"/>
          <mvcSiteMapNode title="New" area="Stores" controller="Products" action="Edit"/>
        </mvcSiteMapNode>
      </mvcSiteMapNode>
    </mvcSiteMapNode>
  </mvcSiteMapNode>
</mvcSiteMap>

Second Attempt:

It seemed the next option was to just add the child nodes right in my DynamicNodeProvider. This worked much better except for the nodes that had dynamic parameters

(the below is modified for ease of explanation...)

public class StoreAreaNodeProvider : IDynamicNodeProvider
{
    public IEnumerable<DynamicNode> GetDynamicNodeCollection()
    {
        var nodes = new List<DynamicNode>();

        foreach (var store in repo.GetStores())
        {
            DynamicNode node = new DynamicNode();
            node.Title = store.Name;
            node.Area = "Stores";
            node.Controller = "Manage";
            node.Action = "Index";
            node.RouteValues.Add("storeID", store.StoreID);
            node.Key = "Store_" + store.StoreID.ToString();

            nodes.Add(node);

            //Child of node
            DynamicNode productsNode = new DynamicNode();
            productsNode.Title = "Products";
            productsNode.Area = "Stores";
            productsNode.Controller = "Products";
            productsNode.Action = "Index";
            productsNode.RouteValues.Add("storeID", store.StoreID);
            productsNode.ParentKey = String.Format("Store_{0}", store.StoreID.ToString());
            productsNode.Key = String.Format("Store_{0}_Products", store.StoreID.ToString());

            nodes.Add(productsNode);

            //child of productsNode
            DynamicNode editNode = new DynamicNode();
            editNode.Title = "Edit";
            editNode.Area = "Stores";
            editNode.Action = "Edit";
            editNode.Controller = "Products";
            editNode.RouteValues.Add("storeID", store.StoreID);
            //I can't add the RouteValue "ID" here because it is dynamic
            //I would have do loop through every product for this store with
            //another dynamic node provider, but that seems terribly inefficient and stupid
            editNode.ParentKey = String.Format("Store_{0}_Products", store.StoreID.ToString());
            editNode.Attributes.Add("visibility", "SiteMapPathHelper,!*");

            nodes.Add(editNode);
        }

        return nodes;
    }
}

In Summary

Does Work: Stores/5/Products/New
Doesn't Work: Stores/5/Products/Edit/23
For Url Pattern: Stores/{storeID}/{controller}/{action}/{id}

What I would like to be able to do:

editNode.Attributes.Add("isDynamic", "true");
editNode.Attributes.Add("dynamicParameters", "id");

How can I mimick the old MvcSiteMapProvider's dynamicParameters attribute on a node that is a child of a dynamicNode? Basically I need it to ignore the "id" route value when matching routes.

Hopefully I explained that properly, and didn't overwhelm you with information. Thanks!


UPDATE:

Here is the solution that worked for me based on Jakub's answer.

In MvcSiteMapProvider 2.x, you can make your own implementation of ISiteMapNodeUrlResolver instead of having to modify the source. So I basically added back in the ability to have the dynamicParameters attribute

Class:

namespace ControlPanel
{
    public class CustomSiteMapNodeUrlResolver : ISiteMapNodeUrlResolver
    {
        public virtual string ResolveUrl(MvcSiteMapNode mvcSiteMapNode, string area, string controller, string action, IDictionary<string, object> routeValues)
        {
            RequestContext ctx;
            if (HttpContext.Current.Handler is MvcHandler)
                ctx = ((MvcHandler)HttpContext.Current.Handler).RequestContext;
            else
                ctx = new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData());

            //Begin Added Code
            if (mvcSiteMapNode["dynamicParameters"] != null)
            {
                foreach (var item in mvcSiteMapNode["dynamicParameters"].Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
                {
                    var dp = item.Trim();
                    routeValues[da] = ctx.RouteData.Values[dp];
                }
            }
            //End Added Code

            return new UrlHelper(ctx).Action(action, controller, new RouteValueDictionary(routeValues));
        }
    }
}

Web.config:

<siteMap defaultProvider="MvcSiteMapProvider" enabled="true">
  <providers>
    <clear/>
    <add name="MvcSiteMapProvider"
         type="MvcSiteMapProvider.DefaultSiteMapProvider, MvcSiteMapProvider"
         siteMapFile="~/Mvc.Sitemap"
         securityTrimmingEnabled="true"
         attributesToIgnore="visibility,dynamicParameters"
         scanAssembliesForSiteMapNodes="true" 
         siteMapNodeUrlResolver="ControlPanel.CustomSiteMapNodeUrlResolver, ControlPanel"
         siteMapNodeVisibilityProvider="MvcSiteMapProvider.FilteredSiteMapNodeVisibilityProvider, MvcSiteMapProvider" />
  </providers>
</siteMap>

Dynamic Node Provider:

DynamicNode node = new DynamicNode();
node.Attributes.Add("dynamicParameters", "id");
1
It's now 2012, I'm using version 3.x and still having the same problem.Alper
And the problem is your custom ResolveUrl method runs as soon as the application starts (without navigating to that parametered page), so the line you are getting the current route parameter ctx.RouteData.Values[dp] is null, so it is not possible to replace the sitemap route parameter with current route parameter. Any ideas?Alper
Hi @AlperOzcetin, I haven't touched this code in so long, I have no idea. And actually a few months after this post, this project evolved enough to where the MvcSiteMapProvider wasn't doing it for me, and we ended up rolling our own solution from scratch (which the site still runs on today). Good luck...Chris Curtis

1 Answers

8
votes

I am using version 1.x. I had a similar problem with dynamic parameters.

I had to modify the source code - made a change in MvcSiteMapNode.cs. This is the new implementation of Url property

    public override string Url
    {
        get
        {
            if (!string.IsNullOrEmpty(this.url))
                return this.url;

            RequestContext ctx;
            if (HttpContext.Current.Handler is MvcHandler)
                ctx = ((MvcHandler)HttpContext.Current.Handler).RequestContext;
            else
                ctx = new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData());

            var routeValues = new RouteValueDictionary(RouteValues);

            foreach (var key in DynamicParameters)
                routeValues.Add(key, ctx.RouteData.Values[key]);

            return new UrlHelper(ctx).Action(Action, Controller, routeValues);
        }
        set
        {
            this.url = value;
        }
    }

Notice how actual values of dynamicParameters are added to routeValues collection.

The above change allowed me to define dynamic paremteres in sitemap (like 'id') and create breadcrumbs with links like Account/User/Edit/23.

I took a brief look at version 2.x and you should be able to apply similar patch. Hope it will help you...