MVC routing a repeatable pattern? - asp.net-mvc

A design goal for a website I'm working on is to keep the URL in the browser in a state where the user can copy it, and the link can be used from another browser/user/machine to return to the spot that the url was copied. (The actual changes will happen via AJAX, but the URL will change to reflect where they are.)
Example: If you were on the customer page looking at customer 123, and had details pulled up on their order #456, and full details on line 6 of this order, your url could simply be /customer/123/456/6
The challenge comes with a second feature: Users can add UI columns (analogous to adding a new tab in a tab view, or a new document in an MDI app) Each column can easily generate a routable url, but I need the url to reflect one or more columns. (E.G. User has both /customer/123/456/6 and /customer/333/55/2 in two side by side columns)
In a perfect world, I'd like the url to be /customer/123/456/6/customer/333/55/2 for the above scenario, but I don't know if MVC routing can handle repetitive patterns, or, if so, how it is done.
Can this be done via routing? If not is there a way to get this type of one-or-more functionality from Url?

You could create a custom route handler (see my previous answer) or derive from a RouteBase like NightOwl888 suggested. Another approach would be to simply use a model binder and a model binder attribute.
public class CustomerInvoiceLineAttribute : CustomModelBinderAttribute
{
public override IModelBinder GetBinder()
{
return new CustomerInvoiceLineModelBinder();
}
}
public class CustomerInvoiceLineModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var path = (string)bindingContext.ValueProvider.GetValue(bindingContext.ModelName).AttemptedValue;
var data = path.Split(new[] { "/customer/" }, StringSplitOptions.RemoveEmptyEntries);
return data.Select(d =>
{
var rawInfo = d.Split('/');
return new CustomerInvoiceLine
{
CustomerId = int.Parse(rawInfo[0]),
InvoiceId = int.Parse(rawInfo[1]),
Line = int.Parse(rawInfo[2])
};
});
}
}
You define your route by specifying a star route data. This mean that the route parameter will contains everything following the action
routes.MapRoute(
name: "CustomerViewer",
url: "customer/{*customerInfo}",
defaults: new { controller = "Customer", action = "Index" });
Then in your controller, you bind your parameter with the same name as the star route parameter using the custom model binder defined above:
public ActionResult Index([CustomerInvoiceLine] IEnumerable<CustomerInvoiceLine> customerInfo)
{
return View();
}
You will need to add validation during the parsing and probably security too, so that a customer cannot read the invoice of other customers.
Also know that URL have a maximum length of 2000 characters.

You can do this with the built-in routing as long as you don't anticipate that any of your patterns will repeat or have optional parameters that don't appear in the same segment of the URL as other optional parameters.
It is possible to use routing with optional parameters by factoring out all of the permutations, but if you ask me it is much simpler to use the query string for this purpose.
NOTE: By definition, a URL must be unique. So you must manually ensure your URLs don't have any collisions. The simplest way to do this is by matching the page with the path (route) and adding this extra information as query string values. That way you don't have to concern yourself with accidentally making routes that are exactly the same.
However, if you insist on using a route for this purpose, you should probably put your URLs in a database in a field with a unique constraint to ensure they are unique.
For the most advanced customization of routing, subclass RouteBase or Route. This allows you to map any URL to a set of route values and map the route values back to the same URL, which lets you use it in an ActionLink or RouteLink to build the URLs for your views and controllers.
public class CustomPageRoute : RouteBase
{
// This matches the incoming URL and translates it into RouteData
// (typically a set of key value pairs in the RouteData.Values dictionary)
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
// Trim the leading slash
var path = httpContext.Request.Path.Substring(1);
if (/* the path matches your route logic */)
{
result = new RouteData(this, new MvcRouteHandler());
result.Values["controller"] = "MyController";
result.Values["action"] = "MyAction";
// Any other route values to match your action...
}
// IMPORTANT: Always return null if there is no match.
// This tells .NET routing to check the next route that is registered.
return result;
}
// This builds the URL for ActionLink and RouteLink
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
VirtualPathData result = null;
if (/* all of the expected route values match the request (the values parameter) */)
{
result = new VirtualPathData(this, page.VirtualPath);
}
// IMPORTANT: Always return null if there is no match.
// This tells .NET routing to check the next route that is registered.
return result;
}
}
Usage
routes.Add(
name: "CustomPage",
item: new CustomPageRoute());
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

Related

Manipulate the url using routing

In my website I have the following route defined:
routes.MapRoute(
name: "Specific Product",
url: "product/{id}",
defaults: new { controller = "", action = "Index", id = UrlParameter.Optional }
);
In that way I want customers to be able to add the ID of the product and go to the product page.
SEO advisors have said that it would be better if we could add a description of the product on the URL, like product-name or something. So the URL should look something like:
/product/my-cool-product-name/123
or
/product/my-cool-product-name-123
Of course the description is stored in the db and I cannot do that with a url rewrite (or can I?)
Should I add a redirection on my controller (this would seem to do the job, but it just doesn't feel right)
On a few sites I checked they do respond with a 301 Moved Permanently. Is that really the best approach?
UPDATE
As per Stephen Muecke's comment I checked on what is happening on SO.
The suggested url was my own Manipulate the url using routing and i opened the console to see any redirections. Here is a screenshot:
So, first of all very special thanks to #StephenMuecke for giving the hint for slugs and also the url he suggested.
I would like to post my approach which is a mix of that url and several other articles.
My goal was to be able to have the user enter a url like:
/product/123
and when the page loads to show in the address bar something like:
/product/my-awsome-product-name-123
I checked several web sites that have this behaviour and it seems that a 301 Moved Permanently response is used in all i checked. Even SO as shown in my question uses 301 to add the title of the question. I thought that there would be a different approach that would not need the second round trip....
So the total solution i used in this case was:
I created a SlugRouteHandler class which looks like:
public class SlugRouteHandler : MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
var url = requestContext.HttpContext.Request.Path.TrimStart('/');
if (!string.IsNullOrEmpty(url))
{
var slug = (string)requestContext.RouteData.Values["slug"];
int id;
//i care to transform only the urls that have a plain product id. If anything else is in the url i do not mind, it looks ok....
if (Int32.TryParse(slug, out id))
{
//get the product from the db to get the description
var product = dc.Products.Where(x => x.ID == id).FirstOrDefault();
//if the product exists then proceed with the transformation.
//if it does not exist then we could addd proper handling for 404 response here.
if (product != null)
{
//get the description of the product
//SEOFriendly is an extension i have to remove special characters, replace spaces with dashes, turn capital case to lower and a whole bunch of transformations the SEO audit has requested
var description = String.Concat(product.name, "-", id).SEOFriendly();
//transform the url
var newUrl = String.Concat("/product/",description);
return new RedirectHandler(newUrl);
}
}
}
return base.GetHttpHandler(requestContext);
}
}
From the above i need to also create a RedirectHandler class to handle the redirections. This is actually a direct copy from here
public class RedirectHandler : IHttpHandler
{
private string newUrl;
public RedirectHandler(string newUrl)
{
this.newUrl = newUrl;
}
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext httpContext)
{
httpContext.Response.Status = "301 Moved Permanently";
httpContext.Response.StatusCode = 301;
httpContext.Response.AppendHeader("Location", newUrl);
return;
}
}
With this 2 classes i can transform product ids to SEO friendly urls.
In order to use these i need to modify my route to use the SlugRouteHandler class, which leads to :
Call SlugRouteHandler class from the route
routes.MapRoute(
name: "Specific Product",
url: "product/{slug}",
defaults: new { controller = "Product", action = "Index" }
).RouteHandler = new SlugRouteHandler();
Here comes the use of the link #StephenMuecke mentioned in his comment.
We need to find a way to map the new SEO friendly url to our actual controller. My controller accepts an integer id but the url will provide a string.
We need to create an Action filter to handle the new param passed before calling the controller
public class SlugToIdAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var slug = filterContext.RouteData.Values["slug"] as string;
if (slug != null)
{
//my transformed url will always end in '-1234' so i split the param on '-' and get the last portion of it. That is my id.
//if an id is not supplied, meaning the param is not ending in a number i will just continue and let something else handle the error
int id;
Int32.TryParse(slug.Split('-').Last(), out id);
if (id != 0)
{
//the controller expects an id and here we will provide it
filterContext.ActionParameters["id"] = id;
}
}
base.OnActionExecuting(filterContext);
}
}
Now what happens is that the controller will be able to accept a non numeric id which ends in a number and provide its view without modifying the content of the controller. We will only need to add the filter attribute on the controller as shown in the next step.
I really do not care if the product name is actually the product name. You could try fetching the following urls:
\product\123
\product\product-name-123
\product\another-product-123
\product\john-doe-123
and you would still get the product with id 123, though the urls are different.
Next step is to let the controller know that it has to use a special filer
[SlugToId]
public ActionResult Index(int id)
{
}

How To Make a Wild Card Route of Sorts with ASP.NET MVC

I want to map all calls that look like mydomain.com/ng* to my Ng Controller index. I'm not getting how to add that MapRoute. I've tried what is below but that give me an immediate error The route parameter name '' is invalid.
// ng*
routes.MapRoute("NgWildcard", "Ng{*}",
new
{
/* Your default route */
controller = "Ng",
action = "Index"
});
From MSDN:
The URL pattern consists of segments that come after the application name in an HTTP request. For example, in the URL http://www.contoso.com/products/show/beverages, the pattern applies to products/show/beverages. A pattern with three segments, such as {controller}/{action}/{id}, matches the URL http://www.contoso.com/products/show/beverages. Each segment is delimited by the / character. When a segment is enclosed in braces ( { and } ), the segment is referred to a URL parameter. ASP.NET routing retrieves the value from the request and assigns it to the URL parameter. In the previous example, the URL parameter action is assigned the value show. If the segment is not enclosed in braces, the value is treated as a literal value.
From MSDN:
You can define more than one placeholder between delimiters, but they must be separated by a constant value. For example, {language}-{country}/{action} is a valid route pattern. However, {language}{country}/{action} is not a valid pattern, because there is no constant or delimiter between the placeholders. Therefore, routing cannot determine where to separate the value for the language placeholder from the value for the country placeholder.
In other words, "Ng{*}" is treated as a literal value in the URL, since you have neither enclosed the entire segment in braces nor you have defined 2 placeholders with a separator in between. In order to have it treated as a placeholder, you must put a valid separator character between the Ng and the {*}. As Daniel mentioned, you should also name the wildcard placeholder so you can reference it in your route.
routes.MapRoute("NgWildcard", "Ng-{*ngName}",
new
{
/* Your default route */
controller = "Ng",
action = "Index"
});
If you want your route to be exactly per your requirements, you will need to inherit RouteBase.
public class NgRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
var path = httpContext.Request.Path;
if (path.StartsWith("Ng"))
{
// Process your route
result.Values["controller"] = "Ng";
result.Values["action"] = "Index";
// Process additional URL segments and set route values accordingly
}
// Always return null in case of a non-match.
return result;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
VirtualPathData result = null;
var controller = Convert.ToString(values["controller"]);
var action = Convert.ToString(values["action"]);
if (controller == "Ng" && action == "Index")
{
// Return the virtual path to your URL (not sure how you will do that)
var virtualPath = "Ng"; // TODO: Finish path
return new VirtualPathData(this, virtualPath);
}
// Always return null in case of a non-match.
return result;
}
}
And as others have already stated, you are on your own as far as parsing other segments out of the URL when using a wildcard.
Try naming it
routes.MapRoute("NgWildcard", "Ng{*ngName}",
new
{
/* Your default route */
controller = "Ng",
action = "Index"
});

How can I set up a simple route with areas in ASP.NET MVC3?

I want to use Areas so I set up the following:
public class ContentAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Content";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Content_default",
"Content/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
What I would like is for a person who enters the following URL to be directed to a controller inside my Content area.
www.stackoverflow.com/Content/0B020D/test-data
I would like a person entering any URL with "/Content/" followed by six characters to be sent to:
- Page action in a controller named ItemController
- Six characters passed as the parameter id
- Optional text after that (test-data in this case) to be put into parameter title
How can I do this? I am not very familiar with setting up routes when using areas.
the six digits to be put into a variable called ID
So you're looking for something like
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Content_default",
"Content/{id}/{optional}",
new { controller = "ItemController", action = "TheActionYouWantThisToAllRouteTo" }
}
This would default everything to one controller and action method (which you have to specify in your instance). You can then get the data like so:
public ActionResult TheActionYouWantThisToAllRouteTo (string id, string optional)
{
// Do what you need to do
}
The way the routes are setup, you can name the pieces of information you want in a URL by wrapping it in a pair of { } curly braces. If you'd rather the name of optional to be isTestData then you would just change the route to read "Content/{id}/{isTestData}".
Note: Since you didn't specify the default action method you want this to route to, I substituted it with TheActionYouWantThisToAllRouteTo. Change that string to read the action method you want this to all go to. This also means you can't have a "regular" controller named ContentController, either.
Edit
Stephen Walther has a good blog post on custom route constraints. It can be found here. It should be a good start to get done what you need.

ASP.NET MVC Map String Url To A Route Value Object

I am creating a modular ASP.NET MVC application using areas. In short, I have created a greedy route that captures all routes beginning with {application}/{*catchAll}.
Here is the action:
// get /application/index
public ActionResult Index(string application, object catchAll)
{
// forward to partial request to return partial view
ViewData["partialRequest"] = new PartialRequest(catchAll);
// this gets called in the view page and uses a partial request class to return a partial view
}
Example:
The Url "/Application/Accounts/LogOn" will then cause the Index action to pass "/Accounts/LogOn" into the PartialRequest, but as a string value.
// partial request constructor
public PartialRequest(object routeValues)
{
RouteValueDictionary = new RouteValueDictionary(routeValues);
}
In this case, the route value dictionary will not return any values for the routeData, whereas if I specify a route in the Index Action:
ViewData["partialRequest"] = new PartialRequest(new { controller = "accounts", action = "logon" });
It works, and the routeData values contains a "controller" key and an "action" key; whereas before, the keys are empty, and therefore the rest of the class wont work.
So my question is, how can I convert the "/Accounts/LogOn" in the catchAll to "new { controller = "accounts", action = "logon" }"??
If this is not clear, I will explain more! :)
Matt
This is the "closest" I have got, but it obviously wont work for complex routes:
// split values into array
var routeParts = catchAll.ToString().Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
// feels like a hack
catchAll = new
{
controller = routeParts[0],
action = routeParts[1]
};
You need to know what part is what in the catchAll parameter. Then you need to parse it yourself (like you are doing in your example or use a regexp). There is no way for the framework to know what part is the controller name and what is the action name and so on, as you haven't specified that in your route.
Why do you want to do something like this? There is probably a better way.

Asp.Net MVC - Best approach for "dynamic" routing

I am trying to come up with an approach to create "dynamic" routing. What I mean, exactly, is that I want to be able to assign the controller and action of a route for each hit rather than having it mapped directly.
For example, a route may look like this "path/{object}" and when that path is hit, a lookup is performed providing the appropriate controller / action to call.
I've tried discovering the mechanisms for creating a custom route handler, but the documentation / discoverability is a bit shady at the moment (I know, its beta - I wouldn't expect any more). Although, I'm not sure if thats even the best approach and perhaps a controller factory or even a default controller/action that performs all of the mappings may be the best route (no pun intended) to go.
Any advice would be appreciated.
You can always use a catch all syntax ( I have no idea if the name is proper).
Route:
routeTable.MapRoute(
"Path",
"{*path}",
new { controller = "Pages", action = "Path" });
Controller action is defined as:
public ActionResult Path(string path)
In the action for controller you will have a path, so just have to spilt it and analyse.
To call another controller you can use a RedirectToAction ( I think this is more proper way). With redirection you can set up a permanent redirectionfor it.
Or use a something like that:
internal class MVCTransferResult : RedirectResult
{
public MVCTransferResult(string url) : base(url)
{
}
public MVCTransferResult(object routeValues)
: base(GetRouteURL(routeValues))
{
}
private static string GetRouteURL(object routeValues)
{
UrlHelper url = new UrlHelper(
new RequestContext(
new HttpContextWrapper(HttpContext.Current),
new RouteData()),
RouteTable.Routes);
return url.RouteUrl(routeValues);
}
public override void ExecuteResult(ControllerContext context)
{
var httpContext = HttpContext.Current;
// ASP.NET MVC 3.0
if (context.Controller.TempData != null &&
context.Controller.TempData.Count() > 0)
{
throw new ApplicationException(
"TempData won't work with Server.TransferRequest!");
}
// change to false to pass query string parameters
// if you have already processed them
httpContext.Server.TransferRequest(Url, true);
// ASP.NET MVC 2.0
//httpContext.RewritePath(Url, false);
//IHttpHandler httpHandler = new MvcHttpHandler();
//httpHandler.ProcessRequest(HttpContext.Current);
}
}
However this method require to run on IIS or a IIS Expres Casinni is not supporting a Server.Transfer method

Resources