MVC3 routing with an optional first parameter - asp.net-mvc-3

My site is currently set up with the following routes:
routes.MapRoute(
"BrandList",
"Brands",
new { controller = "Brand", action = "Index" }
);
routes.MapRoute(
"BrandProducts",
"{brand}/Products",
new { controller = "Brand", action = "Products", manufacturer = "" },
new { manufacturer = new BrandConstraint() }
);
routes.MapRoute(
"Product",
"{brand}/{partNumber}",
new { controller = "Product", action = "Details", brand = "" },
new { manufacturer = new BrandConstraint() }
);
This produces URLs like
http://oursite/Brands -> List of all brands
http://oursite/SomeBrand -> List of one brand's products
http://oursite/SomeBrand/ProductA -> Details for product
I just got the directive, however, that we now need to serve up those same pages on
http://oursite/Brands -> List of all brands
http://oursite/SomeBrand -> List of one brand's products
http://oursite/Brands/SomeBrand -> List of one brand's products
http://oursite/SomeBrand/ProductA -> Details for product
http://oursite/Brands/SomeBrand/ProductA -> Details for product
I know I can create two more Routes, identical to the current BrandProducts and Product routes, with the extra "Brands/" at the beginning. I'll do that if I need to, but I'd really much prefer to not have to duplicate each route entry (there's more than just these two).
Anyone have suggestions?

You may want to just try using Url Rewrites instead of adding the complexity of additional routes pointing to the same place.
Also its not good to have multiple canonical formats of URL's that are valid, one should really 301 to the "correct" url.

Related

MVC Ecommerce routing

I am trying to get my head around routing for MVC4
I want to create a URL structure consisting of BrandName / ProductType / PageNumber
Sometimes it could just have Brand or just product type deppending on how you filter.
e.g.
Store/{BrandName}//{PaginationId} this is unique
Store/{ProductType}/{PaginationId} this is unique
Store/{BrandName}/{ProductType}/{PaginationId}
Store/{ProductType}/BrandName}/{PaginationId}
Any help?
thanks
You must register the following routes:
// 1: Store/ProductType/BrandName/PaginationId
// (all parts must exists in the URL)
routes.MapRoute("ProductType", "Store/{productType}/{brandName}/{paginationId}",
new { controller = "Store", action = "Index" },
new { /* constraints */ });
// 2: Store/BrandName/ProductType/PaginationId
// 3: Store/BrandName/ProductType
// 4: Store/BrandName
// (both productType and paginationId can be missing)
routes.MapRoute("BrandProduct", "Store/{brandName}/{productType}/{paginationId}",
new { controller = "Store", action = "Index",
productType = UrlParameter.Optional,
paginationId = UrlParameter.Optional},
new { /* constraints */ });
// 5: Store/ProductType/PaginationId
// (all parts must exists in the URL)
routes.MapRoute("ProductType", "Store/{productType}/{paginationId}",
new { controller = "Store", action = "Index",
brandName = 0, paginationId = 0},
new { /* constraints */ });
// Action Index should have 3 parameters: brandName, productType and paginationId
// brandName, productType and paginationId should be nullable
// or reference type (class) to accept nulls
When an URL is received, the first route that matches it will handle it. So there must be a way to distinguish the routes. This can be done using constraints. A constraint is a regex which decides if the received value is valid for a parameter.
Suppose that in the first mapping the ProductType must be something starting with "P", you would add this constraint: new {productType="P.*"}:
If the user types this URL: /Store/P22/TheBrand/12, it will be processed by the first route
If the user types this URL: /Store/TheBrand/P22/12, it won't be processed bythe first route because of the constraint, but will be processed by the second one.
You must disambiguate routes 1 & 2, and also routes 3 & 5
If there is no regex that can do that for you, you can modify the routes with some extra chars that allow to disimbiguate them, ie, put P- and B- before product type nad brand name, like this:
// 1:
routes.MapRoute("ProductType", "Store/P-{productType}/B-{brandName}/{paginationId}",
// 2, 3, 4
routes.MapRoute("BrandProduct", "Store/B-{brandName}/P-{productType}/{paginationId}",
Remember that the routes are processed in the same order in which they are registered.
EDIT - Answer to OP comment:
If you just want a behaviour similar to ASP.NET, where a single page uses all the info, then use this mapping:
routes.MapRoute("Store", "Store", new {controller="Store",action="Index"}}
With this mapping, all the extra information will end up in the query string like this:
http://mysite/Store?ProductType=xxx&BrandName=yyy&PaginationId=23
If you don't provide some of the parameters, they will be simply omitted from the query string.
The action would look like this:
Index(string brandName, string prouctType, int? paginationId)
Note that, as all parameters are optional, they must be nullable (reference type like string or nullable value type like int?). So they'll be automatically bound from the query string, or left null if not present.
There is no reason why you must use Routing. You can use routing to get "smart" urls, but you don't need to.
Create a "main" browsing controller, and make BrandName, ProductType and PaginationId be parameters.
Result:
Store/Browse?BrandName=XX&ProductType=YY&PaginationId=ZZ
You can then overlap some URL-Rewriting logic (link to other SO answer) to achieve the required URL structure.
For cases when a parameter would be missing, I suggest setting them to a default value: for example, if you would not have ProductType, you could do this:
Store/Browse?BrandName=XX&ProductType=ALL&PaginationId=ZZ
In short: if it's missing, default its value and make it so you always have values everywhere they can be. Easier to handle and easier to understand too.

Asp.Net MVC 3 variable in url

The default MVC 3 route config is
{controller}/{action}/{id}
My NEWS application structure is like
/News/Latest10
/News/Critical/10June2013
/Entertainment/Latest10
Bold ones being controller, italics as actions, and normal text are optional params.
Now I want to add new variable, language, into the url structure.
It should be like
/en/News/Latest10
/ja/News/Critical/10June2013
/de/Entertainment/Latest10
I would like to know how to access this language variable in the controller. Is it possible?
Thanks
To meet your needs change the Route config to:
routes.MapRoute(
name: "Language",
url: "{language}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional, language="en" },
constraints: new {language=new LanguageConstraint()}
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
The two key parts are the route itself, {language}/{controller}/{action}/{id} and the constraint part, new {language=new LanguageConstraint()}.
The first part will select the {language} part as a variable (default being en for now) to the controller. The controller signature:
public ActionResult Index(string language) {
will pick up the new language variable. Since adding language to each and every controller could seem cumbersome you could create a ViewModelBase class to passed to every controller with a property that contains the language value, which every subsequent View Model class inherits from.
Without a constraint the route pattern would pick up all values in the url for the language part and writing a Regex expression to match all wanted language values would be tedious, I think it's easier to write an IRouteConstraint based class similar to the following:
public class LanguageConstraint : IRouteConstraint{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values,
RouteDirection routeDirection) {
//create accepted lanaguages collection from somewhere.
string[] languageArray = new[]{"en","jp", "de"};
string language = values["language"].ToString();
if (string.IsNullOrEmpty(language))
return false;
return languageArray.FirstOrDefault(l=>l.Equals(language,StringComparison.InvariantCultureIgnoreCase)) != null;
}
}
Simply it creates a list of known language values and check the provided language value against that list. If it doesn't exist false is returned and a 404 is thrown.

Structuring MVC Routes

I have the following url structure, and just trying to figure out the best Routes to configure.
EDIT: Added more url's
/cars/{name} (shows general information about a car)
/cars/{name}/models (shows a list of models for a particular car)
/cars/{name}/models/{id} (shows a specific model for a particular car)
/cars/{name}/models/edit (add a new model which would be an action)
/cars/{name}/models/{id}/owners (a list of owners for a particular model)
/cars/{name}/models/{id}/owners/create (add a new owner)
So far, I have
routes.MapRoute(
name: "CarReleases",
url: "cars/{name}/models/{id}",
defaults:
new
{
controller = "Releases",
action = "Index",
id = UrlParameter.Optional
}
);
This works if I use /cars/{name}/models, but obviously, I don't have the action available for the models page. Do I have to create a new route map for this situation?
I also have the CarController, which is mapped as follows:
routes.MapRoute(
name: "Cars",
url: "cars/{name}/{action}",
defaults: new { controller = "Cars", action = "Details", id = UrlParameter.Optional }
);
As you can see, I have a general mixture of actions and dynamic requests. Bit confused the best way to put this into maproutes.
Order your routes from the most specific to the least specific. In my example all actions are mapped to the controller Cars; however, you may separate them. For example:
Owners:
routes.MapRoute(
name: "owners",
url: "cars/{name}/models/{id}/owners/{action}/{ownerId} ",
defaults: new { controller = "Cars", action = "OwnerIndex", id = UrlParameter.Optional, ownerId = UrlParameter.Optional }
);
Models:
routes.MapRoute(
name: "models",
url: "cars/{name}/models/{action}/{id}",
defaults: new { controller = "Cars", action = "ModelIndex", id = UrlParameter.Optional }
);
Cars:
routes.MapRoute(
name: "Cars",
url: "/cars/{name}/{action}/{id}",
defaults: new { controller = "Cars", action = "Index", id = UrlParameter.Optional }
);
Notice the default action was changed to Index so it lists all when you omit the action and Id (you may need to change it if you decide to keep them all in one controller)
Regarding your question whether you should keep them in one single controller, I think that's fine unless you would like to separate admin functions (edit, delete, etc) from viewing. In any case you can still have them in one controller and just add the Authorize attribute.
[Authorize(Roles = "admin")]
public ViewResult Delete(int id){}

reading related data after a selection of a foreign key - MVC3 and EF4

I am new to MVC and EF and I have a question.
I have built a site with models views controllers etc.
On an edit view for a Case (pretty big model so I won't post it here) I have a FK to a Customer model using CustomerID. When a user selects a customer id from a drop down list, I would like to display CustomerName, CustomerPhone etc after the selection of the ID. I think I might need to do a post back for this to work?
Also, do I need to Include the related entities as part of the initial data "get"? I have read some detail on that but I dont fully understand how that needs to work.
Please let me know if I should post more info. Thanks!
Here is my ActionResult for Edit
public ActionResult Edit(int id)
{
Cases cases = db.Cases.Find(id);
//related data needs to loaded to show related data fields
//include related data entities
var v = db.Cases.Include("Customers");
ViewBag.TechnicianID = new SelectList(db.Technicians, "TechnicianID", "LastName", cases.TechnicianID);
ViewBag.BranchID = new SelectList(db.Branches, "BranchID", "BranchName", cases.BranchID);
ViewBag.EngineModelID = new SelectList(db.EngineModels, "EngineModelID", "EngineModelName", cases.EngineModelID);
ViewBag.CaseCategoryID = new SelectList(db.CaseCategories, "CaseCategoryID", "CategoryName",cases.CaseCategoryID);
ViewBag.Qualified = new SelectList(new[] { "YES", "NO", "PARTIALLY" });
ViewBag.CaseStatus = new SelectList(new[] { "OPEN/IN PROCESS", "CLOSED" });
return View(cases);
}
The line
var v = db.Cases.Include("Customers")
is what I am trying to use to load related customer data and then show in my edit view like this:
#Html.EditorFor(model => model.Customer.CustomerName)
Well it depends on what you are trying to do. You could include a model which holds all the required data and send it with every call on that page (initial empty ofcourse)
Once you selected the customer, do post-back and send the customerId to your application and return the same page with the desired model.
You could do that via AJAX too.
Also, do I need to Include the related entities as part of the initial data "get"?
Not sure if I understand what you are trying to say. You mean that you think you would have to send all customer's data down to the page and select the related data on client side?

ASP.NET MVC 3 areas and DDD aggregate roots

I'm building a site and am considering using areas to cover a similar scenario to the one I'm about to describe.
I currently have a site with 4 sections, lets call these Create, Manage, Section 3 and Section 4
Create and Manage are actions on the domain object that I'm working with. The domain object has a number of collections of sub objects that relate to it. These need to be created and managed as well.
I am using Products as an example so as not to give anything away but it doesn't quite fit the same domain - so please don't say "Why don't you have a Products section"
My current implementation has a ManageController which has Actions like Categories, Category, ProductsForCategory
I'm thinking I need areas, however, some URLs will need to be scoped so I want
/Manage/Category/8/Products
/Manage/Category/8/Product/1
Is this possible using Areas? Do I need to set up new routing rules?
Would my CategoryController have 2 parameters on the action e.g.
public ActionResult Product(int categoryId, int productId)
{
//get category
var cat = GetCategory(categoryId);
//get product
var product = cat.Products.SingleOrDefault(x=>x.Id == productId);
if(product == null)
return RedirectToAction("Index","Manage");
return View(product);
}
Then I would have a routing rule that passed in the category id?
Is my thinking on this correct?
This is possible with Areas.. although it's my understanding that areas are mainly recommended for structuring your code into a meaningful folder structure to deal with large-ish MVC apps, whereas it looks like you want to use it for achieving nested routes?
To map your nested route to /Manage/Category/8/Product/1 you could create your "Manage" area, and then add a route like so:
context.MapRoute(null,
"Manage/{controller}/{categoryId}/{action}/{id}",
new
{
action = "Product",
id = "1",
categoryId = "2"
});
You then create an action method to accept those params:
public ActionResult Product(string categoryId, string id)
However, your question talks about aggregate DDD roots, so I suspect I've only answered part of the question?

Resources