Skip to content

Defining Sitemap Nodes using IDynamicNodeProvider

Shad Storhaug edited this page Feb 10, 2014 · 1 revision

In many web applications, sitemap nodes are directly related to content in a persistent store like a database. For example, in an e-commerce application, a list of product details pages in the sitemap maps directly to the list of products in the database. Using dynamic sitemaps, a small class can be provided to the MvcSiteMapProvider offering a list of dynamic nodes that should be included in the sitemap. This ensures the product pages do not have to be specified by hand in the sitemap XML or by using .NET attributes.

First of all, a sitemap node should be defined in XML or in attributes. This node will serve as a template and tell the MvcSiteMapProvider infrastructure to use a custom dynamic node provider.

Add dynamicNodeProvider attribute to your newly created sitemap node in the following format: Fully.Qualified.Class.Name, AssemblyName. Note that the dynamicNodeProvider is ignored in the root node.

<mvcSiteMapNode title="Details" action="Details" dynamicNodeProvider="MvcMusicStore.Code.StoreDetailsDynamicNodeProvider, MvcMusicStore" />

Alternatively, you can use a MvcSiteMapNodeAttribute to attach the provider:

[MvcSiteMapNodeAttribute(Title = "Details", ParentKey = "HomeKey", DynamicNodeProvider = "MvcMusicStore.Code.StoreDetailsDynamicNodeProvider, MvcMusicStore")]
public ActionResult Details(int id)
{
	var album = storeDB.Albums
		.Single(a => a.AlbumId == id);

	return View(album);
}

Next, a class implementing MvcSiteMapProvider.IDynamicNodeProvider or extending MvcSiteMapProvider.DynamicNodeProviderBase should be created in your application code. Here’s an example:

public class StoreDetailsDynamicNodeProvider 
    : DynamicNodeProviderBase 
{ 
    public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node) 
    {
        using (var storeDB = new MusicStoreEntities())
        {
            // Create a node for each album 
            foreach (var album in storeDB.Albums.Include("Genre")) 
            { 
                DynamicNode dynamicNode = new DynamicNode(); 
                dynamicNode.Title = album.Title; 
                dynamicNode.ParentKey = "Genre_" + album.Genre.Name; 
                dynamicNode.RouteValues.Add("id", album.AlbumId);

                yield return dynamicNode;
            }
        }
    } 
}

Note: If using an internal DI container, the instances are created dynamically using reflection based on the strings provided. If using an external DI container, the assemblies are automatically scanned for any implementations that are assignable from IDynamicNodeProvider. In both cases, a single instance of each implementation is instantiated regardless of how many nodes are configured. However, when using the internal DI container, you must ensure your dynamic node provider has a public default constructor.

Caching

When providing dynamic sitemap nodes to the MvcSiteMapProvider, chances are that the hierarchy of nodes will become stale, for example when adding products in an e-commerce website. You can fix this by adding a SiteMapCacheReleaseAttribute to the action methods that change the data.

[HttpPost]
[SiteMapCacheRelease]
public ActionResult Edit(int id, Product product)
{
    try
    {
        using (var db = new CRUDExample())
        {
            var model = (from p in db.Product
                     where p.Id == id
                     select p).FirstOrDefault();
            if (model != null)
            {
                model.Name = product.Name;
                db.SaveChanges();
            }
        }
        return RedirectToAction("Index");
    }
    catch
    {
        return View();
    }
}

You can also pass in the SiteMapCacheKey as a parameter to clear a specific sitemap if your administration pages are using a different one than the one you need to target. You can think of the SiteMapCacheKey as the name of a specific sitemap instance.

[HttpPost]
[SiteMapCacheRelease(SiteMapCacheKey="MyOtherSiteMap")]
public ActionResult Edit(int id, Product product)
{
   // Implementation omitted...
}

Alternatively, you may call SiteMaps.ReleaseSiteMap() from within your controller action. It also has an overload that accepts a SiteMapCacheKey.

SiteMaps.ReleaseSiteMap();
SiteMaps.ReleaseSiteMap("MyOtherSiteMap");

Both of the above methods will force the next client request to immediately build a new sitemap based on the changed data rather than waiting for the cache to time out.

Scenario: creating nested nodes

It often happens that in a sitemap, you want to nest different nodes. For example when building a webshop, a list of categories may be created dynamically under which items sold are added as well.

We can model this in our sitemap, adding both node "templates" and specifying their IDynamicNodeProvider:

<mvcSiteMapNode title="Browse" action="Browse" dynamicNodeProvider="MvcMusicStore.Code.StoreBrowseDynamicNodeProvider, Mvc Music Store" preservedRouteParameters="browse">
    <mvcSiteMapNode title="Details" action="Details" dynamicNodeProvider="MvcMusicStore.Code.StoreDetailsDynamicNodeProvider, Mvc Music Store" />
</mvcSiteMapNode>

Doing this will trigger the following behaviour in the MvcSiteMapProvider:

  • Clone the "Browse" node for every dynamic node created by the StoreBrowseDynamicNodeProvider
  • Clone the "Details" node for every dynamic node created by the StoreDetailsDynamicNodeProvider

Note: MvcSiteMapProvider doesn't enforce a relationship between these nodes. You will have to do this manually by specifying Key and ParentKey properties. Here's how you can create the parent level (note the assignment of the Key property):

public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node) 
{ 
    // ...

    // Create a node for each genre
    foreach (var genre in storeDB.Genres)
    {
        DynamicNode dynamicNode = new DynamicNode("Genre_" + genre.Name, genre.Name);
        dynamicNode.RouteValues.Add("genre", genre.Name);

        yield return dynamicNode; 
    }

    // ...
} 

The item level can be created as follows (note the assignment of the ParentKey property in which we are manually constructing the relationship):

public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node) 
{ 
    // ...

    // Create a node for each album
    foreach (var album in storeDB.Albums.Include("Genre"))
    {
        DynamicNode dynamicNode = new DynamicNode();
        dynamicNode.Title = album.Title;
        dynamicNode.ParentKey = "Genre_" + album.Genre.Name;
        dynamicNode.RouteValues.Add("id", album.AlbumId);

        yield return dynamicNode; 
    }

    // ...
} 

In the example, note that we are not filtering the data and we are using the referential integrity of the database to ensure that each Album has a Genre. If your data source does not enforce referential integrity, you may need to filter out any orphaned records yourself.

Also note that we are only calling the database 2 times to fetch all of the data, because the Key - ParentKey mapping we are doing will automatically put the nodes in the right place.

Adding non-dynamic children to dynamic nodes

It may happen that we want to add non-dynamic nodes underneath dynamically generated nodes. For example we may want to add a "details" node to every dynamic node we create.

We can model this in our sitemap:

<mvcSiteMapNode title="Browse" action="Browse" dynamicNodeProvider="MvcMusicStore.Code.StoreBrowseDynamicNodeProvider, Mvc Music Store" preservedRouteParameters="browse">
    <mvcSiteMapNode title="Details" action="Details" />
</mvcSiteMapNode>

Doing this will trigger the following behavior in the MvcSiteMapProvider:

  • Clone the "Browse" node for every dynamic node created by the StoreBrowseDynamicNodeProvider
  • Clone the "Details" node for every "Browse" node created by the StoreBrowseDynamicNodeProvider and add it as a child
  • Optionally clone children of "Details" for every "Browse" node created by the StoreBrowseDynamicNodeProvider and add the tree recursively

Parent/child relationships will be managed by the MvcSiteMapProvider.

Tip: If you create a custom view an place the @Html.MvcSiteMap().SiteMap() Html helper extension on the view, you can use it as a diagnostic tool to determine if your nodes are nested properly. If the keys are not specified correctly, the nodes won't appear in the sitemap.


Want to contribute? See our Contributing to MvcSiteMapProvider guide.



Version 3.x Documentation


Unofficial Documentation and Resources

Other places around the web have some documentation that is helpful for getting started and finding answers that are not found here.

Tutorials and Demos

Version 4.x
Version 3.x

Forums and Q & A Sites

Other Blog Posts

Clone this wiki locally