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.
Related
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.
I can't figure out why I need to create an empty method signature to allow a Get rest call with 3 null parameters to work. I have the following Code:
public class SessionPresenterController : ApiController
{
public HttpResponseMessage Get()
{
return Get(null, null, null);
}
public HttpResponseMessage Get(int? codeCampYearId, int? sessionId, int? attendeesId)
{
and in my WebApiConfig I have
config.Routes.MapHttpRoute
("API Default Rest", "rest/{controller}/{id}",
new { id = RouteParameter.Optional });
config.Routes.MapHttpRoute
("API Default RPC", "rpc/{controller}/{action}/{id}",
new { id = RouteParameter.Optional });
If I call /rest/SessionPresenter without any parameters and I Don't have the Get() defined, the Get with the three null parameters is not found.
Why not?
Try specifying defaults for your parameters:
public HttpResponseMessage Get(
int? codeCampYearId = null,
int? sessionId = null,
int? attendeesId = null)
{
//...
}
Jacobs' snippet will solve your issue. Let's answert the question why not? why get is not found..
There is a complete documentation of the Routing and Action Selection. Let's use some extratc and reveal what happens:
Action Selection
Create a list of all actions on the controller that match the HTTP request method.
If the route dictionary has an "action" entry, remove actions whose name does not match this value.
Try to match action parameters to the URI, as follows:
For each action, get a list of the parameters that are a simple type, where the binding gets the parameter from the URI. Exclude optional parameters.
From this list, try to find a match for each parameter name, either in the route dictionary or in the URI query string. Matches are case insensitive and do not depend on the parameter order.
Select an action where every parameter in the list has a match in the URI.
If more that one action meets these criteria, pick the one with the most parameter matches.
Other words, if there will be only Get with three parameters (omited Get()), to decided which action should be selected:
Selected was Get(int? codeCampYearId, int? sessionId, int? attendeesId)
still the Get(int? codeCampYearId, int? sessionId, int? attendeesId) is selected
URL is /rest/SessionPresenter
no optional parameters excluded. All have to be found
URL does not have a match for all three parameters
So to solve it, we have to either pass all params (empty, null):
/rest/SessionPresenter?codeCampYearId&attendeesId&sessionid
Or change the signature to have parameters optinal (Jacobs' answer), and skipped during the action selection
I wasn't able to put the write words for the Title, so I explained what I'm looking after.
Currently, I am using following ActionLink code in View.
#Html.ActionLink(#product.Name, "Index", "Product", new { id = #product.Id }, null)
This code redirects to following action method in Product Controller
public ActionResult Index(int id)
{
Product product = pe.Products.Where(p => p.Id == id).First();
ViewBag.Title = product.Name;
ViewBag.Description = product.MetaDescription;
ViewBag.Keywords = product.MetaKeywords;
return View(product);
}
Now, what i want: instead of mysite.com/Product/Index/22 , my URL should be something like mysite.com/Product/Apple-ipad.
I know, I can use product Name instead of Id and pass it to the action method. But, this way i think that the queries will get slower since Id field is indexed but Name isn't. Is this the only option at my disposal. Let me know how will you handle this requirement.
Use the product name in the route instead of ID and put a nonclustered index on the product name column in the database.
That way you don't get a performance hit on the select (although you will get one on the insert/update/delete but I suspect those happen far less than the selects).
CREATE INDEX IX_[index_name]
ON [schema].[table_name] ([column_name]);
Another way I've seen used in a few places is using both the name and the id like mysite.com/Product/Apple-ipad/22. The name is not actually used by the code, and it's just there for SEO. One big disadvantage of this is that someone that does not like your site can put various urls leading to the same content all over the internet for google to find. Google doesn't like that so your site is penalized and you are worse than before.
When passing a RouteValueDicitonary or anonymous object into a #Url.Action method (or any of its analogs), is there a way to properly pass in a collection object or IEnumerable so that it would generate a url that would be compatible with the default model binder?
For example, say I have an action like this:
public ActionResult Index(ICollection<int> ids)
{
...do something
}
and in my template I do something like this:
#Url.Action("Index", routeValues:new int[]{1,2,3})
the goal is to have a url output like this:
... /index?ids=1&ids=2&ids=3
but the url output is actually something like this:
... /index?ids=System.Int[]
I'm assuming there is no support for this currently. If not, then at what part in MVC do I need to create a custom handler or whatever to override this default functionality?
Unfortunately currently there is no existing helper that will allow you to generate such url. So one possibility is to do it manually:
#(Url.Action("Index") + "?" + string.Join("&", new int[] { 1, 2, 3 }.Select(x => "ids=" + x)))
or write an extension method to encapsulate the logic:
#Url.ActionWithIds("Index", new int[] { 1, 2, 3 })
Since those are integers we don't need url encoding but if you want to do the same thing with a collection of strings the values should be properly url encoded.
Pass each element of the array to a RouteValueDictionary using the model binding syntax for round trip purposes.
int i = 0;
var temprvd = arr.ToDictionary(m => String.Format("name[{0}]", i++), m => (object) m);
#Url.Action("index", new RouteValueDictionary(temprvd));
or concat on to an existing route value dictionary.
Cast the value to an object (as shown above) to ensure the correct RouteValueDictionary constructor overload is used.
For use the default model binder, you should end up with something like :
?ids=1&ids=2&ids=3
you can returns a private collection named HttpValueCollection even the documentation says it's a NameValueCollection using the ParseQueryString utility.
Then add the keys manually, HttpValueCollection do the encoding for you.
And then just append the QueryString manually :
var qs = HttpUtility.ParseQueryString("");
new List<int> { 1, 2, 3, 4, 5 }.ForEach(x => qs.Add("ids", x.ToString()));
#Url.Action("Index")?#qs
I was looking for a way to pass "GET" variables in codeigniter and ended up coming across this :
link text
I am wondering how to implement it.
For example :
www.website.com/query would give me every entry in the DB .
Typically I would have
www.website.com/query/?id=5 to get the equivalent entry.
when i try to do that the CI way :
www.website.com/query/id/5
I get a 404 error since it is looking for a class named id and it can't find it.
is there any way to get a step by step way to do this?
thank you.
Two good ways to achieve this using methods intended by the Codeigniter developers.
OPTION ONE:
If you always expect an "id" parameter to be present you could take advantage of a feature where you pass the value in the URI immediately after the method (function) you want to call.
Example passing /[controller]/[method]/[value]:
http://www.website.com/query/index/5
You would then access the value of "id" as an expected parameter of the function.
Class Query extends Controller {
...
// From your URL I assume you have an index method in the Query controller.
function index($id = NULL)
{
// Show current ID value.
echo "ID is $id";
...
}
...
}
OPTION TWO:
If you would like to allow many parameters to be passed in addition to ID, you could add all parameters as key=>value pairs to the URI segments in any order.
Example passing /[controller]/[method]/[key1]/[val1]/[key2]/[val2]/[key3]/[val3]:
http://www.website.com/query/index/id/5/sort/date/highlight/term
You would then parse all the URI segments from the 3rd segment ("id") forward into an array of key=>value pairs with the uri_to_assoc($segment) function from the URI Class.
Class Query extends Controller {
...
// From your code I assume you are calling an index method in the Query controller.
function index()
{
// Get parameters from URI.
// URI Class is initialized by the system automatically.
$data->params = $this->uri->uri_to_assoc(3);
...
}
...
}
This would give you easy access to all the parameters and they could be in any order in the URI, just like a traditional query string.
$data->params would now contain an array of your URI segments:
Array
(
[id] => 5
[sort] => date
[highlight] => term
)
HYBRID OF ONE AND TWO:
You could also do a hybrid of these where ID is passed as an expected parameter and the other options are passed as key=>value pairs. This is a good option when ID is required and the other parameters are all optional.
Example passing /[controller]/[method]/[id]/[key1]/[val1]/[key2]/[val2]:
http://www.website.com/query/index/5/sort/date/highlight/term
You would then parse all the URI segments from the 4th segment ("sort") forward into an array of key=>value pairs with the uri_to_assoc($segment) function from the URI Class.
Class Query extends Controller {
...
// From your code I assume you are calling an index method in the Query controller.
function index($id = NULL)
{
// Show current ID value.
echo "ID is $id";
// Get parameters from URI.
// URI Class is initialized by the system automatically.
$data->params = $this->uri->uri_to_assoc(4);
...
}
...
}
$id would contain your ID value and $data->params would contain an array of your URI segments:
You can still use GET parameters, they're just mapped to controller member function parameters:
test.com/query/id/4
Would map to the controller:
$query->id($id);
This assumes you have added a query controller and member function properly in the controllers folder in your CI application.
You can also pass your parameter values as POST parameters using a form and the CI input class.
Use $this->uri->uri_to_assoc(2) 2 is an offset, as you starting your associative array of segments in the 2nd segment. You will also need a route to make /query map to a controller and method (unless you do this in the index() method).
So, this URL:
/query/id/foo/key/bar
could be read using:
$get = $this->uri->uri_to_assoc(2);
echo $get['id']; // 'foo'
echo $get['key']; // 'bar'
It's not great, but it works.