Asp.Net MVC实践 - 探索UrlRouting并分析UrlHelper (基于ASP.NET MVC Preview 3)

使用asp.net mvc以来,UrlRouting的处理就是一个非常关键的问题,由于使用的不小心,经常导致我们无法得到预期的结果,这的确是个很麻烦的问题,于是很多朋友推测是MVC框架的bug,到底事实如何呢?今天我便尽力探索系统中UrlRouting到底是如何工作的,希望能找出问题的关键.
总所周知,Asp.Net MVC框架一般使用Global.asax在程序第一次启动的时候初始化RouteCollection,在Preview3中,我们一般使用RouteCollection. MapRoute方法来添加新的规则.然后,系统理论上会非常听话执行我们给出的规则,然后我们直接或者间接在页面中使用UrlHelper提供的方法处理Url,UrlHelper使用路由而非路径的方式定义url,能给我们更大的方便,但是问题来了,很多朋友发现UrlHelper并不是那么听话的,可以说有时候会给出一个莫名其妙的地址.为了解开这个问题,我们得先看看系统到底怎么来处理这些规则的.
首先我们把这个MapRoute方法找出来,查询源代码:
C#代码
  1. public static void MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints) {   
  2.     if (routes == null) {   
  3.         throw new ArgumentNullException("routes");   
  4.     }   
  5.     if (url == null) {   
  6.         throw new ArgumentNullException("url");   
  7.     }   
  8.   
  9.     Route route = new Route(url, new MvcRouteHandler()) {   
  10.         Defaults = new RouteValueDictionary(defaults),   
  11.         Constraints = new RouteValueDictionary(constraints)   
  12.     };   
  13.   
  14.     if (String.IsNullOrEmpty(name)) {   
  15.         // Add unnamed route if no name given   
  16.         routes.Add(route);   
  17.     }   
  18.     else {   
  19.         routes.Add(name, route);   
  20.     }   
  21. }   
这是一个扩展方法,我们看到这实际上是简化了PreView2中添加路由的方式.这儿仍然和以前一样使用routes.Add方法来添加路由,由于System.Web.Routing没有开发源码,只好使用反编译该程序集来研究,我们再看RouteCollection的关键定义:
C#代码
  1. public class RouteCollection : Collection<RouteBase>   
  2. {   
  3.     // Fields   
  4. private Dictionary<string, RouteBase> _namedMap;   
  5. xxx…   
这表明实际上RouteCollection维护了两个容器,一个是Collection<RouteBase>,一个是Dictionary<string, RouteBase> _namedMap,再查看Add方法的代码:
C#代码
  1. public void Add(string name, RouteBase item)   
  2. {   
  3.     if (item == null)   
  4.     {   
  5.         throw new ArgumentNullException("item");   
  6.     }   
  7.     if (!string.IsNullOrEmpty(name) && this._namedMap.ContainsKey(name))   
  8.     {   
  9.         throw new ArgumentException(string.Format(CultureInfo.CurrentUICulture, RoutingResources.RouteCollection_DuplicateName, new object[] { name }), "name");   
  10.     }   
  11.     base.Add(item);   
  12.     if (!string.IsNullOrEmpty(name))   
  13.     {   
  14.         this._namedMap.set_Item(name, item);   
  15.     }   
  16. }   
如果未提供name参数,则直接使用Collection提供的Add方法添加Route,这时并没有向_ namedMap添加route,只有提供了name,且提供的name满足!IsNullOrEmpty参数才会向_namedMap添加规则.ok,这下明白了RouteCollection是如何存储路由规则了,我们继续看UrlHelper部分和Url有关的主要提供了Action, ContentRouteUrl3个方法,RouteUrlAction方法则都是调用了UrlHelper.GenerateUrl方法,至于其他和Url有关的部分,HtmlHelper也都是直接或者间接调用UrlHelper.GenerateUrl方法.我们一个个查看.
首先看Action,该方法会给出一个连接到所提供的actionurl,有好几个重载,但是总结起来都是调用return GenerateUrl(null /* routeName */, actionName,xxx,xxx)的模式,也就是说前面所有Action间接调用GenerateUrl时候前两个参数固定,一个是null,一个是actionName,而在RouteUrl中则不同,会根据不同的重载模式来,既有需要routeName,也有不需要routeName.现在关键就是GenerateUrl方法了,该方法代码如下:
C#代码
  1.         private string GenerateUrl(string routeName, string actionName, string controllerName, RouteValueDictionary valuesDictionary) {   
  2.             return GenerateUrl(routeName, actionName, controllerName, valuesDictionary, RouteCollection, ViewContext);   
  3.         }   
  4.   
  5.         internal static string GenerateUrl(string routeName, string actionName, string controllerName, RouteValueDictionary valuesDictionary, RouteCollection routeCollection, ViewContext viewContext) {   
  6.             if (actionName != null) {   
  7.                 if (valuesDictionary.ContainsKey("action")) {   
  8.                     throw new ArgumentException(   
  9.                         String.Format(   
  10.                             CultureInfo.CurrentUICulture,   
  11.                             MvcResources.Helper_DictionaryAlreadyContainsKey,   
  12.                             "action"),   
  13.                         "actionName");   
  14.                 }   
  15.                 valuesDictionary.Add("action", actionName);   
  16.             }   
  17.             if (controllerName != null) {   
  18.                 if (valuesDictionary.ContainsKey("controller")) {   
  19.                     throw new ArgumentException(   
  20.                         String.Format(   
  21.                             CultureInfo.CurrentUICulture,   
  22.                             MvcResources.Helper_DictionaryAlreadyContainsKey,   
  23.                             "controller"),   
  24.                         "controllerName");   
  25.                 }   
  26.                 valuesDictionary.Add("controller", controllerName);   
  27.             }   
  28. VirtualPathData vpd;   
  29.             if (routeName != null) {   
  30.                 vpd = routeCollection.GetVirtualPath(viewContext, routeName, valuesDictionary);   
  31.             }   
  32.             else {   
  33.                 vpd = routeCollection.GetVirtualPath(viewContext, valuesDictionary);   
  34.             }   
  35.   
  36.             if (vpd != null) {   
  37.                 return vpd.VirtualPath;   
  38.             }   
  39.             return null;   
  40.         }   
关键是第二个,分析下这个方法,它要求必须唯一提供action,且不能重复提供controller,然后,对于路径的查找,如果提供了routeName,则系统会使用GetVirtualPath(viewContext, routeName, valuesDictionary);否则使用routeCollection.GetVirtualPath(viewContext, valuesDictionary);
我们再看这两个方法,照样反编译下,具体代码如下:
C#代码
  1. public VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)   
  2. {   
  3.     requestContext = this.GetRequestContext(requestContext);   
  4.     using (this.GetReadLock())   
  5.     {   
  6.         using (IEnumerator<RouteBase> enumerator = base.GetEnumerator())   
  7.         {   
  8.             while (enumerator.MoveNext())   
  9.             {   
  10.                 VirtualPathData virtualPath = enumerator.get_Current().GetVirtualPath(requestContext, values);   
  11.                 if (virtualPath != null)   
  12.                 {   
  13.                     virtualPath.VirtualPath = GetUrlWithApplicationPath(requestContext, virtualPath.VirtualPath);   
  14.                     return virtualPath;   
  15.                 }   
  16.             }   
  17.         }   
  18.     }   
  19.     return null;   
  20. }   
  21.   
  22. public VirtualPathData GetVirtualPath(RequestContext requestContext, string name, RouteValueDictionary values)   
  23. {   
  24.     RouteBase base2;   
  25.     bool flag;   
  26.     requestContext = this.GetRequestContext(requestContext);   
  27.     if (string.IsNullOrEmpty(name))   
  28.     {   
  29.         return this.GetVirtualPath(requestContext, values);   
  30.     }   
  31.     using (this.GetReadLock())   
  32.     {   
  33.         flag = this._namedMap.TryGetValue(name, ref base2);   
  34.     }   
  35.     if (!flag)   
  36.     {   
  37.         throw new ArgumentException(string.Format(CultureInfo.CurrentUICulture, RoutingResources.RouteCollection_NameNotFound, new object[] { name }), "name");   
  38.     }   
  39.     VirtualPathData virtualPath = base2.GetVirtualPath(requestContext, values);   
  40.     if (virtualPath == null)   
  41.     {   
  42.         return null;   
  43.     }   
  44.     virtualPath.VirtualPath = GetUrlWithApplicationPath(requestContext, virtualPath.VirtualPath);   
  45.     return virtualPath;   
  46. }   
第一个方法,由于没有提供name,于是遍历自身容器查询,第二个方法,如果提供的name不为空,则直接使用_namedMap获取,这是一个字典结构,如果没找到,则抛出异常.到这儿我们可以发现一个问题了,在没有提供routeName的情况下是遍历查询,只要找到满足条件的就返回了,那么如果有多可匹配的情况会如何呢?由于算法的特性,必然会返回第一个找到的结果,到这儿便焕然大悟了.如果我们的route初始化这样写:
routes.MapRoute("default", "default.aspx", new { controller = "demo", action = "test" });
routes.MapRoute("testroute ", "demo/{action}", new { controller = "demo", action = "test" });
那么我们用Url.Action(“demo”)的时候永远返回的是”default.aspx”,而不是可能需要的”demo/test”,于是在书写route规则的时候,必须做到从一般到特殊的规则,让系统从一般规则开始找,找不到再找特殊规则.当然,通过Url.RouteUrl便没有问题啦.因此,上面更规则的写法是:
routes.MapRoute("testroute", "demo/{action}", new { controller = "demo", action = "test" });
routes.MapRoute("default", "default.aspx", new { controller = "demo", action = "test" });
而对以这种定义:
Url.Action(“test”)Url. RouteUrl(“testroute”)返回值将是一样的,都是”demo/test”.如果有参数,比如:
routes.MapRoute("testroute", "demo/{action}/{id}", new { controller = "demo", action = "test", id="1" });
routes.MapRoute("default", "default.aspx", new { controller = "demo", action = "test", id="0" });
则下面两种调用是等价的:
<%=Url.Action("test", new { id = "2" }) %>
<%=Url.RouteUrl("testroute", new { id = "2" })%>
都会输出:” demo/test/2”;同时由于这时不适用routename的查找也同时根据了action和id,因此上面的规则顺序改变下也不会出问题.
但是如果调用:
<%=Url.Action("test") %>
<%=Url.RouteUrl("testroute")%>
这时分别返回的是:
/default.aspx
/demo

这时想想上面的代码,自然可以理解原因了.

修改一下,刚才忘记继续分析UrlHelper.Content方法了,这个方法是用来处理路径的,由于使用UrlRouting,image等资源文件的引用路径就没有以前那么直接了,因此,框架提供了一个Content方法来转换,比如引用css,可以:
<style ref="<%=Url.Content("images/style.css")%>" ... />
看看源代码:

public string Content(string contentPath) {
            
if (String.IsNullOrEmpty(contentPath)) {
                
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "contentPath");
            }

            
string appRelative = VirtualPathUtility.IsAppRelative(contentPath) ? contentPath : "~/" + contentPath;
            
return VirtualPathUtility.ToAbsolute(appRelative, ViewContext.HttpContext.Request.ApplicationPath);
        }

首先确定currentpath不能为空,然后获取currentPath关于应用程序的相对路径,最后将相对路径转换成绝对路径.思路并不复杂,看一下msdn关于VirtualPathUtility类关于这两个方法的描述就行:

  名称 说明
AppendTrailingSlash 将正斜杠 (/) 追加到虚拟路径的末尾(如果尚不存在正斜杠)。
Combine 将一个基路径和一个相对路径进行组合。
GetDirectory 返回虚拟路径的目录部分。
GetExtension 检索虚拟路径中引用的文件的扩展名。
GetFileName 检索虚拟路径中引用的文件的文件名。
IsAbsolute 返回一个布尔值,该值指示指定的虚拟路径是否为绝对路径,也就是以正斜杠 (/) 开头。
IsAppRelative 返回一个布尔值,该值指示指定的虚拟路径是否为相对于应用程序的相对路径。
MakeRelative 返回从一个包含根操作符(代字号 [~])的虚拟路径到另一个此类虚拟路径的相对虚拟路径。
RemoveTrailingSlash 从虚拟路径去除末尾的正斜杠 (/)。
ToAbsolute 已重载。 将虚拟路径转换为应用程序绝对路径。
ToAppRelative 已重载。 将虚拟路径转换为应用程序相对路径。

最后妄自猜测下作者对UrlHelper.Action和UelHelper.RouteUrl的区分,从本质上来看这两个方法都是调用同一个内部方法,但是区别在于Action不允许根据name查询,而RouteUrl可以,这样,Action就相当是专门针对Action设计的url解析,而RouteUrl是针对特定命名的route来查询.也正是由于RouteCollection存储了两套路由规则,才导致两种情况出现.

文章来自: 本站原创
引用通告: 查看所有引用 | 我要引用此文章
Tags: asp.net  MVC  C# 
评论: 2 | 引用: 0 | 查看次数: 670
2 1/1 页
Jason [2008-06-04 11:18:38 ]
文章中的代码段很漂亮,能说说是用什么软件生成的吗?
Leven [2008-06-09 18:32:01 ]
这是使用一个fck的插件实现的,不过具体下载地址我都忘记了
2 1/1 页
发表评论
用户名:
密 码: 游客发言不需要密码.
验证码: 验证码
内 容:
选 项:
虽然发表评论不用注册,但是为了保护您的发言权,建议您注册帐号.
字数限制 500 字 | HTML代码允许 关闭 | 评论可修改 关闭