Recursion within Dynamic Node Provider

Aug 20, 2011 at 12:31 AM

Hi there,

First and foremost - I'm loving this project - was a breeze to drop into an ASP.NET MVC 3 app, and so far integration has been a breeze. I am however, having trouble with implementing a recursive Dynamic Node Provider. We have a location object within our domain which can obviously contain children (a state may contain counties, counties contain cities, etc). I am certain the entire object graph is begin passed to the Dynamic Node Provider (using NHibernate and eagerly loading the entire tree).

I've simplified the code below (simply removed some noise). The first level of the tree works fine, however as soon as we start accessing the depth of the tree, the Html SiteMapPath helper fails. Is this a limitation of the Html helper? I haven't dug around the source code yet - as I figured I'd ask here first.

public class RecursiveLocationDynamicNodeProvider : DynamicNodeProviderBase
    {
        private readonly ILocationService _locationService;

        public RecursiveLocationDynamicNodeProvider()
        {
            _locationService = ServiceLocator.Current.GetInstance<ILocationService>();
        }

        public override IEnumerable<DynamicNode> GetDynamicNodeCollection()
        {
            var result = new List<DynamicNode>();
            var locations = _locationService.FetchTree();
            
            result = locations.Select(location => new DynamicNode
            {
                Key = string.Format("Locations_{0}", location.Id.ToString()),
                Title = location.Name,
                Controller = "locations",
                Action = "display",
                Area = null,
                ParentKey = location.Heirarchy.IsRoot 
                    ? null 
                    : string.Format("Locations_{0}", location.Heirarchy.Parent.Id.ToString()),
                RouteValues = new Dictionary<string, object> {{ "path", GetPath(location) }}
            }).ToList();

            return result;
        }

        private string GetPath(Location location)
        {
            var paths = new Stack<string>();

            while(location != null)
            {
                paths.Push(location.Route);

                location = location.Heirarchy.Parent;
            }

            return String.Join("/", paths);
        }
    }

Coordinator
Aug 26, 2011 at 12:56 PM

Do you have a small repro sample on this one?

Aug 26, 2011 at 4:51 PM
Edited Aug 26, 2011 at 5:41 PM

I'll post all relevant source code shortly, including the Razor Templates and calling code.

Thanks in advance,
Chris
Aug 26, 2011 at 5:23 PM
Edited Aug 26, 2011 at 5:40 PM

Ok maartenba, here's *all* the relevant information, let me know if you need anything else.

Web.config:

<siteMap defaultProvider="MvcSiteMapProvider" enabled="true">
  <providers>
	<clear />
	<add name="MvcSiteMapProvider" 
	     type="MvcSiteMapProvider.DefaultSiteMapProvider, MvcSiteMapProvider" 
		 siteMapFile="~/Web.sitemap" 
		 securityTrimmingEnabled="true" 
		 cacheDuration="5" enableLocalization="true" 
		 scanAssembliesForSiteMapNodes="true" 
		 excludeAssembliesForScan="" 
		 includeAssembliesForScan="" 
		 attributesToIgnore="bling,icon,operations" 
		 nodeKeyGenerator="MvcSiteMapProvider.DefaultNodeKeyGenerator, MvcSiteMapProvider" 
		 controllerTypeResolver="MvcSiteMapProvider.DefaultControllerTypeResolver, MvcSiteMapProvider" 
		 actionMethodParameterResolver="MvcSiteMapProvider.DefaultActionMethodParameterResolver, MvcSiteMapProvider" 
		 aclModule="MvcSiteMapProvider.DefaultAclModule, MvcSiteMapProvider" siteMapNodeUrlResolver="MvcSiteMapProvider.DefaultSiteMapNodeUrlResolver, MvcSiteMapProvider" 
		 siteMapNodeVisibilityProvider="MvcSiteMapProvider.DefaultSiteMapNodeVisibilityProvider, MvcSiteMapProvider" 
		 siteMapProviderEventHandler="MvcSiteMapProvider.DefaultSiteMapProviderEventHandler, MvcSiteMapProvider" />
  </providers>
</siteMap>

Web.sitemap:

<?xml version="1.0" encoding="utf-8" ?>
<mvcSiteMap xmlns="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-3.0" enableLocalization="true">
    <mvcSiteMapNode title="Home" controller="home" action="index">
        <mvcSiteMapNode dynamicNodeProvider="Mhs.Mvc.Infrastructure.MvcSiteMapProvider.DynamicNodes.RecursiveLocationDynamicNodeProvider, Mhs.Mvc" />
    </mvcSiteMapNode>
</mvcSiteMap>

_Layout.cshtml: (excess html removed for brevity)

<div id="toolbar" class="bottom">
	<div class="breadcrumbs">@Html.MvcSiteMap().SiteMapPath()</div>
</div>

SiteMapPathHelperModel.cshtml:

@model MvcSiteMapProvider.Web.Html.Models.SiteMapPathHelperModel
@using System.Web.Mvc.Html
@using System.Linq
@using MvcSiteMapProvider.Web.Html.Models

<ol>
@foreach (var node in Model) { 
    @Html.DisplayFor(m => node);
}
</ol>

SiteMapNodeModel.cshtml:

@model MvcSiteMapProvider.Web.Html.Models.SiteMapNodeModel
@using System.Web.Mvc.Html
@using MvcSiteMapProvider.Web.Html.Models

@if (Model.IsCurrentNode) 
{
    <li class="current"><a href="@Model.Url">@Model.Title</a></li>
} 
else 
{
    <li><a href="@Model.Url">@Model.Title</a></li>
}

RecursiveLocationDynamicNodeProvider.cs:

public class RecursiveLocationDynamicNodeProvider : DynamicNodeProviderBase
{
	private readonly ILocationService _locationService;

	public RecursiveLocationDynamicNodeProvider()
	{
		_locationService = ServiceLocator.Current.GetInstance<ILocationService>();
	}

	public override IEnumerable<DynamicNode> GetDynamicNodeCollection()
	{
		var result = new List<DynamicNode>();
		var locations = _locationService.FetchTree();
		
		result = locations.Select(location => new DynamicNode
		{
			Key = string.Format("Locations_{0}", location.Id.ToString()),
			Title = location.Name,
			Controller = "locations",
			Action = "display",
			Area = null,
			ParentKey = location.Heirarchy.IsRoot 
				? null 
				: string.Format("Locations_{0}", location.Heirarchy.Parent.Id.ToString()),
			RouteValues = new Dictionary<string, object> {{ "path", GetPath(location) }}
		}).ToList();

		return result;
	}

	private string GetPath(Location location)
	{
		var paths = new Stack<string>();

		while(location != null)
		{
			paths.Push(location.Route);

			location = location.Heirarchy.Parent;
		}

		return String.Join("/", paths);
	}
}

Let me know if you need to see anything else. The service and repository layer code is pretty vanilla NHibernate eagerly loading my "Location" domain. This code works for the first level of the tree (let's assume a generic URL structure).

Say I browse to http://domain.com/ca/ - the breadcrumbs are rendered as expected. However, when I browse to http://domain.com/ca/san-diego/ - the breadcrumbs are no longer present. I have a catch-all route (with a custom route constraint) which processes all unmatched routes and attempts to match them to a location. This is done by segmenting the url to logical locations (matching each piece of the url to the tree hierarchy).

Even if we can't adapt it to my situation, I'd love to see a Recursive Dynamic Node Provider - I've tried simplifying the moving parts with a blank MVC3 Application - but still can't get the breadcrumbs to work in a recursive situation. 

-Chris

Coordinator
Sep 2, 2011 at 8:04 PM

Here's a sample which produces a valid sitemap representing the hieracrchy created:

    public class RecursiveLocationDynamicNodeProvider
        : DynamicNodeProviderBase
    {
        public override IEnumerable<DynamicNode> GetDynamicNodeCollection()
        {
            yield return new DynamicNode
            {
                Key = "Locations_1",
                Title = "Location 1",
                Controller = "Home",
                Action = "Index",
                Area = null,
                //ParentKey = null, // this you probably do NOT want... so let's comment it for the root node
                RouteValues = new Dictionary<stringobject> { { "path""Location1" } }
            };
 
            yield return new DynamicNode
            {
                Key = "Locations_2",
                Title = "Location 2",
                Controller = "Home",
                Action = "Index",
                Area = null,
                ParentKey = "Locations_1",
                RouteValues = new Dictionary<stringobject> { { "path""Location2" } }
            };
 
            yield return new DynamicNode
            {
                Key = "Locations_3",
                Title = "Location 3",
                Controller = "Home",
                Action = "Index",
                Area = null,
                ParentKey = "Locations_1",
                RouteValues = new Dictionary<stringobject> { { "path""Location3" } }
            };
 
            yield return new DynamicNode
            {
                Key = "Locations_4",
                Title = "Location 4",
                Controller = "Home",
                Action = "Index",
                Area = null,
                ParentKey = "Locations_2",
                RouteValues = new Dictionary<stringobject> { { "path""Location4" } }
            };
        }
    }