You can do this with ISiteMapBuilder, but you are probably better off instead implementing ISiteMapNodeProvider. The reason is because adding the nodes to the SiteMap must be done at the very end of the process after all nodes have been instantiated to ensure that every node is correctly mapped to a parent node (except of course, the root node which doesn't need a parent). This was the major design change that was done in 4.3.0.
The default SiteMapBuilder class is now already set up to ensure
- The nodes are properly mapped to their parent nodes
- There is only 1 root node
- All nodes are added to the SiteMap
- The visitors are executed last after the SiteMap is completely built
It dosen't make sense to add more than one ISiteMapBuilder instance because this makes it possible to circumvent this important logic. Therefore, it is best if you do not implement ISiteMapBuilder, but instead implement ISiteMapNodeProvider.
The SiteMapBuilder class takes an ISiteMapNodeProvider as a dependency through its constructor. You can use the CompositeSiteMapNodeProvider class to handle multiplicity on this interface so you can add more than one ISiteMapNodeProvider implementation, if needed.
The ISiteMapNodeProvider interface looks like this:
public interface ISiteMapNodeProvider
{
IEnumerable<ISiteMapNodeToParentRelation> GetSiteMapNodes(ISiteMapNodeHelper helper);
}
There is just 1 method to implement. In addition, many of the common (but optional) services are injected through the interface automatically from the SiteMapBuilder class through ISiteMapNodeHelper.
This class is at a lower level than IDynamicNodeProvider. You are interacting with ISiteMapNode directly, but all interaction with the SiteMap class is handled by SiteMapBuilder. The ISiteMapNode is wrapped in a ISiteMapNodeToParentRelation instance, which is just there to ensure that its parent node key can be tracked until the time it is added to the SiteMap object.
Your SiteMapNodeProvider should look something like this:
public class CustomSiteMapNodeProvider
: ISiteMapNodeProvider
{
private readonly string sourceName = "CustomSiteMapNodeProvider";
#region ISiteMapNodeProvider Members
public IEnumerable<ISiteMapNodeToParentRelation> GetSiteMapNodes(ISiteMapNodeHelper helper)
{
var result = new List<ISiteMapNodeToParentRelation>();
using (var db = new DatabaseContextClass())
{
foreach (var category in db.Categories.ToList())
{
var categoryRelation = this.GetCategoryRelation("Products", category, helper);
result.Add(categoryRelation);
}
foreach (var product in db.Products.Include("Category"))
{
var productRelation = this.GetProductRelation("Category_" + product.CategoryId, product, helper);
result.Add(productRelation);
}
}
return result;
}
#endregion
protected virtual ISiteMapNodeToParentRelation GetCategoryRelation(string parentKey, Category category, ISiteMapNodeHelper helper)
{
string key = "Category_" + category.Id;
var result = helper.CreateNode(key, parentKey, this.sourceName);
var node = result.Node;
node.Title = category.Name;
// Populate other node properties here
// Important - always set up your routes (including any custom params)
node.Area = "MyArea"; // Required - set to empty string if not using areas
node.Controller = "Category"; // Required
node.Action = "Index"; // Required
node.RouteValues.Add("id", category.Id.ToString());
return result;
}
protected virtual ISiteMapNodeToParentRelation GetProductRelation(string parentKey, Product product, ISiteMapNodeHelper helper)
{
string key = "Product_" + product.Id;
var result = helper.CreateNode(key, parentKey, this.sourceName);
var node = result.Node;
node.Title = product.Name;
// Populate other node properties here
// Important - always set up your routes (including any custom params)
node.Area = "MyArea"; // Required - set to empty string if not using areas
node.Controller = "Product"; // Required
node.Action = "Index"; // Required
node.RouteValues.Add("id", product.Id.ToString());
node.RouteValues.Add("categoryId", product.CategoryId.ToString()); // Optional - use if you have a many-to-many relationship.
return result;
}
}
The above example assumes that you have added a node by other means that has the key set to "Products", which all categories will be children of. You can of course adjust this to meet your needs.
It is typically best if you only implement this interface 1 time and use a single database connection to load the entire SiteMap. You can always refactor this into multiple classes that each handle a single table on your side of the interface to separate concerns. But it is generally best if you put all of the key mapping logic between related entities together to make it easier to maintain.
For additional examples of implementations of this interface, see XmlSiteMapNodeProvider and ReflectionSiteMapNodeProvider.