I'd like to generate a new URL based on the existing route, but will add a new parameter 'page'
Here are a few examples:
old: ~/localhost/something?what=2
new: ~/localhost/something?what=2&page=5
old: ~/localhost/Shoes
new: ~/localhost/Shoes/5
I can not just append &page=5 to existing url because routes may be different.
Some use the query string and some do not.
I had a similar issue, and took the approach of extending the UrlHelper. The code in the View looks like:
Page 2
The UrlHelper extension looks like:
using System.Web.Mvc;
using System.Web.Routing;
using System.Collections.Specialized;
public static class UrlHelperExtension
{
public static string AddPage(this UrlHelper helper, int page)
{
var routeValueDict = new RouteValueDictionary
{
{ "controller", helper.RequestContext.RouteData.Values["controller"] },
{ "action" , helper.RequestContext.RouteData.Values["action"]}
};
if (helper.RequestContext.RouteData.Values["id"] != null)
{
routeValueDict.Add("id", helper.RequestContext.RouteData.Values["id"]);
}
foreach (string name in helper.RequestContext.HttpContext.Request.QueryString)
{
routeValueDict.Add(name, helper.RequestContext.HttpContext.Request.QueryString[name]);
}
routeValueDict.Add("page", page);
return helper.RouteUrl(routeValueDict);
}
}
A couple of notes: I check for the ID, since I don't use it in all my routes. I add the Page route value at the end, so it is the last url parameter (otherwise you could add it in the initial constructor).
This seems like a good approach:
// Clone Current RouteData
var rdata = new RouteValueDictionary(Url.RequestContext.RouteData.Values);
// Get QueryString NameValueCollection
var qstring = Url.RequestContext.HttpContext.Request.QueryString;
// Pull in QueryString Values
foreach (var key in qstring.AllKeys) {
if (rdata.ContainsKey(key)) { continue; }
rdata[key] = qstring[key];
}
// Update RouteData
rdata["pageNo"] = "10";
// Build Url
var url = Url.RouteUrl(rdata);
and it avoids collisions such as ?controller=example&action=problem etc.
You could reconstruct a url by pulling out the parts of the existing route by way of the RouteData object. For instance, the following would render a url with the controller and action of the current route:
<%=Url.RouteUrl(new { controller = ViewContext.RouteData.Values["controller"],
action = ViewContext.RouteData.Values["action"] }) %>
To get you started, you could go with something like a custom extension method that generates the url with an additional "page" parameter. Adjust as necessary:
public static string UrlWithPage(this UrlHelper urlHelper, string name, int page)
{
string url = urlHelper.RouteUrl(
new {
controller = urlHelper.RequestContext.RouteData.Values["controller"],
action = urlHelper.RequestContext.RouteData.Values["action"],
id = urlHelper.RequestContext.RouteData.Values["id"],
page = page
}
);
return "" + name + "";
}
This will construct a properly formatted link based on the routing configuration, whether page is real segment in the url or just appended as a querystring.
Related
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 }
);
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)
{
}
I have link
http://localhost:3163/PaymentOrder?AgentCode=&InvoiceNo=&AgentName=&FromDate=&fromDate=12%2F11%2F2013&FromDate=12%2F11%2F2013+9%3A08%3A01+SA&toDate=12%2F11%2F2013
after click button "Delete" the page should be redirect to "Index"
return RedirectToAction("Index","PaymentOrder");
But i want keep link same as first, i don't know what method, please help me. thanks
I can fix it, i save session in
public ActionResult Index{
Session["LastPage"] = Request.Url.ToString();
}
after I'm
return Redirect(Session["LastPage"] as String);
You can pass the query strings to the third parameter of RedirecToAction
return RedirectToAction("Index","PaymentOrder", new { fromDate = model.FromDate });
Or pass the entire model as well, that contains the properties similar to your query strings
return RedirectToAction("Index","PaymentOrder", new { paymentModel = model });
As your query string is quite long, it would probably be better to write an extension method and use that instead, to keep your controllers thin. I haven't tested this, but something like this should work:
public static RouteValueDictionary ToRouteDictionary(this NameValueCollection nameValues)
{
if (nameValues == null || nameValues.HasKeys() == false)
return new RouteValueDictionary();
var routeValues = new RouteValueDictionary();
foreach (var key in nameValues.AllKeys)
routeValues.Add(key, nameValues[key]);
return routeValues;
}
Then in your controller:
return RedirectToAction("Index","PaymentOrder", Request.QueryString.ToRouteDictionary());
Just don't redirect but return the view, the URL will remain the same.
I want to map all CMS pages url to single controller(PageController) and action(Details).
How can I create custom routing to map all these urls?
/teacher
/teacher/kindergarten
/teacher/kindergarten/1
/teacher/primary
/teacher/primary/english
/teacher/primary/language
/teacher/primary/language/chinese
/teacher/primary/math
/teacher/primary/science
/parent
/parent/kindergarten
/parent/primary1-3
/parent/primary4-6
/leader
/leader/kindergarten
/leader/kindergarten/1
If you have these URLs in a database you could map the routes when the application starts up:
var pages = siteDB.Pages.ToList();
string pagePath = "";
foreach (var page in pages)
{
routeVals = new RouteValueDictionary();
constraints = new RouteValueDictionary();
routeVals.Add("controller", "page");
routeVals.Add("action", "details");
constraints.Add("path", "[a-zA-Z0-9\\-]*");
// any child pages? must add these routes before their parent pages.
var childPages = siteDB.Pages.Where(p => p.ParentPageId == page.PageId).ToList();
foreach (var childPage in childPages)
{
pagePath = BuildPath(childPage);
RouteTable.Routes.Add(new Route(pagePath, new MvcRouteHandler())
{
Defaults = routeVals,
Constraints = constraints,
DataTokens =
new RouteValueDictionary {
{ "pageid", childPage.PageId },
{ "path", pagePath }
}
});
// Any further child pages? (Only 3 levels supported)
var childSubPages = siteDB.Pages.Where(p => p.ParentPageId == childPage.PageId).ToList();
foreach (var childSubPage in childSubPages)
{
pagePath = BuildPath(childSubPage);
RouteTable.Routes.Add(new Route(pagePath, new MvcRouteHandler())
{
Defaults = routeVals,
Constraints = constraints,
DataTokens =
new RouteValueDictionary {
{ "pageid", childSubPage.PageId },
{ "path", pagePath }
}
});
}
}
This code takes the pages from a database where they are linked by parent id.
Here's the BuildPath function which generates a full path to each page:
public static string BuildPath(Page page)
{
if (page.ParentPageId == 1)
{
return page.PageKey;
}
else
{
SiteDataEntities siteDB = new SiteDataEntities();
string path = page.PageKey;
Page parent = siteDB.Pages.Find(page.ParentPageId);
while (parent != null)
{
path = parent.PageKey + "/" + path;
parent = siteDB.Pages.Find(parent.ParentPageId);
if (parent.PageKey == "home") break;
}
return path;
}
}
Previous proposed solution is working only for small amount of pages.
Because according to the code:
application generate and register Route for each of site page. In result we have at least same amount of routes as pages in our site. As you probably know RouteModule have to check route by route each of them to find first right one and execute correct handler, controller, action, view...
There are two other way to solve this:
You can create a class that derives from RouteBase and implement the properties and methods that you need: split url to segments, determinate current page fill RouteValueDictionary with pageid, path, parents etc
You can customize UrlRewriteModule with custom rewrite provider. Idea to transform all requests url from tree base structure to mvc default route:
{controller}/{action}/{id}?path=parentlevel1/parent2/parent3/....
90% -same code for both variants could be prepared.
that solution also could be useful when you have different controllers, correct one we could determinate by current page (by page data: type)
I have a base controller that I use to return basic views like this.
public ActionResult Index(string pageName)
{
return View(pageName);
}
public ActionResult LanguageSpecific(string ul, string pageName)
{
var result = View("sv/" + pageName);
return View(result.ViewName);
}
The controller's name is home is there a way that for it not to look for the sv content in /home but just in /sv
"EnglishRoute", // Route name
"{pageName}.aspx", // URL with parameters
new { controller = "Home", action = "Index", pageName = "" } // Parameter defaults
);
routes.MapRoute(
"SwedishRoute", // Route name
"{ul}/{pageName}.aspx", // URL with parameters
new { controller = "Home", action = "LanguageSpecific", ul = "",pageName = "" } // Parameter defaults
);
It looks in these locations:
~/Views/Home/sv/index.aspx
~/Views/Home/sv/index.ascx
When you call the View method you can pass in an app-relative path that starts with "~/" and then ASP.NET MVC will use the exact path you specify:
return View("~/UseExactlyThisFile.aspx");
That way it won't do its search in the various paths and locations that are pre-configured.
Please keep in mind that this doesn't have very much to do with routing (though it does a little bit).
If you try to localize your pages, why don't you use resources? With the pattern above you don't really take the advantages of mvc. Or do i misunderstand you? A simple solution would be to use an action filter which picks up the language identifier from the route and sets the UICulture. The Views then may use resources to localize their content.