1

I've been messing with MVC SiteMapProvider for a little while and love it. I'm building an ecommerce site and it has worked really well so far during my development.

The one issue I can't seem to wrap my head around is how to get dynamicNode to work on first level.

Something like this:

www.mysite.com/{type}/{category}/{filter}

There are only 3 types so for now I just have 3 controllers named after the type and they all use the same logic and viewModels which is not an ideal set up for maintainability down the line. My routeConfig includes 3 routes like this.

routes.MapRoute(
            name: "Hardscape",
            url: "hardscape-products/{category}/{filter}",
            defaults: new { controller = "Products", action = "Index", category = UrlParameter.Optional, filter = UrlParameter.Optional},
            namespaces: new[] { "MyApp.Web.Controllers" }
        );

routes.MapRoute(
            name: "Masonry",
            url: "masonry-products/{category}/{filter}",
            defaults: new { controller = "Products", action = "Index", category = UrlParameter.Optional, filter = UrlParameter.Optional},
            namespaces: new[] { "MyApp.Web.Controllers" }
        );

routes.MapRoute(
            name: "Landscape",
            url: "landscape-products/{category}/{filter}",
            defaults: new { controller = "Products", action = "Index", category = UrlParameter.Optional, filter = UrlParameter.Optional},
            namespaces: new[] { "MyApp.Web.Controllers" }
        );

I've tried something like this but it returns 404.

   routes.MapRoute(
            name: "Products",
            url: "{productType}/{category}/{filter}",
            defaults: new { controller = "Products", action = "Index", productType = UrlParameter.Optional,  category = UrlParameter.Optional, filter = UrlParameter.Optional},
            namespaces: new[] { "MyApp.Web.Controllers" }
        );

I've been able to generate my nodes in the sitemap and menu using dynamicNode for my category and filter parameter. Just having trouble with first level when I'm not naming the first level statically

masonry-products/ vs. {productType}/

Please let me know if you have a solution. Hopefully NightOwl can chime in.

Cœur
  • 37,241
  • 25
  • 195
  • 267
SeanG80
  • 159
  • 1
  • 1
  • 10
  • Please remove "Solved" from your title and the solution from your question. If your own solution differs from the accepted answer, you are free to add it as a separate answer. – Jongware Nov 08 '15 at 00:55
  • 1
    Thank you for the suggestion. – SeanG80 Nov 08 '15 at 01:08

2 Answers2

1

The routing framework of .NET is very flexible.

For this situation, you could just use a constraint for the types. There are 2 ways:

  1. Use a RegEx.
  2. Implement a custom class.

The first option wouldn't be so bad if you aren't expecting a lot of changes:

routes.MapRoute(
    name: "Products",
    url: "{productType}/{category}/{filter}",
    defaults: new { controller = "Products", action = "Index", category = UrlParameter.Optional, filter = UrlParameter.Optional},
    constraints: new { productType = @"hardscape-products|masonry-products|landscape-products" },
    namespaces: new[] { "MyApp.Web.Controllers" }
);

The second option is more dynamic:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Routing;

public class ProductTypeConstraint : IRouteConstraint
{
    private object synclock = new object();

    public bool Match
        (
            HttpContextBase httpContext,
            Route route,
            string parameterName,
            RouteValueDictionary values,
            RouteDirection routeDirection
        )
    {
        return GetProductTypes(httpContext).Contains(values[parameterName]);
    }

    private IEnumerable<string> GetProductTypes(HttpContextBase httpContext)
    {
        string key = "ProductTypeConstraint_GetProductTypes";
        var productTypes = httpContext.Cache[key];
        if (productTypes == null)
        {
            lock (synclock)
            {
                productTypes = httpContext.Cache[key];
                if (productTypes == null)
                {
                    // TODO: Retrieve the list of Product types from the 
                    // database or configuration file here.
                    productTypes = new List<string>()
                    {
                        "hardscape-products",
                        "masonry-products",
                        "landscape-products"
                    };

                    httpContext.Cache.Insert(
                        key: key,
                        value: productTypes,
                        dependencies: null,
                        absoluteExpiration: System.Web.Caching.Cache.NoAbsoluteExpiration,
                        slidingExpiration: TimeSpan.FromMinutes(15),
                        priority: System.Web.Caching.CacheItemPriority.NotRemovable,
                        onRemoveCallback: null);
                }
            }
        }

        return (IEnumerable<string>)productTypes;
    }
}

Caching is necessary here because constraints are hit on every request.

routes.MapRoute(
    name: "Products",
    url: "{productType}/{category}/{filter}",
    defaults: new { controller = "Products", action = "Index", category = UrlParameter.Optional, filter = UrlParameter.Optional},
    constraints: new { productType = new ProductTypeConstraint() },
    namespaces: new[] { "MyApp.Web.Controllers" }
);

Of course, that is not the only dynamic option. If you really need to just pick any URL of your choosing, like in a CMS, you can inherit RouteBase and drive all of your URLs from the database.

Not sure what this question has to do with dynamic node provider is, though. Nor do I understand what is meant by "first level".

The only thing you really need to do with the dynamic node provider is match the same route values you have in your routes and to provide a key-parent key relationship. There must be a parent key defined in either XML or as a .NET attribute to attach the top level node(s) from the provider on.

Routing

dynamicNode.Controller = "Product";
dynamicNode.Action = "Index";
dynamicNode.RouteValues.Add("productType", "hardscape-products");
dynamicNode.RouteValues.Add("category", "some-category");
dynamicNode.RouteValues.Add("filter", "some-filter");

OR

dynamicNode.Controller = "Product";
dynamicNode.Action = "Index";
dynamicNode.PreservedRouteParameters = new string[] { "productType", "category", "filter" };

OR

Some combination of route values and preserved route parameters that makes sense for your application.

For an explanation of these options, read How to Make MvcSiteMapProvider Remember a User's Position.

Key Matching

// This assumes you have explicitly set a key to "Home"
// in a node outside of the dynamic node provider.
dynamicNode.ParentKey = "Home";
dynamicNode.Key = "Product1";

// This node has the node declared above
// as its parent.
dynamicNode.ParentKey = "Product1";
dynamicNode.Key = "Product1Details";
Community
  • 1
  • 1
NightOwl888
  • 55,572
  • 24
  • 139
  • 212
  • Thank you for the quick response. I really appreciate your help. You're right "first level" was too vague of a description. I was thinking about the problem visually based on the menu tree. The solution ended up being the method you provided with fixed constraint values. In my previous attempts I was only attempting to constrain the string to a minimum number of characters. I have provided a sample of my final solution which includes my dynamic nodes. I will probably add your other suggestions in the future. Thanks again!! – SeanG80 Oct 16 '15 at 21:22
0

Solution by OP.

Big thank you to NightOwl888 for a detailed answer which helped me solve this. I had previously followed an MSDN tutorial here which I think confused me regarding the use of constraints.

To summarize, I didn't define my constraint correctly which caused the 404 and all of my other issues with MVCSiteMapProvider. Here's a sample of the working solution.

Route

routes.MapRoute(
         name: "Products",
         url: "{productType}/{category}/{filter}/{filterAction}/{filterId}",
         defaults: new { controller = "Products", action = "Index", productType = UrlParameter.Optional, category = UrlParameter.Optional, filter = UrlParameter.Optional, filterAction = UrlParameter.Optional, filterId = UrlParameter.Optional },
         constraints: new { productType = @"building-products|installation-materials|tools" },
        namespaces: new[] { "MyApp.Web.Controllers" }
        );

XML

<mvcSiteMapNode title="Product Type" dynamicNodeProvider="MyApp.Web.SiteMapProviders.ProductTypeSiteMapProvider, MyApp.Web">
  <mvcSiteMapNode title="Category" dynamicNodeProvider="MyApp.Web.SiteMapProviders.CategorySiteMapProvider, MyApp.Web">
    <mvcSiteMapNode title="Option" dynamicNodeProvider="MyApp.Web.SiteMapProviders.OptionSiteMapProvider, MyApp.Web" />
    <mvcSiteMapNode title="Association" dynamicNodeProvider="MyApp.Web.SiteMapProviders.AssociationSiteMapProvider, MyApp.Web" />
  </mvcSiteMapNode>
</mvcSiteMapNode>

The first 2 of 4 DynamicNodes to give you an idea

public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
    {
        using (var db = new ProductContext())
        {              
            foreach (var productType in db.ProductTypes.ToList())
            {
                DynamicNode dynamicNode = new DynamicNode();
                dynamicNode.Key = productType.Name.ToLower().Replace(" ", "-");
                dynamicNode.Title = productType.Name;
                dynamicNode.Clickable = false;
                yield return dynamicNode;
            }
        }
    }

public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
    {
        using (var db = new ProductContext())
        {              
            foreach (var category in db.Categories.ToList())
            {
                DynamicNode dynamicNode = new DynamicNode();
                dynamicNode.Key = category.Name.Replace(" ", "");
                dynamicNode.Title = category.Name;
                dynamicNode.Controller = "Products";
                dynamicNode.Action = "Index";
                dynamicNode.ParentKey = category.ProductType.Name.ToLower().Replace(" ", "-");
                dynamicNode.RouteValues.Add("productType", category.ProductType.Name.ToLower().Replace(" ", "-"));
                dynamicNode.RouteValues.Add("category", category.Name.ToLower().Replace(" ", "-"));
                dynamicNode.ImageUrl = category.CategoryImage();
                yield return dynamicNode;
            }
        }
    }
Cœur
  • 37,241
  • 25
  • 195
  • 267