MvcSiteMapProvider ISiteMapBuilder in conjunction

2020-02-13 05:50发布

问题:

I'm using MvcSiteMapProvider 4.4.3 to dynamically build a sitemap from the database. I'm following this article: https://github.com/maartenba/MvcSiteMapProvider/wiki/Multiple-Sitemaps-in-One-Application because I'm using multiple sitemaps.

This works and this is the basic structure which is returned:

  • Home
    • News
    • Products
    • About
    • Contact

One of the nodes (/Products) should be dynamically populated again based on different data. So for this I need a IDynamicNodeProvider implementation on the /Productsnode? (please correct me if i'm wrong?)

Anyway, I think I do need the above. Documentation shows ways to do this on a node defined in XML and on a node defined using attributes on controller actions, but not 'manually' in a ISiteMapBuilder. So if I set the .DynamicNodeProvider property of the ISiteMapNode instance it doesn't seem to get instantiated... The .HasDynamicNodeProvider property also returns false.

Looking at the source, i see PluginProvider-stuff which is related to DynamicNodeProviderStrategy and there you go, they've lost me...

How do I create a ISiteMapNode for "/Products" in my ISiteMapBuilder so that it's descendents (/Products/Cat and /Products/Cat/Product) are dynamically loaded from the database?

回答1:

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

  1. The nodes are properly mapped to their parent nodes
  2. There is only 1 root node
  3. All nodes are added to the SiteMap
  4. 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.