воскресенье, 23 июня 2013 г.

Addition to ASP.NET MVC Localization - Using routing

In my previous post I described the way of localization using session, but in real-world applications it's definitely not the best way of localization. Now I'll describe very simple and very powerful way of storing it in URL using routing mechanism.

Also this way of localization will not require OutputCache tricks described in previous post

The goal of this post is to show how to get URL  like this/{culture}/{Controller}/{Action}... in your application like /ru/Home/About.


Custom Route Handlers

First of all we'll need to extend standard MvcRouteHandler class. One classMultiCultureMvcRouteHandler for routes that will use culture in params andSingleCultureMvcRouteHandler class (will be used as a marker, no implementation changes)

public class MultiCultureMvcRouteHandler : MvcRouteHandler
{
    protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        var culture = requestContext.RouteData.Values["culture"].ToString();
        var ci = new CultureInfo(culture);
        Thread.CurrentThread.CurrentUICulture = ci;
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name);
        return base.GetHttpHandler(requestContext);
    }
}

In the overridden GetHttpHandler before calling it's base implementation we just get "culture" param from RouteData collection, create CultureInfo object and set it to current thread current culture. So here is a place where we set culture and will not useApplication_AcquireRequestState method in Global.asax

public class SingleCultureMvcRouteHandler : MvcRouteHandler {}

As I mention this class will be used only for marking some routes for case if you'll need some routes to be culture independent.


Registering routes

Now lets go to Global.asax file where we have route registering methodRegisterRoutes(). Right after last route mapping add foreach statement code snippet like in the following example.

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
         "Default", // Route name
         "{controller}/{action}/{id}", // URL with parameters
         new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
    );

    foreach (Route r in routes)
    {
        if (!(r.RouteHandler is SingleCultureMvcRouteHandler))
        {
            r.RouteHandler = new MultiCultureMvcRouteHandler();
            r.Url = "{culture}/" + r.Url;
           //Adding default culture 
           if (r.Defaults == null)
           {
               r.Defaults = new RouteValueDictionary();
           }
           r.Defaults.Add("culture", Culture.ru.ToString());

           //Adding constraint for culture param
           if (r.Constraints == null)
           {
               r.Constraints = new RouteValueDictionary();
           }
           r.Constraints.Add("culture", new CultureConstraint(Culture.en.ToString(), 
Culture.ru.ToString()));
        }
   }

}


OK, lets go through this code... So for each route we first of all check whether its handler type is SingleCultureMvcRouteHandler or not... So if not we change RouteHandler property of the current route to MultiCulture one, add prefix to Url, add default culture and finally add constraint for culture param checking.

public class CultureConstraint : IRouteConstraint
{
    private string[] _values;
    public CultureConstraint(params string[] values)
    {
        this._values = values;
    }

    public bool Match(HttpContextBase httpContext,Route route,string parameterName,
                        RouteValueDictionary values, RouteDirection routeDirection)
    {

        // Get the value called "parameterName" from the 
        // RouteValueDictionary called "value"
        string value = values[parameterName].ToString();
        // Return true is the list of allowed values contains 
        // this value.
        return _values.Contains(value);

    }

}

And enum of cultures
    public enum Culture
    {
        ru = 1,
        en = 2
    }


Simple culture switching mechanism

For changing culture we'll need following simple action which I placed in AccountController

public ActionResult ChangeCulture(Culture lang, string returnUrl)
{
     if (returnUrl.Length >= 3)
     {
         returnUrl = returnUrl.Substring(3);
     }
     return Redirect("/" + lang.ToString() + returnUrl);
}

and partial view with languages links - CultureSwitchControl.ascx


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>

<%= Html.ActionLink("eng", "ChangeCulture", "Account",
    new { lang = (int)MvcLocalization.Helpers.Culture.en, returnUrl =  
    this.Request.RawUrl }, new { @class = "culture-link" })%>

<%= Html.ActionLink("рус", "ChangeCulture", "Account",
    new { lang = (int)MvcLocalization.Helpers.Culture.ru, returnUrl = 
    this.Request.RawUrl }, new { @class = "culture-link" })%>



Single culture case

Finally, if we need some single culture route all we need to do is to set RouteHandlerproperty to SingleCultureMvcRouteHandler  like this

routes.MapRoute(
          "AboutRoute",
          "About",
          new { controller = "Home", action = "About"}
   ).RouteHandler = new SingleCultureMvcRouteHandler();


So, that's it :) Localization without using Session, without problems with OutputCache(will be explained in my next post) and with use of routing.

Here is the link of  source code(project created in VS2010) 

Комментариев нет: