在 ASP.Net MVC 应用程序中设置文化

在 ASP.net MVC 应用程序中设置 Culture/UI Culture 的最佳位置是什么

目前我有一个 CultureController 类,它看起来像这样:

public class CultureController : Controller
{
public ActionResult SetSpanishCulture()
{
HttpContext.Session["culture"] = "es-ES";
return RedirectToAction("Index", "Home");
}


public ActionResult SetFrenchCulture()
{
HttpContext.Session["culture"] = "fr-FR";
return RedirectToAction("Index", "Home");
}
}

以及每种语文的超连结,连结如下:

<li><%= Html.ActionLink("French", "SetFrenchCulture", "Culture")%></li>
<li><%= Html.ActionLink("Spanish", "SetSpanishCulture", "Culture")%></li>

这样做很好,但我认为有一个更合适的方式来做到这一点。

我正在使用以下 ActionFilter 读取区域性 Http://www.iansuttle.com/blog/post/aspnet-mvc-action-filter-for-localized-sites.aspx.我是一个有点 MVC 菜鸟,所以没有信心,我设置在正确的地方。我不想在 web.config 级别进行,它必须基于用户的选择。我也不想检查他们的 http 头,从他们的浏览器设置中获得文化。

编辑:

只是想弄清楚-我并不是在试图决定是否使用会话。我很满意。我试图弄清楚的是,是否最好在一个 Culture 控制器中执行此操作,该控制器有一个用于设置每个 Culture 的操作方法,还是在 MVC 管道中有一个更好的位置来执行此操作?

131933 次浏览

Being as it is a setting that is stored per-user, the session is an appropriate place to store the informtion.

I would change your controller to take the culture string as a parameter, rather than having a different action method for each potential culture. Adding a link to the page is easy, and you shouldn't need to write the same code repeatedly any time a new culture is required.

public class CultureController : Controller
{
public ActionResult SetCulture(string culture)
{
HttpContext.Session["culture"] = culture
return RedirectToAction("Index", "Home");
}
}


<li><%= Html.ActionLink("French", "SetCulture", new {controller = "Culture", culture = "fr-FR"})%></li>
<li><%= Html.ActionLink("Spanish", "SetCulture", new {controller = "Culture", culture = "es-ES"})%></li>

I would do it in the Initialize event of the controller like this...

    protected override void Initialize(System.Web.Routing.RequestContext requestContext)
{
base.Initialize(requestContext);


const string culture = "en-US";
CultureInfo ci = CultureInfo.GetCultureInfo(culture);


Thread.CurrentThread.CurrentCulture = ci;
Thread.CurrentThread.CurrentUICulture = ci;
}

I'm using this localization method and added a route parameter that sets the culture and language whenever a user visits example.com/xx-xx/

Example:

routes.MapRoute("DefaultLocalized",
"{language}-{culture}/{controller}/{action}/{id}",
new
{
controller = "Home",
action = "Index",
id = "",
language = "nl",
culture = "NL"
});

I have a filter that does the actual culture/language setting:

using System.Globalization;
using System.Threading;
using System.Web.Mvc;


public class InternationalizationAttribute : ActionFilterAttribute {


public override void OnActionExecuting(ActionExecutingContext filterContext) {


string language = (string)filterContext.RouteData.Values["language"] ?? "nl";
string culture = (string)filterContext.RouteData.Values["culture"] ?? "NL";


Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(string.Format("{0}-{1}", language, culture));
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(string.Format("{0}-{1}", language, culture));


}
}

To activate the Internationalization attribute, simply add it to your class:

[Internationalization]
public class HomeController : Controller {
...

Now whenever a visitor goes to http://example.com/de-DE/Home/Index the German site is displayed.

I hope this answers points you in the right direction.

I also made a small MVC 5 example project which you can find here

Just go to http://{yourhost}:{port}/en-us/home/index to see the current date in English (US), or change it to http://{yourhost}:{port}/de-de/home/index for German etcetera.

I know this is an old question, but if you really would like to have this working with your ModelBinder (in respect to DefaultModelBinder.ResourceClassKey = "MyResource"; as well as the resources indicated in the data annotations of the viewmodel classes), the controller or even an ActionFilter is too late to set the culture.

The culture could be set in Application_AcquireRequestState, for example:

protected void Application_AcquireRequestState(object sender, EventArgs e)
{
// For example a cookie, but better extract it from the url
string culture = HttpContext.Current.Request.Cookies["culture"].Value;


Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(culture);
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(culture);
}

EDIT

Actually there is a better way using a custom routehandler which sets the culture according to the url, perfectly described by Alex Adamyan on his blog.

All there is to do is to override the GetHttpHandler method and set the culture there.

public class MultiCultureMvcRouteHandler : MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
// get culture from route data
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);
}
}

If using Subdomains, for example like "pt.mydomain.com" to set portuguese for example, using Application_AcquireRequestState won't work, because it's not called on subsequent cache requests.

To solve this, I suggest an implementation like this:

  1. Add the VaryByCustom parameter to the OutPutCache like this:

    [OutputCache(Duration = 10000, VaryByCustom = "lang")]
    public ActionResult Contact()
    {
    return View("Contact");
    }
    
  2. In global.asax.cs, get the culture from the host using a function call:

    protected void Application_AcquireRequestState(object sender, EventArgs e)
    {
    System.Threading.Thread.CurrentThread.CurrentUICulture = GetCultureFromHost();
    }
    
  3. Add the GetCultureFromHost function to global.asax.cs:

    private CultureInfo GetCultureFromHost()
    {
    CultureInfo ci = new CultureInfo("en-US"); // en-US
    string host = Request.Url.Host.ToLower();
    if (host.Equals("mydomain.com"))
    {
    ci = new CultureInfo("en-US");
    }
    else if (host.StartsWith("pt."))
    {
    ci = new CultureInfo("pt");
    }
    else if (host.StartsWith("de."))
    {
    ci = new CultureInfo("de");
    }
    else if (host.StartsWith("da."))
    {
    ci = new CultureInfo("da");
    }
    
    
    return ci;
    }
    
  4. And finally override the GetVaryByCustomString(...) to also use this function:

    public override string GetVaryByCustomString(HttpContext context, string value)
    {
    if (value.ToLower() == "lang")
    {
    CultureInfo ci = GetCultureFromHost();
    return ci.Name;
    }
    return base.GetVaryByCustomString(context, value);
    }
    

The function Application_AcquireRequestState is called on non-cached calls, which allows the content to get generated and cached. GetVaryByCustomString is called on cached calls to check if the content is available in cache, and in this case we examine the incoming host domain value, again, instead of relying on just the current culture info, which could have changed for the new request (because we are using subdomains).

protected void Application_AcquireRequestState(object sender, EventArgs e)
{
if(Context.Session!= null)
Thread.CurrentThread.CurrentCulture =
Thread.CurrentThread.CurrentUICulture = (Context.Session["culture"] ?? (Context.Session["culture"] = new CultureInfo("pt-BR"))) as CultureInfo;
}

What is the best place is your question. The best place is inside the Controller.Initialize method. MSDN writes that it is called after the constructor and before the action method. In contrary of overriding OnActionExecuting, placing your code in the Initialize method allow you to benefit of having all custom data annotation and attribute on your classes and on your properties to be localized.

For example, my localization logic come from an class that is injected to my custom controller. I have access to this object since Initialize is called after the constructor. I can do the Thread's culture assignation and not having every error message displayed correctly.

 public BaseController(IRunningContext runningContext){/*...*/}


protected override void Initialize(RequestContext requestContext)
{
base.Initialize(requestContext);
var culture = runningContext.GetCulture();
Thread.CurrentThread.CurrentUICulture = culture;
Thread.CurrentThread.CurrentCulture = culture;
}

Even if your logic is not inside a class like the example I provided, you have access to the RequestContext which allow you to have the URL and HttpContext and the RouteData which you can do basically any parsing possible.

1: Create a custom attribute and override method like this:

public class CultureAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// Retreive culture from GET
string currentCulture = filterContext.HttpContext.Request.QueryString["culture"];


// Also, you can retreive culture from Cookie like this :
//string currentCulture = filterContext.HttpContext.Request.Cookies["cookie"].Value;


// Set culture
Thread.CurrentThread.CurrentCulture = new CultureInfo(currentCulture);
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(currentCulture);
}
}

2: In App_Start, find FilterConfig.cs, add this attribute. (this works for WHOLE application)

public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
// Add custom attribute here
filters.Add(new CultureAttribute());
}
}

That's it !

If you want to define culture for each controller/action in stead of whole application, you can use this attribute like this:

[Culture]
public class StudentsController : Controller
{
}

Or:

[Culture]
public ActionResult Index()
{
return View();
}