业务上的一个需求, 同一页面, 两种不同的使用方法, 为了区分这两种需求, 需要加一个参数到 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 中添加这个东西, 搞的有些错乱.
这是因为 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 ...
这个地址:
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 ...
先来看一眼怎么处理多语言的:
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.
怎么办呢? 扩展 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
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", "") 返回的一直是空字符串.
在调试器中发现:
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 的问题也解决了!
-----------------------
总结: 挺曲折的, 其它没有.
完.