How to setup multilingual and localized Views in ASP.NET MVC? - c#

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

Related

Select route depends on URL parameter. Constraints

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.

Sitecore, custom MVC controllers and routes

I have a website defined in Sitecore's site definitions. The path to it is /localhost/mysite/home. And it works.
I need to create a custom controller to submit forms with an API bypassing Sitecore. So I have FormsController (inheriting from MVC controller) with an action named "Test" taking no parameters.
I defined the route in the initialize pipeline like this:
public class Initialize
{
public void Process(PipelineArgs args)
{
MapRoutes();
GlassMapperSc.Start();
}
private void MapRoutes()
{
RouteTable.Routes.MapRoute(
"Forms.Test",
"forms/test",
new
{
controller = "FormsController",
action = "Test"
},
new[] { "Forms.Controller.Namespace" });
}
}
The route is added to the route table correctly, and it's there when I debug it.
Now, when I try to call method "test", the route is not found and the debugger doesn't hit the breakpoint in the action.
I'm trying different routes:
/localhost/mysite/home/forms/test
/localhost/forms/test (default website)
But no luck so far.
---- UPDATE ---
Going deeper into it, I noticed that there's something wrong with Sitecore's behavior. The TransferRoutedRequest processor is supposed to abort the httpRequestBegin pipeline, giving control back to MVC, in case the context item is null (simplifying). It happens after some checks, among which is one on RoutTable data. But the call to RouteTable.Routes.GetRouteData returns always null, which makes the processor return without aborting the pipeline. I overrode it to make it abort the pipeline correctly, but still, even if I it calls method args.AbortPipeline(), pipeline is not aborted and route not resolved.
this is how the original TransferRoutedRequest looked like:
public class TransferRoutedRequest : HttpRequestProcessor
{
public override void Process(HttpRequestArgs args)
{
Assert.ArgumentNotNull((object) args, "args");
RouteData routeData = RouteTable.Routes.GetRouteData((HttpContextBase) new HttpContextWrapper(HttpContext.Current));
if (routeData == null)
return;
RouteValueDictionary routeValueDictionary = ObjectExtensions.ValueOrDefault<Route, RouteValueDictionary>(routeData.Route as Route, (Func<Route, RouteValueDictionary>) (r => r.Defaults));
if (routeValueDictionary != null && routeValueDictionary.ContainsKey("scIsFallThrough"))
return;
args.AbortPipeline();
}
}
and this is how I overrode it:
public class TransferRoutedRequest : global::Sitecore.Mvc.Pipelines.HttpRequest.TransferRoutedRequest
{
public override void Process(HttpRequestArgs args)
{
if (Context.Item == null || Context.Item.Visualization.Layout == null)
args.AbortPipeline();
else
base.Process(args);
}
}
Here is a working example taken from one of my projects.
Custom Route Registration:
namespace Test.Project.Pipelines.Initialize
{
public class InitRoutes : Sitecore.Mvc.Pipelines.Loader.InitializeRoutes
{
public override void Process(PipelineArgs args)
{
RegisterRoutes(RouteTable.Routes);
}
protected virtual void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(
"Test", // Route name
"api/test/{controller}/{action}/{id}", // URL with parameters
new { id = UrlParameter.Optional }
);
}
}
}
Initialize Pipeline Config:
<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<initialize>
<processor type="Test.Project.Pipelines.Initialize.InitRoutes, Test.Project"
patch:after="processor[#type='Sitecore.Mvc.Pipelines.Loader.InitializeRoutes, Sitecore.Mvc']" />
</initialize>
</pipelines>
</sitecore>
</configuration>
Here is the code that will create a route for you. In global.asax.cs you will call RegisterRoutes from App_Start event handler:
protected void Application_Start()
{
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
And there you specify your route as:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(
name: "test",
url: "mvc/Forms/{action}/{id}",
defaults: new { controller = "Forms", action = "Test", id = UrlParameter.Optional }
);
}
You will have /mvc/ prefix in this case that will handle your route to specifies controller, so you will call it as:
/mvc/Forms/Test/{you_may_pass_some_optional_GUID_here}
This will route to FormsController class action method Test(string id) but you may omit id parameter
Finally I've got it working correctly. As I wrote TransferRoutedRequest wasn't working as expected, so I had to override it. Still, even if it worked as it's supposed to, the route wasn't being resolved. The problem was in the pipeline configuration. Thanks to a colleague I opened SitecoreRocks pipeline tool and it showed the pipeline was registered in a position too far away from where it should have been, so it was never hit (I had registered it after ItemResolver, as it was on the original configuration). I patched it before LayoutResolver and that did the trick. The route was resolved.
Still, Sitecore wasn't able to create an instance of the type, as it was in another assembly. Even specifying the controller's namespace didn't solve the problem. So I had to make some modifications and override CreateController method of ControllerFactory class and override InitializeControllerFactory processor (which I already had modified to be able to work with a DI container), writing a new ControllerFactory and a new SitecoreControllerFactory.
The final code looks like this:
public override IController CreateController(RequestContext requestContext, string controllerName)
{
var controller = SC.Context.Item == null || SC.Context.Item.Visualization.Layout == null
? base.GetControllerType(requestContext, controllerName)
: TypeHelper.GetType(controllerName);
return GetControllerInstance(requestContext, controller);
}
In case I'm dealing with a Sitecore item, I use TypeHelper to return current controller type from a controller rendering or layout. Otherwise I use DefaultControllerFactory.GetControllerType to resolve the custom route.
The only thing to be careful: in case 2 or more namespaces have a controller with the same name and action, it's mandatory to add a namespace in order to identify them.

Is it possible for two areas to share the same route and still both be reachable?

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.

Using a prefix in MVC routing

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

How to route a .aspx page in asp.net mvc 3 project?

I have a .aspx page in the following path:
Areas/Management/Views/Ticket/Report.aspx
I want to route that to the following path in my browser:
http://localhost/Reports/Tickets
How can i do that?
I try this:
routes.MapRoute(
"Tickets", // Route name
"Areas/Management/Views/Ticket/Report.aspx", // Original URL
new { controller = "Reports", action = "Tickets" } // New URL
);
But i got the 404 error.
What i'm doing wrong?
Obs: I put that before the Default route.
If you are trying to utilise web forms in a MVC project then I would move your .aspx out of the views folder, as it isn't really a view, so something like WebForms/Tickets/Report.aspx.
In web forms you map a route by calling the MapPageRoute method.
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapPageRoute("Tickets", "Reports/Tickets", "~/WebForms/Tickets/Report.aspx");
routes.MapRoute("Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional });
You'll need to put that before the default MVC route.
Solved! So, we need to add a route contraint to the webforms route to ensure that it only catches on incoming routes, not outgoing route generation.
Add the following class to your project (either in a new file or the bottom of global.asax.cs):
public class MyCustomConstaint : IRouteConstraint{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection){
return routeDirection == RouteDirection.IncomingRequest;
}
}
Then change the Tickets route to the following:
routes.MapPageRoute(
"Tickets",
"Reports/Tickets",
"~/WebForms/Reports/Tickets.aspx",
true, null,
new RouteValueDictionary { { "outgoing", new MyCustomConstaint() } }
);
you are doing it opposite. this maps your url Areas/Management/Views/Ticket/Report.aspx to { controller = "Reports", action = "Tickets" }
what u should do instead is
set the url as Reports/Tickets
EDIT:- you can create a routeHandler just for routing to this .aspx page.. like this.
public class ASPXRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return BuildManager.CreateInstanceFromVirtualPath("~/Areas/Management/Views/Ticket/Report.aspx", typeof(Page)) as Page;
}
}
then u can add ur route to the existing routes table using
Route customRoute = new Route("Reports/Ticket",null, new ASPXRouteHandler());
routes.Add(customRoute);
if you leave the default routing when you create the asp.net project
public class ReportsController : Controller
{
public ActionResult Ticket()
{
return View();
}
}
this should do the trick.
The routing in asp.net mvc means that you don't link directly to .aspx but to Actions (methods) that in turn return an appropriate view (.aspx)

Categories

Resources