Html.MvcSiteMap().Menu() failing to show some nodes

Nov 8, 2010 at 4:15 PM

I find that when I create a node in the mvc sitemap file, it doesn't show when you use <%=Html.MvcSiteMap().Menu() %> if you have a key assigned to it.

I have a top menu, populated from the site map:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl`1[ [MvcSiteMapProvider.Web.Html.Models.MenuHelperModel,MvcSiteMapProvider] ]" %>
<%@ Import Namespace="System.Web.Mvc.Html" %>
<%@ Import Namespace="MvcSiteMapProvider.Web.Html.Models" %>
<ul id="menu">
<% foreach (var node in Model.Nodes) { %>
    <li><%=Html.DisplayFor(m => node)%></li>
<% } %>
</ul>

Then show it using:

<div id="menucontainer">
	<%= Html.MvcSiteMap().Menu("TopMenuHelperModel", false) %>
</div>

Then add my controller to the site map:

<mvcSiteMapNode controller="Test" title="Test" action="Index">
	
</mvcSiteMapNode>

It shows up in the menu. However, if you have assigned a key and added a dynamic node under it, only it shows in the menu. I have also seen the opposite happen (i.e. everything else is shown, but those with key's / dynamic nodes), usually with nested dynamic nodes.

<mvcSiteMapNode key="Test" controller="Test" title="Test" action="Index">
	<mvcSiteMapNode action="Dates" dynamicNodeProvider="MvcSite.DynamicNodeProvider.TestDynamicNodeProvider, MvcSite" />
</mvcSiteMapNode>

Any ideas what is happening?

Coordinator
Nov 10, 2010 at 5:50 AM

Looking at your template, it will only render one level of menu items. Does it work like expected if you use the default menu helper template?

Nov 10, 2010 at 11:23 AM

Using the default it has the same issue. When going on the page, I have a breadcrumb trail and it is the root node, rather the the correct 'Home' one. e.g. Test > November 2010, rather than Home > Test > November 2010.

Nov 12, 2010 at 4:25 PM

I've looked at the my route table and it looks that it might have been the cause. However I've found 2.2.1 is a lot slower than 2.1, taking several minutes to load the page, even though I have set securityTrimming="false" in the web.config.

In my situation, I am looping through existing records and adding the dates associated with them as entries:

MyPage > November 2010

Then MyPage/Edit/1 would end up with:

MyPage > November 2010 > Edit

<mvcSiteMapNode controller="Test" title="Test" action="Index">
	<mvcSiteMapNode action="Create" title="Create Test" />
	<mvcSiteMapNode dynamicNodeProvider="MvcSite.DynamicNodeProvider.DatesDynamicNodeProvider, MvcSite">
		<mvcSiteMapNode action="Details" dynamicNodeProvider="MvcSite.DynamicNodeProvider.DetailsDynamicNodeProvider, MvcSite" />
		<mvcSiteMapNode action="Edit" dynamicNodeProvider="MvcSite.DynamicNodeProvider.EditDynamicNodeProvider, MvcSite" />
		<mvcSiteMapNode action="Delete" dynamicNodeProvider="MvcSite.DynamicNodeProvider.DeleteDynamicNodeProvider, MvcSite" />
	</mvcSiteMapNode>
</mvcSiteMapNode>

In the DateDynamicNodeProvider:

public override IEnumerable<DynamicNode> GetDynamicNodeCollection()
{
	DateTime start = DateTime.Now;
	DateTime current = start.Value;
	do
	{
		DynamicNode node = CreateNode(current);
		yield return node;
	} while ((current = current.AddMonths(1)) < DateTime.Now.AddMonths(2));
}

public static DynamicNode CreateNode(DateTime date)
{
	DynamicNode node = new DynamicNode("Test_" + date.ToString("yyyy-M"), date.ToString("MMMM yyyy"));
	node.RouteValues.Add("year", date.Year);
	node.RouteValues.Add("month", date.Month);
	return node;
}

The Details/Edit/Delete DynamicNodeProviders:

public override IEnumerable<DynamicNode> GetDynamicNodeCollection()
{
	db = new TestDBDataContext();

	var  = db.Test.ToList();
	if (tests != null)
	{
		DateTime created;
		// Create a node for each tests
		foreach (var test in tests)
		{
			if (test.DateReferralCreated.HasValue)
			{
				created = test.DateReferralCreated.Value;
				DynamicNode node = new DynamicNode("Test_Details_" + test.ID, "Test_" + created.ToString("yyyy-M"), "Test Details", "");
				node.RouteValues["id"] = test.ID;
				yield return node;
			}
		}
	}
}

This is the same code used as it was when it was working fine in the previous version of MvcSiteMap.

Coordinator
Nov 15, 2010 at 10:44 AM

Regarding speed: yes, it is slower but only in debug mode. Check http://mvcsitemap.codeplex.com/wikipage?title=HtmlHelper%20functions&referringTitle=Home:

Known performance issues and the solution

A performance degradation may be noticed working with HtmlHelper functions from Visual Studio. This is because during debugging, no caching occurs internally in ASP.NET MVC regarding view rendering. The solution to this is running the application in release mode or changing Web.config to run under release mode:

<compilation debug="false">


See Simone Chiaretta's blog for a detailed explanation on this.

Nov 15, 2010 at 12:51 PM
Edited Nov 15, 2010 at 5:24 PM

It's in debug mode on my development machine and faster that the server, which is not in debug node.

I am loading about 1000 rows of data from the database, and for each of them there are 3 nodes (Details, Edit, Delete). I am loading only the record id's and the dates and it doesn't take long to get this data.

Are there other options for setting the parent node for an action, I can't use a filter as the parent key depends on the record loaded (could be 'Test_2010-8' for record '1', 'Test_2010-10' for record '2' etc). Can the nodes parent be set while executing the action?

How is the 'key' generated for each node, since I have a node with the same values, although under different parent nodes. e.g.

 

<mvcSiteMapNode controller="Test" title="Test" action="Index">
	<mvcSiteMapNode action="Create" title="Create" />
</mvcSiteMapNode>
<mvcSiteMapNode controller="Test2" title="Test2" action="Index">
	<mvcSiteMapNode action="Create" title="Create" />
</mvcSiteMapNode>

Is the key created before the controller, area and action attributes are inherited from the parent nodes?

The pages that it is very slow on are those that are not in the sitemap yet (which would happen when you create a new record and go back into edit it)

Coordinator
Nov 18, 2010 at 6:08 AM

No, parent nodes can only be specified based on a string...

Can you produce a small sample that replicates your issue?

Nov 19, 2010 at 11:07 AM

In the sitemap:

<?xml version="1.0" encoding="utf-8" ?>
<mvcSiteMap xmlns="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-2.0" enableLocalization="true">
	<mvcSiteMapNode title="Home" controller="Home" action="Index" changeFrequency="Always" updatePriority="Normal">
		<mvcSiteMapNode controller="Test" title="Test" action="Index">
			<mvcSiteMapNode action="Create" title="Create" />
		</mvcSiteMapNode>
		<mvcSiteMapNode controller="Test2" title="Test2" action="Index">
			<mvcSiteMapNode action="Create" title="Create" />
		</mvcSiteMapNode>
	</mvcSiteMapNode>
</mvcSiteMap>

Master page / page:

You are here: <%= Html.MvcSiteMap().SiteMapPath() %>

Controller (nothing special, just a regular controller):

public class TestController : Controller
{
	public ActionResult Index()
	{
		return View();
	}
	public ActionResult Create()
	{
		return View();
	}
}

Go to /Test2/Create. Page shows, but nothing in breadcrumb trail. Fixing that may fix the slow loading, although I have yet to replicate this. Sometimes all the dynamic nodes are not created and it just stops (maybe timing out?). I can debug it easily enough on my development box, but not on the live server. 1000 records shouldn't be a problem though should it (the data is loaded through a helper), resulting in 3000+ site nodes (and likely to increase by a few hundred a month)? Model is LINQ to SQL generated.

public static Dictionary<int, DateTime?> DateAndId(this Table<MyTable> table)
{
	string cacheKey = "MyTableDateAndId";
	var cachedData = HttpContext.Current.Cache[cacheKey] as Dictionary<int, DateTime?>;
	Dictionary<int, DateTime?> output;
	if (cachedData == null)
	{
		output = table.Select( t => new { t.ID, t.DateCreated } ).ToDictionary( t => t.ID, t => t.DateCreated);
		// add to cache
		HttpContext.Current.Cache.Insert(cacheKey, output, null, DateTime.Now.AddMinutes(5), Cache.NoSlidingExpiration);
	}
	else
	{
		output = cachedData;
	}
	return output;
}

Dynamic node provider:

MyDBDataContext db;

public override IEnumerable<DynamicNode> GetDynamicNodeCollection()
{
	HttpContext.Current.Trace.Write("DetailsDynamicNodeProvider", "Start");
	db = new MyDBDataContext();
	var records = db.MyTable.DateAndId();
	if (records != null)
	{
		DateTime created;
		// Create a node for each record
		foreach (var record in records)
		{
			if (record.Value.HasValue)
			{
				created = record.Value.Value;
				DynamicNode node = new DynamicNode("MyTable_Details_" + record.Key, "MyTable_" + created.ToString("yyyy-M"), "Details", "");
				node.RouteValues["id"] = record.Key;
				yield return node;
			}
		}
	}
	HttpContext.Current.Trace.Write("DetailsDynamicNodeProvider", "End");
}

Nov 22, 2010 at 1:07 PM

Relating to performance, it seems breadcrumbs generation Html.MvcSiteMap().SiteMapPath() is ok, but the menu Html.MvcSiteMap().Menu() isn't, regardless of whether I use my template or a built in one. Probably loading all the nodes (3600+), even though I only want the top level ones, hence the slowdown.

Coordinator
Nov 22, 2010 at 1:17 PM

I'll look into it, thanks.