在 Area 中使用RouteAttribute 定义路由, 并支持多语言

业务上的一个需求, 同一页面, 两种不同的使用方法, 为了区分这两种需求, 需要加一个参数到 URL 中,
不改路由的话, 是这样:

http://localhost:16269/en-US/Forwarder/Bargain/Create/G20150911000009?from=FAK

虽然不是处女座的, 但是我想把地址变成这样:

http://localhost:16269/en-US/Forwarder/Bargain/FAK/Create/G20150911000009

 

改路由表分分钟的事, 但是每个特殊的业务都去改一下路由表, 那也太蛋疼了.

MVC 中有 RouteAttribute , 在不分 Area 的系统中使用过, 很简单.
具体可参考:

http://blogs.msdn.com/b/webdev/archive/2013/10/17/attribute-routing-in-asp-net-mvc-5.aspx#route-areas

 

今天头一次在 Area 中添加这个东西, 搞的有些错乱.

1, RouteAttribute 配合 RouteAreaAttribute 不起作用

这是因为 MapMvcAttributeRoutes() 方法放在 RegisterAllAreas 后面调用了, 两个调整一下顺序就可以了, 不需在要每个 Area 下面的 AreaRegistration 中调用, 总的调用一次就可行了.

正确的写法是在RouteConfig 中这样:

1     public class RouteConfig {
2         public static void RegisterRoutes(RouteCollection routes) {
3             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
4 
5             RouteTable.Routes.MapMvcAttributeRoutes();           
6             AreaRegistration.RegisterAllAreas();
7 ...
8 ...

 

2, 正确的路由数据顺序

 这个地址: 

http://localhost:16269/en-US/Forwarder/Bargain/FAK/Create/G20150911000009 

的路由的 Template 应该是这样的:

{lang}/{area}/{controller}/{from}/{action}/{id}

 

 

一开始没有把握要领, 搞成这样:

http://localhost:16269/Forwarder/en-US/Bargain/FAK/Create/G20150911000009
http://localhost:16269/Forwarder/en-US/Forwarder/Bargain/FAK/Create/G20150911000009

 

 

过程就是一点点的试试, 不在赘述,
正确的写法应该是这样:

1     [RouteArea("Forwarder", AreaPrefix = "{lang=zh-CN}/Forwarder")]
2     [RoutePrefix("Bargain")]
3     public class BargainController : BaseController {
4 ...
5 ...
6         [Route("{from=FAK}/Create/{id}")]
7         public ActionResult Create([Required]string id, string from = "FAK") {
8 ...

 

 

 

3, 多语言不起作用

先来看一眼怎么处理多语言的:

 1     public class MutiLangRouteHandler : MvcRouteHandler {
 2 
 3         protected override System.Web.IHttpHandler GetHttpHandler(RequestContext requestContext) {
 4             var handler = base.GetHttpHandler(requestContext);
 5             string lang = requestContext.RouteData.Values["lang"].ToString();
 6             try {
 7                 var culture = CultureInfo.GetCultureInfo(lang);
 8                 Thread.CurrentThread.CurrentUICulture = culture;
 9             } catch {
10 
11             }
12 
13             return handler;
14         }
15 
16     }

在这个 Handler 中, 会取路由中的 lang , 然后尝试设置 UICulture 为指定语言.

在注册路由的时候, 要指定 Route 的 RouteHandler 为这个 MutiLangRouteHandler

 1             routes.Add(new Route("{lang}/{controller}/{action}/{id}",
 2                             new RouteValueDictionary(new {
 3                                 lang = "zh-CN",
 4                                 controller = "Home",
 5                                 action = "Index",
 6                                 id = UrlParameter.Optional
 7                             }),
 8                             new RouteValueDictionary(new {
 9                                 lang = "(zh-CN)|(en-US)"
10                             }),
11                             new MutiLangRouteHandler()));

Area 中的路由要这样:

 1             var r2 = context.MapRoute(
 2                 "Forwarder_default1",
 3                 "Forwarder/{controller}/{action}/{id}",
 4                 new {
 5                     lang = "zh-CN",
 6                     action = "Index",
 7                     id = UrlParameter.Optional
 8                 },
 9                 new {
10                     lang = "(zh-CN)|(en-US)"
11                 }
12             );
13 
14             var handler = new MutiLangRouteHandler();
15             r1.RouteHandler = handler;
16             r2.RouteHandler = handler;

 

 

上面虽然用 RouteAttribute 配置好了路由, 但是那只是表面上的, 根本就不会执行到自定义的 多语言处理器 (MutiLangRouteHandler)
原因很简单啊, RouteTable.Routes.MapMvcAttributeRoutes() 生成的路由使有的是 默认的 MvcRouteHandler, 它里面肯定不会处理多语言啦 .

RouteTable.Routes.MapMvcAttributeRoutes()之后, 在调用工具中可以看到 路由表中已经添加由 RouteAttribute 生成的路由, 但是报歉, 包装它的是一个 internal 的类, 所以无法获取这些路由, 更无法给这些路由设置 RouteHandler.

在 Area 中使用RouteAttribute 定义路由, 并支持多语言_第1张图片

 

怎么办呢? 扩展 RouteAttribute ? 但是这个类是 sealed 的, 无法扩展.
看一下源码, 它实现了 IDirectRouteFactory 接口, 这个接口有一个 CreateRoute 方法, 返回 RouteEntity

 1         RouteEntry IDirectRouteFactory.CreateRoute(DirectRouteFactoryContext context)
 2         {
 3             Contract.Assert(context != null);
 4 
 5             IDirectRouteBuilder builder = context.CreateBuilder(Template);
 6             Contract.Assert(builder != null);
 7 
 8             builder.Name = Name;
 9             builder.Order = Order;
10             return builder.Build();
11         }

RouteEntity 中有个 Route, 也就是说, 只要获取到这个 RouteEntity , 就可以给它生成的路由加 RouteHandler 了.

 

于是我这样定义:

 1     /// <summary>
 2     /// 
 3     /// </summary>
 4     [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
 5     public class RouteWithHandlerAttribute : Attribute, IDirectRouteFactory, IRouteInfoProvider {
 6 
 7 ...
 8 ...
 9 
10         public Type HandlerType {
11             get;
12             set;
13         }
14 
15         /// <summary>
16         /// 
17         /// </summary>
18         /// <param name="handlerType"></param>
19         /// <param name="template"></param>
20         public RouteWithHandlerAttribute(Type handlerType, string template = "") {
21             if (handlerType == null)
22                 throw new ArgumentNullException("handlerType");
23 
24             if (!(handlerType.GetInterfaces().Contains(typeof(IRouteHandler))
25                 && handlerType.GetConstructor(Type.EmptyTypes) != null)
26             )
27                 throw new ArgumentException("handerType 必须是 IRouteHandler 的子类, 必须有无参构造函数");
28 
29             if (template == null)//可以为空字符串
30                 throw new ArgumentNullException("template");
31 
32             this.Template = template;
33             this.HandlerType = handlerType;
34         }
35 
36 
37         /// <summary>
38         /// 
39         /// </summary>
40         /// <param name="context"></param>
41         /// <returns></returns>
42         RouteEntry IDirectRouteFactory.CreateRoute(DirectRouteFactoryContext context) {
43             if (context == null)
44                 throw new ArgumentNullException("context");
45 
46             var handler = (IRouteHandler)Activator.CreateInstance(this.HandlerType);
47 
48  ...
49 ...
50 
51             IDirectRouteBuilder builder = context.CreateBuilder(Template);
52 ...
53 ...
54 
55             var entry = builder.Build();
56             entry.Route.RouteHandler = handler;
57 
58             return entry;
59         }
60     }

 

即在返回之前给 entry.Route.RouteHandler 赋值.

 

看似很完美, 结果运行就报错:

[InvalidOperationException: 直接路由不支持按路由路由处理程序。]
   System.Web.Mvc.Routing.DirectRouteBuilder.ValidateRouteEntry(RouteEntry entry) +245

 

直接路不支持路由处理程序啊...功夫白搭了

看来在 RouteAttribute 上下功夫是没用了, 换其它方法吧.

 

我在系统中写了一堆 ActionFilter, 用于判断权限啦 , Action 参数完整性啦, 错误处理等等, 对这个东西还是有过深入了解的.

ActionFilterAttribute 提供了 OnActionExecuting, 会在 Action 执行前执行, 我可以借助这个方法来处理多语言:

 1     public class MutiLangAttribute : ActionFilterAttribute {
 2 
 3         public override void OnActionExecuting(ActionExecutingContext filterContext) {
 4             //不能直接调用 Handler, 会报以下错误:
 5             // 只能在引发“HttpApplication.AcquireRequestState”之前调用“HttpContext.SetSessionStateBehavior”。
 6             //IRouteHandler handler = new MutiLangRouteHandler();
 7             //handler.GetHttpHandler(filterContext.RequestContext).ProcessRequest(HttpContext.Current);
 8 
 9             string lang = filterContext.RequestContext.RouteData.Values["lang"].ToString();
10             try {
11                 var culture = CultureInfo.GetCultureInfo(lang);
12                 Thread.CurrentThread.CurrentUICulture = culture;
13             } catch {
14 
15             }
16 
17         }
18 
19     }

 

 

OK, Action 上这样写:

1         [Route("{from=FAK}/Create/{id}"), MutiLang]
2         public ActionResult Create([Required]string id, string from = "FAK") {

 

即加一个 RouteAttribute, 另外在加上 MutiLangAttribute

 

OK, 多语言的问题基本上处理完毕了, 除了一个例外: DisplayModel

 

4, DisplayMode 中取不出 RouteData 中的 lang

DisplayMode 是 MVC4 中增加的功能,  我用它来做这样的事:

en-Us 的时候, 显示英文字段, zh-CN 的时候, 显示中文字段. 当然,它可以处理的事多去了, 只要脑洞够大.

 1     public class DisplayModelSetting {
 2 
 3         public static void Config() {
 4 
 5             DisplayModeProvider.Instance.Modes.Insert(0, new DefaultDisplayMode("en-US") {
 6                 ContextCondition = ctx => {
 7                     var data = RouteTable.Routes.GetRouteData(ctx);
 8                     if (data != null) {
 9                         var lang = (string)data.Values.Get("lang", "");
10                         return string.Equals(lang, "en-US", StringComparison.OrdinalIgnoreCase);
11                     }
12                     return false;
13                 }
14             });
15 
16             var config = ConfigurationHelper.GetSection<CustomDomainsConfig>();
17             if (config != null && config.Domains != null) {
18                 //匹配自定义域名, 以达到不同公司, 显示不同界面的功能.
19                 //自定义域名在 Configs/CustomDomains.config 中定义
20                 config.Domains.Cast<CustomDomainItem>().ToList().ForEach(c => {
21                     DisplayModeProvider.Instance.Modes.Insert(0, new DefaultDisplayMode(c.View) {
22                         ContextCondition = ctx => {
23                             return string.Equals(ctx.Request.Url.Host, c.Domain, StringComparison.OrdinalIgnoreCase);
24                         }
25                     });
26                 });
27             }
28         }
29 
30     }

 

结果运行发现, 无论是何种语言, (string)data.Values.Get("lang", "") 返回的一直是空字符串.

在调试器中发现:

在 Area 中使用RouteAttribute 定义路由, 并支持多语言_第2张图片

 

RouteData 中还包含一个 RouteData 集合,  lang 是在这个集合中, 所以上面的代码无法直接取出.

修改成这样:

 1             DisplayModeProvider.Instance.Modes.Insert(0, new DefaultDisplayMode("en-US") {
 2                 ContextCondition = ctx => {
 3                     var data = RouteTable.Routes.GetRouteData(ctx);
 4                     if (data != null) {
 5                         RouteValueDictionary rdic = data.Values;
 6                         //适用于 RouteAttribute
 7                         var routeData = data.Values.Get("MS_DirectRouteMatches", null);
 8                         if (routeData != null) {
 9                             rdic = ((IEnumerable<System.Web.Routing.RouteData>)routeData).First().Values;
10                         }
11 
12                         var lang = (string)rdic.Get("lang", "");
13                         return string.Equals(lang, "en-US", StringComparison.OrdinalIgnoreCase);
14                     }
15                     return false;
16                 }
17             });

 

OK , DisplayMode 的问题也解决了!

-----------------------

总结: 挺曲折的, 其它没有.

完.

 

你可能感兴趣的:(在 Area 中使用RouteAttribute 定义路由, 并支持多语言)