• ASP.NET MVC的全球化方案 (转)


    于项目需要最近在学习ASP.NET MVC。在实践中,网站要支持多语言,需要全球化。在MVC下我实现了一个全球化框架,在这里与各位分享一下,不足之处也请各位看官指教。

    让URL支持全球化

    经常上微软网站的朋友可能很熟悉类似包含..\zh-cn\..、..\en-us\..的url形式,这就是本文要使用的全球化方案。当然还有使用QueryString传递参数的方案,基本思路我想是类似的。

    由于MVC天生的URL路由原理,使得这个方案很容易被接受。

    基本思路

    这个方案的基本思路是:

    1.当用户访问的url含有合法的culture参数时,能够直接路由到对应的controller,在controller初始化时设置线程的Culture;

    2.当用户访问的url不包含culture参数时,同样被路由到对应的controller,但controller在执行action前,重定向到包含Culture的url。这里的Culture按照先检测cookie,再检测语言浏览器设置,最后使用默认值的优先级顺序实施。

    先看下效果演示,注意url,点击下载例子

    image

    image

    Resource.resx

    在接下去之前先回顾一下资源文件。在asp.net web应用程序(winform同样)中定义的资源文件.resx实际上是一个xml配置文件,通常我们只关心其中的key\value配置;我们可以建立一个或多个.resx,这些.resx会对应生成一个cs文件,这个cs文件会定义一个类(可能是Resource类,取决于你的资源文件的命名),通过访问这个类的静态属性即可访问这些key,而选择哪个.resx读取的关键就是CultureInfo,只要我们设置当前线程的CultureInfo,Resource便会自动识别对应的.resx配置文件。而在.resx的命名上,需要按照这样的规则:

    Resource.zh-cn.resx(对应简体中文资源文件)

    Resource.en-us.resx(对应美国英语资源文件)

    中间的Culture名字很重要。

    通常在开发时,只要一个默认的Resource.resx,当开发完成之后,拷贝一个相同的Resource.resx,并改名字成上面的样子,然后手动或自动将其中的所有value都翻译成对应的语言。

    解决路由问题

    在这个方案中,首先要考虑的是url路由配置。首先,理想情况下,我们所有的url都是domain/culture/controller/action/param1/..这种形式,那么只要一份以culture开头的路由就可以了。但是事实上并非这么简单,如果用户不知道这个规则,他手动输入了domain/controller/action/param1..那么这种url将不能被正确路由。这种情况在初次访问网站的时候最为常见(通常我们都会键入http://www.microsoft.com/而不会在后面加上任何的culture参数)。那么难道我们要为了这种场景写两份路由吗?显然不是,或者说不用手动做这件事。这里要解决的第一个问题出现了。我的方案是:只为domain/controller/action/param1..这种路由手动写代码配置,这也比较符合习惯;然后通过一个方法,遍历route表中的所有路由,并在每个url规则前面加上一个参数ci表示culture,生成一份新的路由加到路由表中即可。这样做尽管没有减少路由规则,但是至少不用手动一个个写了,要不然没人会同意这个方案的。下面是代码和解释:

    1 protected void Application_Start()
    2 {
    3     AreaRegistration.RegisterAllAreas();
    4     RegisterRoutes(RouteTable.Routes);
    5     RegisterGlobalizationRoutes();
    6     ...
    7 }
    01 private void RegisterGlobalizationRoutes()
    02 {
    03     //RouteTable.Routes即路由表
    04     if (RouteTable.Routes == null)
    05         return;
    06     //创建一个新的路由集合,存放将要添加到路由
    07     RouteCollection rc = new RouteCollection();
    08   
    09     //这里需要跳过routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    10     //由于IgnoreRouteInternal是个私有类,所以这里只能反射
    11     //skip IgnoreRouteInternal
    12     var routes = RouteTable.Routes.SkipWhile(p => (p.GetType().Name == "IgnoreRouteInternal"));
    13   
    14     int insertpoint = RouteTable.Routes.Count() - routes.Count();
    15   
    16     //遍历所有需要处理的路由
    17     foreach (var r in routes)
    18     {
    19         Route item = (r as Route);
    20         //下面的代码创建一个新的路由对象,在url规则前面加上ci参数,并拷贝其他设置
    21         Route newitem = new Route(
    22             //string.Format(@"{ci}/{0}",item.Url),
    23             @"{ci}/" + item.Url,
    24             new MvcRouteHandler());
    25         newitem.Defaults = new RouteValueDictionary(item.Defaults);
    26         newitem.Constraints = new RouteValueDictionary(item.Constraints);
    27         //ci参数需要验证,因为只有合法的culture才能被接受
    28         newitem.Constraints.Add("ci", new CulturePrefixRule());
    29         newitem.DataTokens = new RouteValueDictionary();
    30         newitem.DataTokens["Namespaces"] = item.DataTokens["Namespaces"];
    31         rc.Add(newitem);
    32     }
    33     //带ci参数的路由应当靠前放,所以这里插入到前面
    34     foreach (var c in rc)
    35     {
    36         RouteTable.Routes.Insert(insertpoint++, c);
    37     }
    38 }
    01 //实现IRouteConstraint的一个类
    02 private class CulturePrefixRule : IRouteConstraint
    03 {
    04     IEnumerable<string> cultureConllection = CultureInfo.GetCultures(CultureTypes.SpecificCultures).Select(p => p.Name.ToLower());
    05     public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    06     {
    07         if (values[parameterName] != null)
    08             return cultureConllection.Contains(values[parameterName].ToString().ToLower());
    09         else
    10             return false;
    11     }
    12 }

    这里要注意几点:

    1.routes.IgnoreRoute("{resource}.axd/{*pathInfo}");会在路由表中添加一条IgnoreRouteInternal类型的路由,只不过这条是需要被跳过的而已。三个类的关系是:

    RouteBase->Route->IgnoreRouteInternal

    而不巧的是IgnoreRouteInternal是个私有类,因此,只能借助反射了。

    2.为路由设置Constraints属性时,实际上是为其指定一个IRouteConstraint。MVC内部有一个实现了IRouteConstraint的接受正则表达式的类,我们在MapRoute方法中用一个string初始化Constraints,实际上就是实例化了这个类。而这里我们的需求显然要复杂点:需要判断ci参数是否是支持的,所以也就有了CulturePrefixRule实现IRouteConstraint。

    3.带有ci参数的路由更“特殊”,所以最好还是放在路由表前面。原因我就不再累述了。

      

    在Controller的Action执行前跳转

    所有的Controller都应该具有一个相同的行为:能够针对没有ci参数的url实施跳转。因此自然想到实现一个基类Controller,这里我命名为BaseController,代码如下:

    01 public class BaseController : Controller
    02 {
    03     protected string redirectUrl;
    04   
    05     protected override void Initialize(System.Web.Routing.RequestContext requestContext)
    06     {
    07         base.Initialize(requestContext);
    08         object cultureValue;
    09         //检测ci参数
    10         if (requestContext.RouteData.Values.TryGetValue("ci", out cultureValue))
    11         {
    12             //设置当前线程的culture
    13             try
    14             {
    15                 Thread.CurrentThread.CurrentUICulture = CultureProvider.GetCultureInfo(cultureValue.ToString());
    16                 Thread.CurrentThread.CurrentCulture = CultureProvider.GetCultureInfo(cultureValue.ToString());
    01                 Response.Cookies.Add(new HttpCookie(CultureProvider.culturecookiekey,cultureValue.ToString()));
    02             }
    03             catch { throw new Exception("Culture Error!"); }
    04         }
    05         else //如果没有ci参数
    06         {
    07             //check cookie
    08             HttpCookie cLang = requestContext.HttpContext.Request.Cookies[CultureProvider.culturecookiekey];
    09             if (cLang != null)
    10             {
    11                 cultureValue = cLang.Value;
    12             }
    13             else //check brower setting
    14             {
    15                 string[] langs = requestContext.HttpContext.Request.UserLanguages;
    16                 if (langs != null && langs.Length > 0)
    17                 {
    18                     cultureValue = langs[0].Split(';').First();
    19                 }
    20             }
    21   
    22             if (cultureValue == null)
    23             {
    24                 cultureValue = CultureProvider.culturedefault;
    25             }
    26             //设置redirectUrl,如果不需要重定向到化redirectUrl 为null
    27             redirectUrl = string.Format(@"/{0}{1}",
    28                 cultureValue.ToString(),
    29                 requestContext.HttpContext.Request.RawUrl);
    30   
    31         }
    32     }
    33   
    34     protected override IActionInvoker CreateActionInvoker()
    35     {
    36         return new CustomControllerActionInvoker(redirectUrl);
    37     }
    38 }
    39   
    40 //一个IActionInvoker 的实现,MVC默认使用ControllerActionInvoker,因为在
    41 //redirectUrl != null 的时候需要在action执行之前执行重定向
    42 internal class CustomControllerActionInvoker : ControllerActionInvoker
    43 {
    44     string redirectUrl;
    45     public CustomControllerActionInvoker(string url)
    46         : base()
    47     {
    48         redirectUrl = url;
    49     }
    50     protected override ActionResult InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters)
    51     {
    52         object returnValue;
    53        //ChildAction内部不能重定向
    54         if (!string.IsNullOrEmpty(redirectUrl) && !controllerContext.IsChildAction)
    55             returnValue = new RedirectResult(redirectUrl);
    56         else
    57             returnValue = actionDescriptor.Execute(controllerContext, parameters);
    58         ActionResult result = CreateActionResult(controllerContext, actionDescriptor, returnValue);
    59         return result;
    60     }
    61 }
    62   
    63 public static class CultureProvider
    64 {
    65     public const string culturecookiekey = "Lang";
    66     public const string culturedefault = "en-US";
    67   
    68     public static CultureInfo GetCultureInfo(string ci)
    69     {
    70         try
    71         {
    72             return new CultureInfo(ci);
    73         }
    74         catch
    75         {
    76             return null;
    77         }
    78     }
    79 }

    只要所有的Controller继承这个BaseController即可。

    这里需要重点指出的是CustomControllerActionInvoker类,事实上发现从这个类入手解决重定向问题花了我不少时间,为此我不得不调试MVC的源码。当然最初的想法是在每个action执行时手动判断redirectUrl,从而指导重定向,但显然,没人愿意将自己已经写好的action都拿出来一个个改,所以也就有了这个小小的探索。

    页面中的链接、跳转

    最后令我感到即高兴又担心的问题是:当我使用这个框架后,页面中的所有链接和跳转因素几乎都能自动在url前面加上ci参数!虽然我知道类似Html.ActionLink之类的helper有从路由表中产生url的能力,但是能够自动添加上ci,还是让我感到有点始料未及。不过,链接的url是否正确,还是要注意,有一些特殊情况。

    页面中使用资源

    在页面中引用资源可以直接在C#脚本中引用Resource类。这里提供一个helper。这个Html的扩展方法。

    01 public static class ResourceExtensions
    02 {
    03     public static string Resource(this Controller controller, string expression, params object[] args)
    04     {
    05         ResourceExpressionFields fields = GetResourceFields(expression, "~/");
    06         return GetGlobalResource(fields, args);
    07     }
    08  
    09     public static string Resource(this HtmlHelper htmlHelper, string expression, params object[] args)
    10     {
    11         string path = "~/";
    12         ResourceExpressionFields fields = GetResourceFields(string.Format("Resource,{0}", expression), path);
    13         return GetGlobalResource(fields, args);
    14     }
    15  
    16     static string GetGlobalResource(ResourceExpressionFields fields, object[] args)
    17     {
    18         return string.Format((string)HttpContext.GetGlobalResourceObject(fields.ClassKey, fields.ResourceKey, CultureInfo.CurrentUICulture), args);
    19     }
    20  
    21     static ResourceExpressionFields GetResourceFields(string expression, string virtualPath)
    22     {
    23         var context = new ExpressionBuilderContext(virtualPath);
    24         var builder = new ResourceExpressionBuilder();
    25         return (ResourceExpressionFields)builder.ParseExpression(expression, typeof(string), context);
    26     }
    27 }

    需要注意的是这个方法默认认为Resource是资源的类名,所以必要的话需要修改

  • 相关阅读:
    Qt error: ‘class Ui::XXXXX‘ has no member named ‘XXXXX‘
    Visual Studio 模块计算机类型“x64”与目标计算机类型“x86”冲突
    Qt 报错:error dependent 'xxx' does not exist.
    Qt 可重入和线程安全的理解
    光学显微镜的一些技术参数
    C++ 内联函数解析(inline)
    显微镜的景深
    Windows下的Qt编译器 MinGW和MSVC的区别
    C/C++ 回调函数(Callback)& 函数指针
    C++ 回调函数详解
  • 原文地址:https://www.cnblogs.com/luluping/p/1867040.html
Copyright © 2020-2023  润新知