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());
}
}
Related
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.
I want to change this
localhosht:3220/MyController/MyAction?id=1
to this
localhosht:3220/myText/MyController/MyAction?id=1
So that it works any time, even in redirection, routing or other cases.
Thanks.
Just add the text to the route url:
routes.MapRoute(
name: "Default",
url: "myText/{controller}/{action}"
);
You can do it with a custom route to match your desired uri. In the RegisterRoutes method of RouteConfig, add the following.
The order is important here, if you add the "Default" route before the "MyText" route, then MyText route will not be hit.
//for your custom route that starts with "myText"
routes.MapRoute(
name: "MyText",
url: "myText/{controller}/{action}/{id}",
defaults: new { controller = "MyController", action = "MyAction", id = UrlParameter.Optional }
);
//for other normal routes
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
While adding a route with the url parameter of myText/{controller}/{action}/{id} (above your default route) will certainly work, if your intention is to use the myText frequently, you might also want to take a look at the concept of areas.
By default, in your Global.asax, in your Application_Start() function, you have a call for the method:
AreaRegistration.RegisterAllAreas();
What this does is to look for a special Areas folder in your project, and register each folder under it that has a class that extends from AreaRegistration and overrides the RegisterArea function. Similar to your RouteConfig.cs, this class can look like this:
public class MyTextAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "MyText";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
name: "MyText_default",
url: "MyText/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
note: you can create Areas in your MVC project by scaffolding: Just right click on your project and Add.. -> Area. Everything will be done for you automatically.
In the end, Areas provide a more permanent solution to your MyText/SomeController/SomeAction needs. For a detailed article, you can check this.
routes.MapRoute(
name: "Default",
url: "{myText}/{controller}/{action}/{id}"
defaults: new {
controller = "Home",
action = "Index",
id = UrlParameter.Optional,
myText= UrlParameter.Optional
);
It will work for you if you want to use parameter like myText to make your Url Seo friendly.
I have myself a little problem with areas and controllers.
I have a controller called "HomeController" this is the default controller, I then added a new area called "Supplier", I then copied over my HomeController and removed all the methods I don't want to edit and just edited my Index method.
Now when I build it works fine but when I go to my home controller as a supplier it comes up with this error
Multiple types were found that match the controller named 'Home'. This can
happen if the route that services this request ('{controller}/{action}/{id}')
does not specify namespaces to search for a controller that matches the request.
If this is the case, register this route by calling an overload of the
'MapRoute' method that takes a 'namespaces' parameter.
The request for 'Home' has found the following matching controllers:
TestProject.Controllers.Home
TestProject.Areas.Supplier.Controllers.Home
I have updated my areas like so
This is my default area
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new[] { "TestProject.Controllers" }
);
}
}
And here is my area route file
public class SupplierAreaRegistration: AreaRegistration
{
public override string AreaName
{
get
{
return "Supplier";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"SupplierHomeIndex",
"Home/Index/{id}",
defaults: new { area = "Supplier", controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new[] { "TestProject.Areas.Supplier.Controllers" },
constraints: new { permalink = new AreaConstraint(AreaName) }
);
context.MapRoute(
"SupplierDefault",
"{controller}/{action}/{id}",
defaults: new { area = "Supplier", action = "index", id = UrlParameter.Optional },
namespaces: new[] { "TestProject.Controllers"},
constraints: new { permalink = new AreaConstraint(AreaName) }
);
}
}
Can anyone sign some light on this? I have looked at many topics and answers for this via Google and Stackoverflow however nothing seems to work for me.
You've customized the area's routes and removed the Supplier URL prefix. When the routing framework spins up it merely collects all controllers from your application, regardless of where they are, and then looks for a match based on the URL. In your case, you now have two controllers that are both bound to the URL /Home/*. Typically, the area's URL would be prefixed with the area's name to avoid the confusion, i.e. /Supplier/Home.
I have problems building an ASP.NET MVC page which allows two sorts of routing.
I have a database where all pages are stored with an url-path like: /Site1/Site2/Site3
i tried to use an IRouteConstraint in my first route, to check wether the requested
site is a site from my database (permalink).
In the second case, i want to use the default asp.net mvc {controller}/{action} functionality, for providing simple acces from an *.cshtml.
Now i don't know if this is the best way. Furthermore i have the problem, how to root with the IRouteContraint.
Does anyone have any experiance with this?
I'm using asp.net mvc 5.
Problem solved, final solution:
Adding this two routes:
routes.MapRoute(
"FriendlyUrlRoute",
"{*FriendlyUrl}"
).RouteHandler = new FriendlyUrlRouteHandler();
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Page", action = "Load", id = UrlParameter.Optional },
namespaces: controllerNamespaces.ToArray()
);
My own Route-Handler:
public class FriendlyUrlRouteHandler : System.Web.Mvc.MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(System.Web.Routing.RequestContext requestContext)
{
var friendlyUrl = (string)requestContext.RouteData.Values["FriendlyUrl"];
WebPageObject page = null;
if (!string.IsNullOrEmpty(friendlyUrl))
{
page = PageManager.Singleton.GetPage(friendlyUrl);
}
if (page == null)
{
page = PageManager.Singleton.GetStartPage();
}
// Request valid Controller and Action-Name
string controllerName = String.IsNullOrEmpty(page.ControllerName) ? "Page" : page.ControllerName;
string actionName = String.IsNullOrEmpty(page.ActionName) ? "Load" : page.ActionName;
requestContext.RouteData.Values["controller"] = controllerName;
requestContext.RouteData.Values["action"] = actionName;
requestContext.RouteData.Values["id"] = page;
return base.GetHttpHandler(requestContext);
}
}
You can use attribute routing which is in MVC 5 and combine attribute routing with convention-based routing to check the condition that you want on controller class or action methods.
And you could make the constraint yourself to use it on the action methods like this:
public class ValuesConstraint : IRouteConstraint
{
private readonly string[] validOptions;
public ValuesConstraint(string options)
{
validOptions = options.Split('|');
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
object value;
if (values.TryGetValue(parameterName, out value) && value != null)
{
return validOptions.Contains(value.ToString(), StringComparer.OrdinalIgnoreCase);
}
return false;
}
}
To use attribute routing you just need to call MapMvcAttributeRoutes during configuration and call the normal convention routing afterwards. also you should add your constraint before map the attributes, like the code below:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
var constraintsResolver = new DefaultInlineConstraintResolver();
constraintsResolver.ConstraintMap.Add("values", typeof(ValuesConstraint));
routes.MapMvcAttributeRoutes(constraintsResolver);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
Now on your controller class you can check the route and decide what to do with different urls like below:
for example: // /mysite/Site1 and /mysite/Site2 but not /mysite/Site3
[Route("mysite/{site:values(Site1|Site2)}")]
public ActionResult Show(string site)
{
return Content("from my database " + site);
}
And you could do all kind of checking just on you controller class as well.
I hope this gives you a bit of clue to achieve the thing that you want.
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