由于项目需要最近在学习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,点击下载例子
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将不能被正确路由。这种情况在初次访问网站的时候最为常见(通常我们都会键入www.microsoft.com而不会在后面加上任何的culture参数)。那么难道我们要为了这种场景写两份路由吗?显然不是,或者说不用手动做这件事。这里要解决的第一个问题出现了。我的方案是:只为domain/controller/action/param1..这种路由手动写代码配置,这也比较符合习惯;然后通过一个方法,遍历route表中的所有路由,并在每个url规则前面加上一个参数ci表示culture,生成一份新的路由加到路由表中即可。这样做尽管没有减少路由规则,但是至少不用手动一个个写了,要不然没人会同意这个方案的。下面是代码和解释:
1 2 3 4 5 6 7 | protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterRoutes(RouteTable.Routes); RegisterGlobalizationRoutes(); ... } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | private void RegisterGlobalizationRoutes() { //RouteTable.Routes即路由表 if (RouteTable.Routes == null ) return ; //创建一个新的路由集合,存放将要添加到路由 RouteCollection rc = new RouteCollection(); //这里需要跳过routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); //由于IgnoreRouteInternal是个私有类,所以这里只能反射 //skip IgnoreRouteInternal var routes = RouteTable.Routes.SkipWhile(p => (p.GetType().Name == "IgnoreRouteInternal" )); int insertpoint = RouteTable.Routes.Count() - routes.Count(); //遍历所有需要处理的路由 foreach ( var r in routes) { Route item = (r as Route); //下面的代码创建一个新的路由对象,在url规则前面加上ci参数,并拷贝其他设置 Route newitem = new Route( //string.Format(@"{ci}/{0}",item.Url), @"{ci}/" + item.Url, new MvcRouteHandler()); newitem.Defaults = new RouteValueDictionary(item.Defaults); newitem.Constraints = new RouteValueDictionary(item.Constraints); //ci参数需要验证,因为只有合法的culture才能被接受 newitem.Constraints.Add( "ci" , new CulturePrefixRule()); newitem.DataTokens = new RouteValueDictionary(); newitem.DataTokens[ "Namespaces" ] = item.DataTokens[ "Namespaces" ]; rc.Add(newitem); } //带ci参数的路由应当靠前放,所以这里插入到前面 foreach ( var c in rc) { RouteTable.Routes.Insert(insertpoint++, c); } } |
1 | |
1 2 3 4 5 6 7 8 9 10 11 12 | //实现IRouteConstraint的一个类 private class CulturePrefixRule : IRouteConstraint { IEnumerable< string > cultureConllection = CultureInfo.GetCultures(CultureTypes.SpecificCultures).Select(p => p.Name.ToLower()); public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { if (values[parameterName] != null ) return cultureConllection.Contains(values[parameterName].ToString().ToLower()); else return false ; } } |
这里要注意几点:
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,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class BaseController : Controller { protected string redirectUrl; protected override void Initialize(System.Web.Routing.RequestContext requestContext) { base .Initialize(requestContext); object cultureValue; //检测ci参数 if (requestContext.RouteData.Values.TryGetValue( "ci" , out cultureValue)) { //设置当前线程的culture try { Thread.CurrentThread.CurrentUICulture = CultureProvider.GetCultureInfo(cultureValue.ToString()); Thread.CurrentThread.CurrentCulture = CultureProvider.GetCultureInfo(cultureValue.ToString()); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | Response.Cookies.Add( new HttpCookie(CultureProvider.culturecookiekey,cultureValue.ToString())); } catch { throw new Exception( "Culture Error!" ); } } else //如果没有ci参数 { //check cookie HttpCookie cLang = requestContext.HttpContext.Request.Cookies[CultureProvider.culturecookiekey]; if (cLang != null ) { cultureValue = cLang.Value; } else //check brower setting { string [] langs = requestContext.HttpContext.Request.UserLanguages; if (langs != null && langs.Length > 0) { cultureValue = langs[0].Split( ';' ).First(); } } if (cultureValue == null ) { cultureValue = CultureProvider.culturedefault; } //设置redirectUrl,如果不需要重定向到化redirectUrl 为null redirectUrl = string .Format( @"/{0}{1}" , cultureValue.ToString(), requestContext.HttpContext.Request.RawUrl); } } protected override IActionInvoker CreateActionInvoker() { return new CustomControllerActionInvoker(redirectUrl); } } //一个IActionInvoker 的实现,MVC默认使用ControllerActionInvoker,因为在 //redirectUrl != null 的时候需要在action执行之前执行重定向 internal class CustomControllerActionInvoker : ControllerActionInvoker { string redirectUrl; public CustomControllerActionInvoker( string url) : base () { redirectUrl = url; } protected override ActionResult InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary< string , object > parameters) { object returnValue; //ChildAction内部不能重定向 if (! string .IsNullOrEmpty(redirectUrl) && !controllerContext.IsChildAction) returnValue = new RedirectResult(redirectUrl); else returnValue = actionDescriptor.Execute(controllerContext, parameters); ActionResult result = CreateActionResult(controllerContext, actionDescriptor, returnValue); return result; } } public static class CultureProvider { public const string culturecookiekey = "Lang" ; public const string culturedefault = "en-US" ; public static CultureInfo GetCultureInfo( string ci) { try { return new CultureInfo(ci); } catch { return null ; } } } |
只要所有的Controller继承这个BaseController即可。
这里需要重点指出的是CustomControllerActionInvoker类,事实上发现从这个类入手解决重定向问题花了我不少时间,为此我不得不调试MVC的源码。当然最初的想法是在每个action执行时手动判断redirectUrl,从而指导重定向,但显然,没人愿意将自己已经写好的action都拿出来一个个改,所以也就有了这个小小的探索。
页面中的链接、跳转
最后令我感到即高兴又担心的问题是:当我使用这个框架后,页面中的所有链接和跳转因素几乎都能自动在url前面加上ci参数!虽然我知道类似Html.ActionLink之类的helper有从路由表中产生url的能力,但是能够自动添加上ci,还是让我感到有点始料未及。不过,链接的url是否正确,还是要注意,有一些特殊情况。
页面中使用资源
在页面中引用资源可以直接在C#脚本中引用Resource类。这里提供一个helper。这个Html的扩展方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | public static class ResourceExtensions { public static string Resource( this Controller controller, string expression, params object [] args) { ResourceExpressionFields fields = GetResourceFields(expression, "~/" ); return GetGlobalResource(fields, args); } public static string Resource( this HtmlHelper htmlHelper, string expression, params object [] args) { string path = "~/" ; ResourceExpressionFields fields = GetResourceFields( string .Format( "Resource,{0}" , expression), path); return GetGlobalResource(fields, args); } static string GetGlobalResource(ResourceExpressionFields fields, object [] args) { return string .Format(( string )HttpContext.GetGlobalResourceObject(fields.ClassKey, fields.ResourceKey, CultureInfo.CurrentUICulture), args); } static ResourceExpressionFields GetResourceFields( string expression, string virtualPath) { var context = new ExpressionBuilderContext(virtualPath); var builder = new ResourceExpressionBuilder(); return (ResourceExpressionFields)builder.ParseExpression(expression, typeof ( string ), context); } } |
需要注意的是这个方法默认认为Resource是资源的类名,所以必要的话需要修改
ResourceExpressionFields fields = GetResourceFields(string.Format("Resource,{0}", expression), path); 中的"Resource,{0}"
结语
初学MVC,甚至可以说是初学web开发。以上是我个人提出的一种方案,不知道有没有什么不足之处,还请各位看官提出见解,探讨一下。
其他相关资源:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?