I'm trying to set up routing in an MVC app such that an optional language "folder" is at the top level.
e.g.
site.com/jp/complexroute
... is the Japanese version of the page found using complexroute
site.com/complexroute
... is the English version of the page found using complexroute
I can think of one way which I think would work which would be to take the entire list of route mappings in RouteConfig.RegisterRoutes and make a second set which are identical but have a language parameter which is constrained to only known language settings. This sounds like a headache both for writing and maintaining.
I experimented with writing a custom MvcHandler and attaching it to the routes. This let me extract part of the request and put it into RequestContext.RouteData but by the time this is called it seems that the route / controller have already been picked?
I guess I want something that happens in between the request being received and the route being selected where I can manipulate the url and set a value in RequestContext.RouteData before the lookup against the RouteCollection occurs.
Is this possible?
routes.MapRoute(
name: "Default",
url: "{lang}/{controller}/{action}/{id}",
defaults: new { lang = "en", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
and you can access lang in RouteData.Values["lang"]
sample >
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
internal sealed class DoLocalizeAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
string cultureName = filterContext.RouteData.Values["lang"].ToString();
SpecificCulture = new CultureInfo(cultureName);
Thread.CurrentThread.CurrentCulture = SpecificCulture;
Thread.CurrentThread.CurrentUICulture = SpecificCulture;
}
public CultureInfo SpecificCulture { get; set; }
}
and then on each controller you want to be localized apply this attribute
sample >
[DoLocalize]
public class AccountController : Controller
Related
i already have one MVC website in english language. Now I need to add arabic language. To do that i want to use structure below to store my localized .cshtml files
English Views
~/Views/index.cshtml
~/Views/about.cshtml
Arabic Views
~/ar/Views/index.cshtml
~/ar/Views/about.cshtml
English URL (It's working)
www.samplesite.com/home/index
www.samplesite.com/home/index
Arabic URL (It's not working)
www.samplesite.com/ar/home/index
www.samplesite.com/ar/home/index
I need to return english view if url with /en or no language parameter and if URL starts with /ar i need to return view in ar/Views folder. What should i change in route config and actions to get the correct view based on URL ?
In order to localize the ASP.NET MVC app with different views for different languages and cultures, you need to at least do:
Save language code from URL to route data collection, if exists
Set current culture based on the language code, if exists
Route the request to localized views, if language code appears
1. Save language code from URL to route data collection, if exists
You need to define the route mapping so that whenever there is a language code on the URL, it would be put to the route data collection.
You can define the extra route mapping in the RouteConfig:
namespace DL.SO.Globalization.DotNet.Web.UI
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.LowercaseUrls = true;
routes.MapRoute(
name: "DefaultWithLang",
url: "{lang}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { lang = new LanguageRouteConstraint() }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
}
We will come back to the route constraint, but the idea here is to run this "DefaultWithLang" mapping first, and see if the incoming request has the language code attached on it or not. If yes, save it to the route collection with the key lang. Otherwise, just process it as normal.
Route constraints
Imagine we have those 2 mappings defined, and the incoming request looks like /home/index. Without that route constraint, this would be mapped to the "DefaultWithLang" with lang being "home", which is incorrect.
We will need a way to test whether the language code is a valid 2-letter code. To do that, you can create a route constraint:
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Routing;
namespace DL.SO.Globalization.DotNet.Web.UI.RouteConstraints
{
public class LanguageRouteConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName,
RouteValueDictionary values, RouteDirection routeDirection)
{
return Regex.IsMatch((string)values[parameterName], #"^[a-z]{2}$");
}
}
}
With this in placed, the request "/home/index" will first come in with "home" being the language code, but it will fail this route constraint because it has more than 2 letters. This would result no matching for "DefaultWithLang" map. Then the MVC will continue to use the 2nd mapping and see if the request is a valid request, which it is!
2. Set current culture based on the language code, if exists
Now we know the language code will be put into the route data collection. We can set the current culture and the current UI culture of the current thread based on that.
To do so, we can create an ActionFilter:
namespace DL.SO.Globalization.DotNet.Web.UI.Filters
{
public class LanguageFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var values = filterContext.RouteData.Values;
string languageCode = (string)values["lang"] ?? "en";
var cultureInfo = new CultureInfo(languageCode);
Thread.CurrentThread.CurrentCulture = cultureInfo;
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(cultureInfo.Name);
}
}
}
Here I am reading the language code value from the route data collection, or it defaults to "en" if it doesn't exist.
To enable it for the whole application, you can add it to FilterConfig.cs:
namespace DL.SO.Globalization.DotNet.Web.UI
{
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new LanguageFilter());
filters.Add(new HandleErrorAttribute());
}
}
}
3. Route the request to localized views, if language code appears
Similar to Step #2, now we have the language code, we need to create a custom view engine to route the request to different views:
namespace DL.SO.Globalization.DotNet.Web.UI
{
public class GlobalizationRazorViewEngine : RazorViewEngine
{
protected override IView CreatePartialView(ControllerContext controllerContext,
string partialPath)
{
partialPath = GetGlobalizeViewPath(controllerContext, partialPath);
return base.CreatePartialView(controllerContext, partialPath);
}
protected override IView CreateView(ControllerContext controllerContext,
string viewPath, string masterPath)
{
viewPath = GetGlobalizeViewPath(controllerContext, viewPath);
return base.CreateView(controllerContext, viewPath, masterPath);
}
private string GetGlobalizeViewPath(ControllerContext controllerContext,
string viewPath)
{
var request = controllerContext.HttpContext.Request;
var values = controllerContext.RouteData.Values;
string languageCode = (string)values["lang"];
if (!String.IsNullOrWhiteSpace(languageCode))
{
string localizedViewPath = Regex.Replace(viewPath,
"^~/Views/",
String.Format("~/{0}/Views/", languageCode)
);
if (File.Exists(request.MapPath(localizedViewPath)))
{
viewPath = localizedViewPath;
}
}
return viewPath;
}
}
}
The logic here should be straight forward: if the language code exists, try to append it to the original view path the MVC app was going to use to find the corresponding view.
Note: you would need to copy _viewStart.cshtml as well as web.config from the original Views folder to the new localized Views folder. You might consider using different folder structure to contain all localized views. One possible way is to put them all inside the ~/Views folder.
To use this custom view engine, you would need to add it to the view engine collection at Application_Start():
namespace DL.SO.Globalization.DotNet.Web.UI
{
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new GlobalizationRazorViewEngine());
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
}
}
Screenshots
With zh/ on the URL:
The folder structure:
Github source code: https://github.com/davidliang2008/DL.SO.Globalization.DotNet/tree/master/DL.SO.Globalization.DotNet.Web.UI
First of all, I need to say that I'm using T4MVC in my project. I have one method for two routes :
public virtual ActionResult List(string slug, string category, String selectedFilters)
Routes:
routes.MapRoute("ProductOnSale", "products/{slug}/{category}/onsale", MVC.Product.List());
routes.MapRoute("ProudctsList", "products/{slug}/{category}/{selectedFilters}", MVC.Product.List()
.AddRouteValue("selectedFilters", ""));
As you can see, this is only one ActionResult for two routes. They have a different url. Example for the first route:
products/living-room-furniture/sofas/sectional-sofa
Example for the second route:
products/living-room-furniture/living-room-tables/onsale
This piece should say that I came from the another page. I need to add Boolean parameter to my method List(string slug, string category, String selectedFilters, bool onsale) and, depends on that, choose route. Is it possible to do using constraints? May anyone provide an example how to do it in this case?
I'm not sure if I understand your question correctly. Two cases I come accross might help you.
CASE 1 : redirect to another URL depending on the URL used to access the page.
STEP 1: Create an MVCRouteHandler
public class LandingPageRouteHandler : MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext Context)
{
if ( Context.HttpContext.Request.Url.DnsSafeHost.ToLower().Contains("abc"))
{
Context.RouteData.Values["controller"] = "LandingPage";
Context.RouteData.Values["action"] = "Index";
Context.RouteData.Values["id"] = "abc";
}
return base.GetHttpHandler(Context);
}
}
STEP 2: Add a RouteHandler
routes.MapRoute(
name: "Landingpage",
url: "Landingpage/{id}/{*dummy}",
defaults: new { controller = "Landingpage", action = "Index" }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
).RouteHandler = new LandingPageRouteHandler();
CASE 2 : Add a property to a controller and view depending on the url used
All controlles in my case derive from a BaseController class. In the BaseController I have :
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
//Set TenantByDomain
var DnsSafeHost = filterContext.HttpContext.Request.Url.DnsSafeHost;
int? TenantByDomain = null;
if (db.Tenants.Any(x => x.DnsDomains.Contains(DnsSafeHost)))
{
TenantByDomain = db.Tenants.First(x => x.DnsDomains.Contains(DnsSafeHost)).Id;
}
((BaseController)(filterContext.Controller)).TenantByDomain = TenantByDomain;
filterContext.Controller.ViewBag.TenantByDomain = TenantByDomain;
}
Applied to your Question.
Using the routehandler you could add an extra property indicating the original route taken and redirect both to a 3th url (! the user does not see this new url).
In the OnActionExecuting do something with the extra routevalue so that the handling can be done as desired.
We have implemented a localized version of an ASP.NET MVC website which has a URL structure as following:
url://{language}-{culture}/{controller}/{action}/{id}
In this way we can generate URLs by language which are properly crawled by Google bot:
http://localhost/en-US/Home
http://localhost/fr-FR/Home
The translation is achieved in two places. First we modified the default route of MVC with this one:
routes.MapRoute(
name: "Default",
url: "{language}-{culture}/{controller}/{action}/{id}",
defaults: new
{
controller = "Home",
action = "Index",
id = UrlParameter.Optional,
language = "en",
culture = "US"
}
);
Then we have created an action filter which switch to the current language available in the URL and if not available to the default one:
public class LocalizationAttribute : ActionFilterAttribute
{ public override void OnActionExecuting(ActionExecutingContext filterContext)
{
string language = (string)filterContext.RouteData.Values["language"] ?? "en";
string culture = (string)filterContext.RouteData.Values["culture"] ?? "US";
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(string.Format("{0}-{1}", language, culture));
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(string.Format("{0}-{1}", language, culture));
}
}
}
The problem occurs if a user enter http://localhost/Whatever. ASP.NET MVC returns "Route not found". How can I pass a default parameter for the language if the user forgets to pass one? I though that by setting the default value into route config would be enough, but it doesn't work
You just need another route to handle the case where there is no first segment.
routes.MapRoute(
name: "Default-Localized",
url: "{language}-{culture}/{controller}/{action}/{id}",
defaults: new
{
controller = "Home",
action = "Index",
id = UrlParameter.Optional
}
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new
{
language = "en",
culture = "US",
controller = "Home",
action = "Index",
id = UrlParameter.Optional
}
);
Matching the URL pattern and building the route values collection (based on defaults or the placeholders that can override them) are 2 different steps that are handled by the Route class. Populating route values doesn't happen unless the URL pattern matches first.
Do note that if you use an action filter to set the locale of the current thread that localization won't be available inside of the model binder. A way around that is to use an IAuthorizationFilter instead of ActionFilterAttribute.
using System.Globalization;
using System.Threading;
using System.Web.Mvc;
public class LocalizationFilter : IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
var values = filterContext.RouteData.Values;
string language = (string)values["language"] ?? "en";
string culture = (string)values["culture"] ?? "US";
CultureInfo ci= CultureInfo.GetCultureInfo(string.Format("{0}-{1}", language, culture));
Thread.CurrentThread.CurrentCulture = ci;
Thread.CurrentThread.CurrentUICulture = ci;
}
}
And then add it as a global filter.
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new LocalizationFilter());
filters.Add(new HandleErrorAttribute());
}
}
I have two areas that register routes as shown below:
"Website" area:
context.MapRoute(
"Landing Controllers",
"{controller}/{action}",
new { controller = "Home", action = "Index" }
);
"Mobile" area:
context.MapRoute(
"Mobile Defaults",
"{controller}/{action}",
new { controller = "MobileHome", action = "Index" },
new { controller = "MobileHome", action = "Index" }
);
By default, one or the other of these routes would be consistently taken when trying to go to the root URL /. But suppose we decorated our controller actions with a custom AuthorizeAttribute, where the OnAuthorization method is overridden to redirect the user to the correct controller when appropriate, as below. (Idea taken from a great blog post.)
public class MobileRedirectAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
var result = // Logic to generate the ActionResult that conditionally
// takes us to the other route goes here.
filterContext.Result = result;
}
}
I've tried using a new RedirectResult and RedirectToRouteResult, neither of which work as I'd like because of the routing conflict. Is there a way to set AuthorizationContext.Result to a value that would take us to the action that we're not currently executing? (As a last resort, I can just prefix the mobile route with some sort of namespacing variable, but I'd like to avoid going down that road just yet.)
My question can probably also be summarized by having a look at Wikipedia's desktop/mobile routing. Their two sites, http://en.m.wikipedia.org/wiki/Main_Page and http://en.wikipedia.org/wiki/Main_Page also share identical routes, but, depending on which mode you're in, return very different results.
Would it be possible to set up Wikipedia's routing in an MVC project where each environment (mobile/desktop) is registered in its own area?
A colleague led me to a promising solution using a custom IRouteConstraint.
public class HelloWorldConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route,
string parameterName, RouteValueDictionary values,
RouteDirection routeDirection)
{
// Determine whether to accept the route for this request.
var browser = BrowserDetector.Parse(httpContext.Request.UserAgent);
if (browser == BrowserPlatform.Mobile)
{
return true;
}
return false;
}
}
And my route declaration now looks like the below, where the route constraint is attached to a route parameter chosen at random.
context.MapRouteLowercase(
"Mobile Defaults",
"{controller}/{action}",
new { controller = "MobileHome", action = "Index" },
// In this case, it's not so much necessary to attach the constraint to
// a particular route parameter as it is important to be able to inspect
// the HttpContextBase provided by the IRouteConstraint.
new {
controller = new HelloWorldConstraint()
}
);
Not with standard MVC Routing. You can probably do with attribute routing, available in either MVC 5 or via the nuget package, AttributeRouting.
I'm new to MVC (and ASP.Net routing). I'm trying to map *.aspx to a controller called PageController.
routes.MapRoute(
"Page",
"{name}.aspx",
new { controller = "Page", action = "Index", id = "" }
);
Wouldn't the code above map *.aspx to PageController? When I run this and type in any .aspx page I get the following error:
The controller for path '/Page.aspx' could not be found or it does not implement the IController interface.
Parameter name: controllerType
Is there something I'm not doing here?
I just answered my own question. I had
the routes backwards (Default was
above page).
Yeah, you have to put all custom routes above the Default route.
So this brings up the next question...
how does the "Default" route match (I
assume they use regular expressions
here) the "Page" route?
The Default route matches based on what we call Convention over Configuration. Scott Guthrie explains it well in his first blog post on ASP.NET MVC. I recommend that you read through it and also his other posts. Keep in mind that these were posted based on the first CTP and the framework has changed. You can also find web cast on ASP.NET MVC on the asp.net site by Scott Hanselman.
http://weblogs.asp.net/scottgu/archive/2007/11/13/asp-net-mvc-framework-part-1.aspx
http://www.asp.net/MVC/
I just answered my own question. I had the routes backwards (Default was above page). Below is the correct order. So this brings up the next question... how does the "Default" route match (I assume they use regular expressions here) the "Page" route?
routes.MapRoute(
"Page",
"{Name}.aspx",
new { controller = "Page", action = "Display", id = "" }
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
On one of Rob Conery's MVC Storefront screencasts, he encounters this exact issue. It's at around the 23 minute mark if you're interested.
Not sure how your controller looks, the error seems to be pointing to the fact that it can't find the controller. Did you inherit off of Controller after creating the PageController class? Is the PageController located in the Controllers directory?
Here is my route in the Global.asax.cs
routes.MapRoute(
"Page",
"{Page}.aspx",
new { controller = "Page", action = "Index", id = "" }
);
Here is my controller, which is located in the Controllers folder:
using System.Web.Mvc;
namespace MvcApplication1.Controllers
{
public class PageController : Controller
{
public void Index()
{
Response.Write("Page.aspx content.");
}
}
}
public class AspxRouteConstraint : IRouteConstraint
{
#region IRouteConstraint Members
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
return values["aspx"].ToString().EndsWith(".aspx");
}
#endregion
}
register the route for all aspx
routes.MapRoute("all",
"{*aspx}",//catch all url
new { Controller = "Page", Action = "index" },
new AspxRouteConstraint() //return true when the url is end with ".aspx"
);
And you can test the routes by MvcRouteVisualizer