Reconstructing a Razor URL using referring URL and language selection - asp.net-mvc

To implement language selection in an MVC Razor application, I use a leading path segment in the route mapping like www.mydomain.com/lang/controller/action/id?param= where lang is a 2 letter ISO country code like fr, de, it, en etc
I use the following route mapping (which works fine):
// Special localisation route mapping - expects specific language/culture code as first param
routes.MapRoute(
name: "Localisation",
url: "{lang}/{controller}/{action}/{id}",
defaults: new { lang = "en", controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { lang = #"[a-z]{2}|[a-z]{2}-[a-zA-Z]{2}" }
);
Previously I generated my language selection links in the master page, so that they were simply variations of the current URL (with only the first segment changed). Now I need to be able to create the links from within a partial view, that may be loaded dynamically via Ajax and the menu items (countries) are data driven.
That means I need to take the referring URL instead (the actual loaded page) and modify it to have a new language inserted, for each available language. The menu items are all database driven, so only contain the 2 letter language code and the display name.
Googling for "how to separate a URL into controller and action" I found an interesting link here: http://average-joe.info/url-to-route-data/
Based on that link, this is what I tried, but it blows up on a root URL like http://localhost:51176/ or with a full URL like http://localhost:51176/en/home/index. I would have expected it to return the defaults of home (controller) and index (action). Instead I get a Null reference exception.
string path = Request.UrlReferrer.ToString();
string queryString = ""; // Blank for now
System.Web.Routing.RouteData routeFromUrl = System.Web.Routing.RouteTable.Routes.GetRouteData(new HttpContextWrapper(new HttpContext(new HttpRequest(null, new UriBuilder(Request.Url.Scheme, Request.Url.Host, Request.Url.Port, path).ToString(), queryString), new HttpResponse(new System.IO.StringWriter()))));
// Blows up with Null exception as routeFromUrl is always null
string controller = (string)routeFromUrl.Values["controller"];
string action = (string)routeFromUrl.Values["action"];
string id = (string)routeFromUrl.Values["id"];
The idea being I can then generate links with href values like these using the referrers controller, action and parameters and therefore stay on the "same page" (except for the obvious language change):
http://localhost:51176/en/home/index
http://localhost:51176/de/home/index
http://localhost:51176/fr/home/index
What is wrong with the way I have used that piece of code (or does it just not work as I expected)?

Do'h... so simple.
Just needed to supply the path part of the URL only as it uses the current scheme, host & port applied to that path.
string path = Request.UrlReferrer.AbsolutePath;
Also note (valuable tip):
If you follow that example I linked, like I did, you need to adjust the following to use ToString() as they do not cast to string when empty:
string controller = routeFromUrl.Values["controller"].ToString();
string action = routeFromUrl.Values["action"].ToString();
string id = routeFromUrl.Values["id"].ToString();
This will give controller="home", action="index" and id="" as expected! Phew

Related

MVC Routing Parameter Precedence

I came across a scenario where I had the default MVC Route setup. Like So.
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Then navigating to a url as such
domain/controller/action/1234
On this page I was then navigating to the same page but with different parameters after a certain event. Like so.
var id = $(this).data("id");
var url = "#Url.Action("action", "controller", new { area = ""})";
var params = $.param({id: id, vrnsearch: true});
var fullUrl = url += "?" + params;
window.location.href = fullUrl;
I noticed that the url was still keeping the same id in the url and attaching parameters like so.
domain/controller/action/1234?id=4321&vrnsearch=true
Now my question is, is there a way to determine a precedence over if it should use the value for id from the url or from the parameter.
I have actually found a work around/fix for my issue by using the below, which removes the id from the url and just uses parameters.
#Url.Action("action","controller", new {id = "", area = ""})
However was just curious if there is a precedence in parameters vs url routing.
The query string has nothing at all to do with routing (at least, not unless you customize routing to consider it).
The values that are passed to the ModelBinder and to your action method are done so by Value Providers. You can control the order of precedence by changing the order in which their corresponding ValueProviderFactory is registered in the static ValueProviderFactories.Factories property.
As you can see, the default configuration is to first use the RouteDataValueProviderFactory and if it returns no value it will try the QueryStringValueProviderFactory. If you change the order of the factories, the order of precedence changes.
ValueProviderFactories.Factories.RemoveAt(3);
ValueProviderFactories.Factories.Insert(4, new RouteDataValueProviderFactory());

MVC routing with optional first parameter

I am trying to get a route like:
{lang:optional}/{controller}/{action}/{id:optional}
With "controller constraints" idea from this article: (MVC Routing Constraint on Controller Names), the above route works very well, when {lang} value is presented or not.
However I have a problem to match this route:
routeName: testRoute
url: {lang:optional}/list/{something:optional}
{controller = "product", action = "index"}
for the above route, the {lang} value must be presented, otherwise it does not work.
I have two workarounds to work it out.
The first way is to set two routes for the above:
The very standard one:
routeName: testRoute
url: /list/{something}
{controller = "product", action = "index"}
and another very standard one:
routeName: testRouteLang
url: {lang:not_optional}/list/{something:optional}
{controller = "product", action = "index", lang="de"}
I am wondering if there is a way to combine the two standard routes into one single route.
The second workaround is to use subdomain name, such as
http://example.com/list (default to English)
http://de.example.com/list (de)
But I really do not like the subdomain idea for the SEO reasons (maybe I am wrong on this point).
My goal is to remove the default "en-us" in the URL. I like this
http://www.example.com/list/something (default as English)
I do not want to force "en-us" in the url
http://www.example.com/en-us/list/something
The "lang" should only be presented in the Url if the current culture is not English:
http://www.example.com/de
http://www.example.com/fr/list/something
Thanks.
Finally I found an very easy and DRY solution. The core thing is to use the HttpContext.Current.RewritePath to inject the default "en", while this "en" will not be shown in the URL.
protected void Application_BeginRequest()
{
var rawUrl = HttpContext.Current.Request.RawUrl;
var segments = HttpContext.Current.Request.Url.Segments;
var segment1 = segments.Count() >= 2 ? segments[1] : string.Empty;
if (IsSomethingThatIWantToHandle("are,you,js,script,css,jpg,png,and,so,on?")
&& !LittleHelper.DoIHaveValidLangAlready(segment1))
{
HttpContext.Current.RewritePath("/en" + rawUrl);
}
}
When generate URL, if lang is null/empty, the URL will be have a double //. I just need a little helper to remove the extra "/".
When define a route, a trick is that the area name must be added to the DataTokens, otherwise the view cannot be correctly located, if areas are presented in the project.
routes.MapRoute(
"good name",
"{lang}/some-cool-stuff/{id}/{slug}",
defaults: new { area = "bigarea", controller = "bigcontroller", action = "tinyaction",
lang = UrlParameter.Optional, id = UrlParameter.Optional, slug = UrlParameter.Optional }
, constraints: new { lang = new CultureConstraint() }
).DataTokens.Add("area", "bigarea");
The CultureConstraint is very straightforward -- just verify whether it is a valid culture name. The namespace constraints is not necessary. However if the route table is big and complicated then the controller constraints, or even action constraints is very necessary otherwise duplicated routes will be an issue.
My default route in my project now is:
url: "{lang}/{area}/{controller}/{action}/{id}/{slug}",
and it works beautifully as I want.

HttpContext AbsolutePath goes to wrong URL for aliased pages - C#

On my website each page has links that are created in the codebehind, where the links are the current URL with one query parameter changed. To do this, I've been using this method (this specific example is for the pagination):
var queryValues = HttpUtility.ParseQueryString(HttpContext.Current.Request.QueryString.ToString());
queryValues.Set("page", num);
string url = HttpContext.Current.Request.Url.AbsolutePath;
string updatedQueryString = "?" + queryValues.ToString();
string newUrl = url + updatedQueryString;
return newUrl;
This worked on my local version fine. However, when I created each page in Ektron and added a manual alias, the URLs generated still went to the file location in the solution. For example, my original page was /WebAssets/Templates/EventListView.aspx. I created the page in Ektron as /Alumni/Events/List. I can go to /Alumni/Events/List, but then when I click on a page button the page that loads is /WebAssets/Templates/EventListView.aspx?page=2 instead of /Alumni/Events/List/?page=2
I found one solution:
var rawUrl = HttpContext.Current.Request.RawUrl;
var url = rawUrl.Split('?')[0];
string newUrl = url + updatedQueryString;
Use the QuickLink property of the primary contentblock for /Alumni/Events/List, this will be the alias use want to use for your page links or for redirects to the same page. This is probably ContentData.QuickLink if you're already loading the ContentData at some point in the code.
Notes:
Aliasing may remove your "page" querystring parameter by default, to resolve this issue, edit your alias in the Workarea to have a "Query String Action" of "Append".
Make sure you preprend a "/" to the QuickLink value (if it's not absolute and not prepended already) if using it on the frontend, otherwise your links will bring you to something like /Alumni/Events/List/Alumni/Events/List?page=2, which is no good.

Can .net mvc routing cause JavaScript errors?

I am having a lot of trouble using routing infrastructure of asp.net mvc2. I have following routes registered in my global.asax file
routes.MapRoute(
"strict",
"{controller}.mvc/{docid}/{action}/{id}",
new { action = "Index", id = "", docid = "" },
new { docid = #"\d+"}
);
routes.MapRoute(
"default",
"{controller}.mvc/{action}/{id}",
new { action = "Index", id = "" },
new { docConstraint = new DocumentConstraint() }
);
The problem is with first route ("strict"). Three kind of urls can match first route. mycontroller/23/myaction, mycontroller/23/myaction/12 or mycontroller/23/mvaction/stringid. If I try to use this route without specifying value of id everything works fine for example:
Html.ActionLink("Link text", "ActionName", new{docid = 23});
Everything goes well, but if I use links like:
Html.ActionLink("Link text", "ActionName", new{docid = 23, id = 223})
This will produce url currentcontroller.mvc/23/ActionName/223 that is absolutely correct but when it loads the page it gives a JavaScript error in jquery1.4.2.min.js file.
This is strange: if I change id to someid =223 it will reflect in query string and there will be no JS error.
Edit: I have done some further debugging and found when both id and docid are mentioned in route values one thing is ignored in global.asax that is the ignore path.
routes.RouteExistingFiles = false;
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.ignoreRoute is totally bypassed and I can see names of JS files in route value dictionary while debugging in my controller.
it gives javascript error in
jquery1.4.2.min.js file
The most likely cause for this is that something you are displaying on the page is different and you are performing an action that is causing the error. Can you supply enough of a sample from the rendered page to show what you are using jQuery for?
If we drag scripts from solution explorer to site.master it results in following output
<script type="text/javscript" src="../../scripts/jquery.min.js"></script>
The leading dots (..) are creating the problem. Putting source path in url.content or using /scripts instead of ../../scripts will solve the problem because these leading periods are forcing them to match some route in global.asax.

How do I create SEO-Friendly urls in ASP.Net-MVC

I'm getting myself acquainted with ASP.Net-MVC, and I was trying to accomplish some common tasks I've accomplished in the past with webforms and other functionality. On of the most common tasks I need to do is create SEO-friendly urls, which in the past has meant doing some url rewriting to build the querystring into the directory path.
for example:
www.somesite.com/productid/1234/widget
rather than:
www.somesite.com?productid=1234&name=widget
What method do I use to accomplish this in ASP.Net-MVC?
I've search around, and all I've found is this, which either I'm not understanding properly, or doesn't really answer my question:
SEO URLs with ASP.NET MVC
MVC stands for "Model View Controller" and while those concepts aren't what you're asking about, you generally can wire up URL's like you see above quite easily
So for example by default the URL's look like the following
http://www.somesite.com/controller/view/
where controller refers to the controller class within your project, and view refers to the page/method combination within the controller. So for example you could write the view to take in an input and look something like the following
http://www.somesite.com/widget/productid/1234/
Now as for SEO Friendly URL's, that's just useless sugar. You author your controller such that it adds harmless cruft to the end of the URL.
So for example, you'll notice that the following three ways to get to this question produce the same result:
How do I create SEO-Friendly urls in ASP.Net-MVC
How do I create SEO-Friendly urls in ASP.Net-MVC
How do I create SEO-Friendly urls in ASP.Net-MVC
Stack Overflow has authored their route values such that the bit that occurs after the question ID isn't really necessary to have.
So why have it there? To increase Google PageRank. Google PageRank relies on many things, the sum total of which are secret, but one of the things people have noticed is that, all other things being equal, descriptive text URL's rank higher. So that's why Stack Overflow uses that text after the question number.
Create a new route in the Global.asax to handle this:
routes.MapRoute(
"productId", // Route name
"productId/{id}/{name}", // URL with parameters
new { controller = "Home", action = "productId", id = 1234, name = widget } // Parameter defaults
);
Asp.Net MVC has routing built in, so no need for the Url Rewriter.
Be careful when implementing routes with names in them, you need to validate that the name coming in is correct or you can end up harming your SEO-Ranking on the page by having multiple URIs share the same content, either set up a proper canonical or have your controller issue 301s when visiting the 'wrong' URI.
A quick writeup I did on the latter solution can be found at:
http://mynameiscoffey.com/2010/12/19/seo-friendly-urls-in-asp-net-mvc/
Some info on canonicals:
http://googlewebmastercentral.blogspot.com/2009/02/specify-your-canonical.html
I think what you are after is MVC Routing
have a look at these
MVC Routing on www.asp.net
Book: ASP.NET MVC in Action - Routing
It is also important to handle legacy urls. I have a database of old and new urls and I redirect with the following code;
public class AspxCatchHandler : IHttpHandler, IRequiresSessionState
{
#region IHttpHandler Members
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
if (context.Request.Url.AbsolutePath.Contains("aspx") && !context.Request.Url.AbsolutePath.ToLower().Contains("default.aspx"))
{
string strurl = context.Request.Url.PathAndQuery.ToString();
string chrAction = "";
string chrDest = "";
try
{
DataTable dtRedirect = SqlFactory.Execute(
ConfigurationManager.ConnectionStrings["emptum"].ConnectionString,
"spGetRedirectAction",
new SqlParameter[] {
new SqlParameter("#chrURL", strurl)
},
true);
chrAction = dtRedirect.Rows[0]["chrAction"].ToString();
chrDest = dtRedirect.Rows[0]["chrDest"].ToString();
chrDest = context.Request.Url.Host.ToString() + "/" + chrDest;
chrDest = "http://" + chrDest;
if (string.IsNullOrEmpty(strurl))
context.Response.Redirect("~/");
}
catch
{
chrDest = "/";// context.Request.Url.Host.ToString();
}
context.Response.Clear();
context.Response.Status = "301 Moved Permanently";
context.Response.AddHeader("Location", chrDest);
context.Response.End();
}
else
{
string originalPath = context.Request.Path;
HttpContext.Current.RewritePath("/", false);
IHttpHandler httpHandler = new MvcHttpHandler();
httpHandler.ProcessRequest(HttpContext.Current);
HttpContext.Current.RewritePath(originalPath, false);
}
}
#endregion
}
hope this is useful
i think stackoverflow is best practice.
because when you remove a word at end of url, it redirect with 301 status to current url.
domain.com/product/{id}/{slug}
it had some benefits:
when you change slug of page it will redirect to correct url
your url is short but have seo friendly words at end.
in backend you use id to find product but you have slug for search engine
you can have same slug for different page. I now its not good for seo but you can have same slug for 2 products
the code will be like #Martin
routes.MapRoute(
name: "Cafes",
url: "cafe/{id}/{FriendlyUrl}",// how send array in metod get
defaults: new { controller = "cafe", action = "index", id = UrlParameter.Optional, FriendlyUrl = UrlParameter.Optional }//, id = UrlParameter.Optional
);
in action controller you should check database slug of product with the parameter that you receive. if they are not same redirect with status 301 to correct url.
the code for redirect:
return RedirectToActionPermanent("index", new
{
id = CafeParent.CafeID,
FriendlyUrl = CafeParent.CafeSlug.Trim()
});
RedirectToActionPermanent redirect with status 301. when you do this, the search engine find that this url is changed to what you redirected.

Resources