|
由于項(xiàng)目需要最近在學(xué)習(xí)ASP.NET MVC。在實(shí)踐中,網(wǎng)站要支持多語(yǔ)言,需要全球化。在MVC下我實(shí)現(xiàn)了一個(gè)全球化框架,在這里與各位分享一下,不足之處也請(qǐng)各位看官指教。
讓URL支持全球化
經(jīng)常上微軟網(wǎng)站的朋友可能很熟悉類(lèi)似包含../zh-cn/..、../en-us/..的url形式,這就是本文要使用的全球化方案。當(dāng)然還有使用QueryString傳遞參數(shù)的方案,基本思路我想是類(lèi)似的。
由于MVC天生的URL路由原理,使得這個(gè)方案很容易被接受。
基本思路
這個(gè)方案的基本思路是:
1.當(dāng)用戶(hù)訪(fǎng)問(wèn)的url含有合法的culture參數(shù)時(shí),能夠直接路由到對(duì)應(yīng)的controller,在controller初始化時(shí)設(shè)置線(xiàn)程的Culture;
2.當(dāng)用戶(hù)訪(fǎng)問(wèn)的url不包含culture參數(shù)時(shí),同樣被路由到對(duì)應(yīng)的controller,但controller在執(zhí)行action前,重定向到包含Culture的url。這里的Culture按照先檢測(cè)cookie,再檢測(cè)語(yǔ)言瀏覽器設(shè)置,最后使用默認(rèn)值的優(yōu)先級(jí)順序實(shí)施。
先看下效果演示,注意url,點(diǎn)擊下載例子
Resource.resx
在接下去之前先回顧一下資源文件。在ASP.NET web應(yīng)用程序(winform同樣)中定義的資源文件.resx實(shí)際上是一個(gè)xml配置文件,通常我們只關(guān)心其中的key/value配置;我們可以建立一個(gè)或多個(gè).resx,這些.resx會(huì)對(duì)應(yīng)生成一個(gè)cs文件,這個(gè)cs文件會(huì)定義一個(gè)類(lèi)(可能是Resource類(lèi),取決于你的資源文件的命名),通過(guò)訪(fǎng)問(wèn)這個(gè)類(lèi)的靜態(tài)屬性即可訪(fǎng)問(wèn)這些key,而選擇哪個(gè).resx讀取的關(guān)鍵就是CultureInfo,只要我們?cè)O(shè)置當(dāng)前線(xiàn)程的CultureInfo,Resource便會(huì)自動(dòng)識(shí)別對(duì)應(yīng)的.resx配置文件。而在.resx的命名上,需要按照這樣的規(guī)則:
Resource.zh-cn.resx(對(duì)應(yīng)簡(jiǎn)體中文資源文件)
Resource.en-us.resx(對(duì)應(yīng)美國(guó)英語(yǔ)資源文件)
中間的Culture名字很重要。
通常在開(kāi)發(fā)時(shí),只要一個(gè)默認(rèn)的Resource.resx,當(dāng)開(kāi)發(fā)完成之后,拷貝一個(gè)相同的Resource.resx,并改名字成上面的樣子,然后手動(dòng)或自動(dòng)將其中的所有value都翻譯成對(duì)應(yīng)的語(yǔ)言。
解決路由問(wèn)題
在這個(gè)方案中,首先要考慮的是url路由配置。首先,理想情況下,我們所有的url都是domain/culture/controller/action/param1/..這種形式,那么只要一份以culture開(kāi)頭的路由就可以了。但是事實(shí)上并非這么簡(jiǎn)單,如果用戶(hù)不知道這個(gè)規(guī)則,他手動(dòng)輸入了domain/controller/action/param1..那么這種url將不能被正確路由。這種情況在初次訪(fǎng)問(wèn)網(wǎng)站的時(shí)候最為常見(jiàn)(通常我們都會(huì)鍵入www.microsoft.com而不會(huì)在后面加上任何的culture參數(shù))。那么難道我們要為了這種場(chǎng)景寫(xiě)兩份路由嗎?顯然不是,或者說(shuō)不用手動(dòng)做這件事。這里要解決的第一個(gè)問(wèn)題出現(xiàn)了。我的方案是:只為domain/controller/action/param1..這種路由手動(dòng)寫(xiě)代碼配置,這也比較符合習(xí)慣;然后通過(guò)一個(gè)方法,遍歷route表中的所有路由,并在每個(gè)url規(guī)則前面加上一個(gè)參數(shù)ci表示culture,生成一份新的路由加到路由表中即可。這樣做盡管沒(méi)有減少路由規(guī)則,但是至少不用手動(dòng)一個(gè)個(gè)寫(xiě)了,要不然沒(méi)人會(huì)同意這個(gè)方案的。下面是代碼和解釋?zhuān)?/p>
protected void Application_Start(){ AreaRegistration.RegisterAllAreas(); RegisterRoutes(RouteTable.Routes); RegisterGlobalizationRoutes(); ...}
private void RegisterGlobalizationRoutes(){ //RouteTable.Routes即路由表 if (RouteTable.Routes == null) return; //創(chuàng)建一個(gè)新的路由集合,存放將要添加到路由 RouteCollection rc = new RouteCollection(); //這里需要跳過(guò)routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); //由于IgnoreRouteInternal是個(gè)私有類(lèi),所以這里只能反射 //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); //下面的代碼創(chuàng)建一個(gè)新的路由對(duì)象,在url規(guī)則前面加上ci參數(shù),并拷貝其他設(shè)置 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參數(shù)需要驗(yàn)證,因?yàn)橹挥泻戏ǖ腸ulture才能被接受 newitem.Constraints.Add("ci", new CulturePrefixRule()); newitem.DataTokens = new RouteValueDictionary(); newitem.DataTokens["Namespaces"] = item.DataTokens["Namespaces"]; rc.Add(newitem); } //帶ci參數(shù)的路由應(yīng)當(dāng)靠前放,所以這里插入到前面 foreach (var c in rc) { RouteTable.Routes.Insert(insertpoint++, c); }}
//實(shí)現(xiàn)IRouteConstraint的一個(gè)類(lèi)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; }}
這里要注意幾點(diǎn):
1.routes.IgnoreRoute("{resource}.axd/{*pathInfo}");會(huì)在路由表中添加一條IgnoreRouteInternal類(lèi)型的路由,只不過(guò)這條是需要被跳過(guò)的而已。三個(gè)類(lèi)的關(guān)系是:
RouteBase->Route->IgnoreRouteInternal
而不巧的是IgnoreRouteInternal是個(gè)私有類(lèi),因此,只能借助反射了。
2.為路由設(shè)置Constraints屬性時(shí),實(shí)際上是為其指定一個(gè)IRouteConstraint。MVC內(nèi)部有一個(gè)實(shí)現(xiàn)了IRouteConstraint的接受正則表達(dá)式的類(lèi),我們?cè)贛apRoute方法中用一個(gè)string初始化Constraints,實(shí)際上就是實(shí)例化了這個(gè)類(lèi)。而這里我們的需求顯然要復(fù)雜點(diǎn):需要判斷ci參數(shù)是否是支持的,所以也就有了CulturePrefixRule實(shí)現(xiàn)IRouteConstraint。
3.帶有ci參數(shù)的路由更“特殊”,所以最好還是放在路由表前面。原因我就不再累述了。
在Controller的Action執(zhí)行前跳轉(zhuǎn)
所有的Controller都應(yīng)該具有一個(gè)相同的行為:能夠針對(duì)沒(méi)有ci參數(shù)的url實(shí)施跳轉(zhuǎn)。因此自然想到實(shí)現(xiàn)一個(gè)基類(lèi)Controller,這里我命名為BaseController,代碼如下:
public class BaseController : Controller{ protected string redirectUrl; protected override void Initialize(System.Web.Routing.RequestContext requestContext) { base.Initialize(requestContext); object cultureValue; //檢測(cè)ci參數(shù) if (requestContext.RouteData.Values.TryGetValue("ci", out cultureValue)) { //設(shè)置當(dāng)前線(xiàn)程的culture try { Thread.CurrentThread.CurrentUICulture = CultureProvider.GetCultureInfo(cultureValue.ToString()); Thread.CurrentThread.CurrentCulture = CultureProvider.GetCultureInfo(cultureValue.ToString());
Response.Cookies.Add(new HttpCookie(CultureProvider.culturecookiekey,cultureValue.ToString())); } catch { throw new Exception("Culture Error!"); } } else //如果沒(méi)有ci參數(shù) { //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; } //設(shè)置redirectUrl,如果不需要重定向到化redirectUrl 為null redirectUrl = string.Format(@"/{0}{1}", cultureValue.ToString(), requestContext.HttpContext.Request.RawUrl); } } protected override IActionInvoker CreateActionInvoker() { return new CustomControllerActionInvoker(redirectUrl); }}//一個(gè)IActionInvoker 的實(shí)現(xiàn),MVC默認(rèn)使用ControllerActionInvoker,因?yàn)樵?/redirectUrl != null 的時(shí)候需要在action執(zhí)行之前執(zhí)行重定向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內(nèi)部不能重定向 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繼承這個(gè)BaseController即可。
這里需要重點(diǎn)指出的是CustomControllerActionInvoker類(lèi),事實(shí)上發(fā)現(xiàn)從這個(gè)類(lèi)入手解決重定向問(wèn)題花了我不少時(shí)間,為此我不得不調(diào)試MVC的源碼。當(dāng)然最初的想法是在每個(gè)action執(zhí)行時(shí)手動(dòng)判斷redirectUrl,從而指導(dǎo)重定向,但顯然,沒(méi)人愿意將自己已經(jīng)寫(xiě)好的action都拿出來(lái)一個(gè)個(gè)改,所以也就有了這個(gè)小小的探索。
頁(yè)面中的鏈接、跳轉(zhuǎn)
最后令我感到即高興又擔(dān)心的問(wèn)題是:當(dāng)我使用這個(gè)框架后,頁(yè)面中的所有鏈接和跳轉(zhuǎn)因素幾乎都能自動(dòng)在url前面加上ci參數(shù)!雖然我知道類(lèi)似Html.ActionLink之類(lèi)的helper有從路由表中產(chǎn)生url的能力,但是能夠自動(dòng)添加上ci,還是讓我感到有點(diǎn)始料未及。不過(guò),鏈接的url是否正確,還是要注意,有一些特殊情況。
頁(yè)面中使用資源
在頁(yè)面中引用資源可以直接在C#腳本中引用Resource類(lèi)。這里提供一個(gè)helper。這個(gè)Html的擴(kuò)展方法。
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); } }
需要注意的是這個(gè)方法默認(rèn)認(rèn)為Resource是資源的類(lèi)名,所以必要的話(huà)需要修改
ResourceExpressionFields fields = GetResourceFields(string.Format("Resource,{0}", expression), path); 中的"Resource,{0}"
結(jié)語(yǔ)
初學(xué)MVC,甚至可以說(shuō)是初學(xué)web開(kāi)發(fā)。以上是我個(gè)人提出的一種方案,不知道有沒(méi)有什么不足之處,還請(qǐng)各位看官提出見(jiàn)解,探討一下。
其他相關(guān)資源:
http://blog.miniASP.com/post/2010/01/ASPNET-MVC-Developer-Note-Part-15-Globalization-and-Localization.ASPx
NET技術(shù):ASP.NET MVC的全球化方案,轉(zhuǎn)載需保留來(lái)源!
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請(qǐng)第一時(shí)間聯(lián)系我們修改或刪除,多謝。