I am running into a problem with my routes in MVC4.
I have a few actions that live outside of a specific product and many more that live within the user chosen product. In order to accommodate the actions I have mapped two routes
context.MapRoute(
"CMS_product",
"CMS/{productId}/{controller}/{action}/{id}",
new { controller = MVC.CMS.Home.Name, action = MVC.CMS.Home.ActionNames.Index, productId = default(Guid).ToString(), id = UrlParameter.Optional },
new string[] { "Areas.CMS.Controllers" }
);
context.MapRoute(
"CMS_default",
"CMS/{controller}/{action}/{id}",
new { controller = MVC.CMS.Home.Name, action = MVC.CMS.Home.ActionNames.Index, id = UrlParameter.Optional },
new string[] { "Areas.CMS.Controllers" }
);
So while this works in a generic since, none of my routes will match the default route any longer and instead of getting a URL like
~/CMS/Product/List
When operating outside of a product I get urls like this.
~/CMS/00000000-0000-0000-0000-000000000000/Product/List
Another note: I have tried to hard code the Prodcut/List in as a route, and have placed it before CMS_product in the hopes that it would match prior to the other url. I feel like I must be overlooking something simple.
For completeness, should anyone else run into a similar issue here is the solution.
// used to match ~/CMS/00000000-0000-0000-0000-000000000000/Product/List
// prevents the GUID.Empty from showing when there is no product value
// in the segment
context.MapRoute(
name: "CMS_nullproduct",
url: "CMS/{controller}/{action}/{id}",
defaults: new { controller = MVC.CMS.Home.Name, action = MVC.CMS.Home.ActionNames.Index, id = UrlParameter.Optional },
constraints: new { productId = Guid.Empty.ToString() },
namespaces: new string[] { "Areas.CMS.Controllers" }
);
// matches any route with a productId segment value of anything aside from
// GUID.Empty
context.MapRoute(
name: "CMS_product",
url: "CMS/{productId}/{controller}/{action}/{id}",
defaults: new { controller = MVC.CMS.Home.Name, action = MVC.CMS.Home.ActionNames.Index, id = UrlParameter.Optional },
constraints: new { productId = #"^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$" },
namespaces: new string[] { "Areas.CMS.Controllers" }
);
context.MapRoute(
name: "CMS_default",
url: "CMS/{controller}/{action}/{id}",
defaults: new { controller = MVC.CMS.Home.Name, action = MVC.CMS.Home.ActionNames.Index, id = UrlParameter.Optional },
namespaces: new string[] { "Areas.CMS.Controllers" }
);
In my opinion you should remove default value for productId.
context.MapRoute(
"CMS_product",
"CMS/{productId}/{controller}/{action}/{id}",
new { controller = MVC.CMS.Home.Name, action = MVC.CMS.Home.ActionNames.Index, id = UrlParameter.Optional },
new string[] { "Areas.CMS.Controllers" }
);
If you not provide productId you routing engine should match second route and generate ~/CMS/Product/List but if you provide productId it match first rule.
Additionaly you can write custom IRouteConstraint or use regex to limit productId values.
context.MapRoute(
"CMS_product",
"CMS/{productId}/{controller}/{action}/{id}",
new { controller = MVC.CMS.Home.Name, action = MVC.CMS.Home.ActionNames.Index, id = UrlParameter.Optional },
new { productId = #"^\d{8}\-\d{4}\-\d{4}\-\d{4}\-\d{12}$" }
new string[] { "Areas.CMS.Controllers" }
);
Related
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.
The routing framework of .NET is very flexible.
For this situation, you could just use a constraint for the types. There are 2 ways:
Use a RegEx.
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";
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;
}
}
}
How would I create a route mapping to the following url:
http://localhost/SiteName/AdminCP/Topics/EditTopic/28
AdminCP is an area -- I can get to the Topics controller and show a list of topics to the user, then the user clicks a link to edit the topic which should
take them to the url above. EditTopic is a controller in AdminCP which returns a single Index action.
This is my AdminCP registration code which is not working.
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"AdminCP_default",
"AdminCP/{controller}/{action}/{id}",
new { controller= "Home", action = "Index", id = UrlParameter.Optional }
);
context.MapRoute(
"AdminCP_Topics",
"AdminCP/Topics/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
I have been able to reproduce this URL:
http://localhost/SiteName/AdminCP/EditTopic/Index/26
using this code snippet
#Html.ActionLink("Edit Options", "", "EditTopic", new { id = item.CategoryId }, new { #class = "popup-link" })
...but that's not exactly what I want.
Try this (Add before the AdminCP Default Route)
context.MapRoute(
"AdminCP_Topics",
url: "AdminCP/Topics/{controller}/{id}",
defaults: new { action = "Index", id = UrlParameter.Optional }
);
And
#Html.ActionLink("Edit Options","Index","EditTopic", new {id = 1, area = "AdminCP" },null)
I created this route
routes.MapRoute(
name: "Survey",
url: "{controller}/{action}/{surveyid}/{userid}/{hash}",
defaults: new { controller = "Home", action = "Survey" },
constraints: new { surveyid = #"\d+", userid = #"\d+" }
);
When I then browse to
http://localhost:3086/Home/Survey/1/1/3r2ytg
It works, however if I browse to
http://localhost:3086/1/1/3r2ytg
it does not work.
If I then changed the route like so
routes.MapRoute(
name: "Survey",
url: "{surveyid}/{userid}/{hash}",
defaults: new { controller = "Home", action = "Survey" },
constraints: new { surveyid = #"\d+", userid = #"\d+" }
);
The exact opposite would work (and that makes sense).
But I am curious with the first route i thought both URLs should work since it shoudl grab the default controller and action when none is given.
Update
In the end I went with only this
routes.MapRoute(
name: "Survey",
url: "{surveyId}/{userId}/{hash}",
defaults: new { controller = "Home", action = "Survey" },
constraints: new { surveyId = #"\d+", userId = #"\d+" }
);
as that is the behavior I wanted. However when I then call
#Url.Action("Survey", "Home", new
{
userId = #Model.UserId,
surveyId = survey.Id,
hash = HashHelpers.CreateShortenedUrlSafeHash(#Model.SecretString + survey.Id.ToString() + #Model.UserId.ToString())
})
It generates
/Admin/Home/Survey?userId=25&surveyId=19&hash=2Norc
instead of a shiny path. I can force it with Url.RouteUrl but I thought it should have picked this one automatically.
You need to create route for each combination.
Check this Phil Haack Article
routes.MapRoute(
name: "Survey",
url: "{controller}/{action}/{surveyid}/{userid}/{hash}",
defaults: new { controller = "Home", action = "Survey" },
constraints: new { surveyid = #"\d+", userid = #"\d+" }
);
routes.MapRoute(
name: "Survey",
url: "{surveyid}/{userid}/{hash}",
defaults: new { controller = "Home", action = "Survey" },
constraints: new { surveyid = #"\d+", userid = #"\d+" }
);
Check this Route with Two optional parameters in MVC3 not working
The routehandler doesn't really know that if you say /1/1/3r2ytg the 1 is for surveyId, the other 1 is for userId etc.
It just knows a path (localhost:3086) and x amount of "folders"
So if you call http://localhost:3086/1/1/3r2ytg he will map 1 to controller, 1 to action and 3r2ytg to surveyId.
It can't find userId or hash and since there are no defaults specified he can't find the route.
The defaults in your first route are pointless since they will never trigger.
Default values should be at the end of your url, which kinda makes sense.
I have this route map defined.
routes.MapRoute("default", // route name
"{controller}/{action}/{id}", // url with parameters
new { controller = "home", action = "index", id = UrlParameter.Optional }, // parameter defaults
new string[] { "mobilesurveys.mt.controllers" }
);
This will work perfectly. now I want to add another routemap
routes.MapRoute("couponreedem", // route name
"{controller}/{action}/{clientname}", // url with parameters
new { controller = "Rc", action = "index", id = UrlParameter.Optional }, // parameter defaults
new string[] { "mobilesurveys.mt.controllers" }
);
i have defined like this. Here Rc is my controller. and I am giving the url as
.com /Rc/Rc/sammy
and method in the controller defined as
public ActionResult Rc(string clientname)
{
viewModel =dataRc.ProductCategoryGet();
return View(viewModel);
}
clientname will be always null. How to add another route while the existing route not be disturbed.
Thanks.
It actually looks identical. But in case you want a new one you can try something like this, and it should be above the default one.
routes.MapRoute("couponreedem", // route name
"RC/{action}/{clientname}", // url with parameters
new { controller = "Rc", action = "index", clientname = UrlParameter.Optional }, // parameter defaults
new string[] { "mobilesurveys.mt.controllers" }
);
That will fix the route with RC/...
Also your action should be named Index
public ActionResult Index (string clientname)
{
viewModel =dataRc.ProductCategoryGet();
return View(viewModel);
}
Im trying to do something like this:
routes.MapRoute("Product", "{product}/{id}",
new
{
action = "Product",
controller = "Home",
product = UrlParameter.Optional,
id = UrlParameter.Optional
});
It gives me error when im trying to load page 404 i think,
Im trying to make the url look like this: www.tables.com/productName/ID .
How can i do it without adding a strong type word like this:
routes.MapRoute("Product", "Products/{product}/{id}", ... )
rest of the routes:
routes.MapRoute("Product", "{product}/{id}",
new
{
action = "Product",
controller = "Home",
product = UrlParameter.Optional,
id = UrlParameter.Optional
});
routes.MapRoute("Category", "Category/{category}/{template}",
new
{
action = "Index",
controller = "Category",
category = UrlParameter.Optional,
template = UrlParameter.Optional
});
routes.MapRoute("Profile", "Profile/{fullName}",
new
{
action = "Index",
controller = "Profile",
fullName = UrlParameter.Optional
});
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
thanks.
Your problem is that the Product route will match everything not starting with Category or Profile.
I would place the product route just before the default route and use a IRouteConstraint such that it doesn't match non products.
Code sample:
routes.MapRoute("Category", "Category/{category}/{template}",
new
{
action = "Index",
controller = "Category",
category = UrlParameter.Optional,
template = UrlParameter.Optional
});
routes.MapRoute("Profile", "Profile/{fullName}",
new
{
action = "Index",
controller = "Profile",
fullName = UrlParameter.Optional
});
routes.MapRoute("Product", "{product}/{id}",
new
{
action = "Product",
controller = "Home",
product = UrlParameter.Optional,
id = UrlParameter.Optional
},
new { product = new ProductRouteConstraint() });
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
And the route constraint:
public class ProductRouteConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
if (routeDirection == RouteDirection.IncomingRequest &&
parameterName.ToLowerInvariant() == "product")
{
var productName = values[parameterName] as string;
if (productName == null)
return false;
var productId = values["id"] as string;
if (productId == null)
returns false;
return ProductCatalogue.HasProductById(productId);
}
return false;
}
}
The ProductCatalogue should obviously be replaced with however you lookup products in your system.