asp.net-mvcasp.net-mvc-5mvcsitemapprovider

Can MVC SiteMapProvider dynamicNode be used on 1st Level?


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.


Solution

  • 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";