载入中,请稍候……

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

Admin 于 2008-08-26 04:12:09 发表转载

订阅: http://www.miniboke.com/Feed/Article_9.aspx
引用: http://www.miniboke.com/Trackback/wIROSnIaEwSksWZRdosY.aspx (UTF-8)
Asp.Net MVC实践 - 自定义ActionResult实现Rss输出 (基于ASP.NET MVC Preview 3) < Asp.Net MVC实践 - 探索UrlRouting并分析UrlHelper (基于ASP.NET MVC Preview 3) > 实体类代码生成器3.0源码发布[新增类图]

使用asp.net mvc以来,UrlRouting的处理就是一个非常关键的问题,由于使用的不小心,经常导致我们无法得到预期的结果,这的确是个很麻烦的问题,于是很多朋友推测是MVC框架的bug,到底事实如何呢?今天我便尽力探索系统中UrlRouting到底是如何工作的,希望能找出问题的关键.

总所周知,Asp.Net MVC框架一般使用Global.asax在程序第一次启动的时候初始化RouteCollection,Preview3,我们一般使用RouteCollection. MapRoute方法来添加新的规则.然后,系统理论上会非常听话执行我们给出的规则,然后我们直接或者间接在页面中使用UrlHelper提供的方法处理Url,UrlHelper使用路由而非路径的方式定义url,能给我们更大的方便,但是问题来了,很多朋友发现UrlHelper并不是那么听话的,可以说有时候会给出一个莫名其妙的地址.为了解开这个问题,我们得先看看系统到底怎么来处理这些规则的.

首先我们把这个MapRoute方法找出来,查询源代码:

        public static void MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints) {
            
if (routes == null{
                
throw new ArgumentNullException("routes");
            }

            
if (url == null{
                
throw new ArgumentNullException("url");
            }


            Route route 
= new Route(url, new MvcRouteHandler()) {
                Defaults 
= new RouteValueDictionary(defaults),
                Constraints 
= new RouteValueDictionary(constraints)
            }
;

            
if (String.IsNullOrEmpty(name)) {
                
// Add unnamed route if no name given
                routes.Add(route);
            }

            
else {
                routes.Add(name, route);
            }

        }

 

 

这是一个扩展方法,我们看到这实际上是简化了PreView2中添加路由的方式.这儿仍然和以前一样使用routes.Add方法来添加路由,由于System.Web.Routing手上没有源码,只好使用反编译该程序集来研究,我们再看RouteCollection的关键定义:

 

public class RouteCollection : Collection<RouteBase>
{
    
// Fields
private Dictionary<string, RouteBase> _namedMap;
xxx…

 

 

这表明实际上RouteCollection维护了两个容器,一个是Collection,一个是Dictionary

public void Add(string name, RouteBase item)
{
    
if (item == null)
    
{
        
throw new ArgumentNullException("item");
    }

    
if (!string.IsNullOrEmpty(name) && this._namedMap.ContainsKey(name))
    
{
        
throw new ArgumentException(string.Format(CultureInfo.CurrentUICulture, RoutingResources.RouteCollection_DuplicateName, new object[] { name }), "name");
    }

    
base.Add(item);
    
if (!string.IsNullOrEmpty(name))
    
{
        
this._namedMap.set_Item(name, item);
    }

}

 

 

如果未提供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方法了,该方法代码如下:

        private string GenerateUrl(string routeName, string actionName, string controllerName, RouteValueDictionary valuesDictionary) {
            
return GenerateUrl(routeName, actionName, controllerName, valuesDictionary, RouteCollection, ViewContext);
        }


        
internal static string GenerateUrl(string routeName, string actionName, string controllerName, RouteValueDictionary valuesDictionary, RouteCollection routeCollection, ViewContext viewContext) {
            
if (actionName != null{
                
if (valuesDictionary.ContainsKey("action")) {
                    
throw new ArgumentException(
                        String.Format(
                            CultureInfo.CurrentUICulture,
                            MvcResources.Helper_DictionaryAlreadyContainsKey,
                            
"action"),
                        
"actionName");
                }

                valuesDictionary.Add(
"action", actionName);
            }

            
if (controllerName != null{
                
if (valuesDictionary.ContainsKey("controller")) {
                    
throw new ArgumentException(
                        String.Format(
                            CultureInfo.CurrentUICulture,
                            MvcResources.Helper_DictionaryAlreadyContainsKey,
                            
"controller"),
                        
"controllerName");
                }

                valuesDictionary.Add(
"controller", controllerName);
            }

VirtualPathData vpd;
            
if (routeName != null{
                vpd 
= routeCollection.GetVirtualPath(viewContext, routeName, valuesDictionary);
            }

            
else {
                vpd 
= routeCollection.GetVirtualPath(viewContext, valuesDictionary);
            }


            
if (vpd != null{
                
return vpd.VirtualPath;
            }

            
return null;
        }

 

 

关键是第二个,分析下这个方法,它要求必须唯一提供action,且不能重复提供controller,然后,对于路径的查找,如果提供了routeName,则系统会使用GetVirtualPath(viewContext, routeName, valuesDictionary);否则使用routeCollection.GetVirtualPath(viewContext, valuesDictionary);

我们再看这两个方法,照样反编译下,具体代码如下:

public VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
    requestContext 
= this.GetRequestContext(requestContext);
    
using (this.GetReadLock())
    
{
        
using (IEnumerator<RouteBase> enumerator = base.GetEnumerator())
        
{
            
while (enumerator.MoveNext())
            
{
                VirtualPathData virtualPath 
= enumerator.get_Current().GetVirtualPath(requestContext, values);
                
if (virtualPath != null)
                
{
                    virtualPath.VirtualPath 
= GetUrlWithApplicationPath(requestContext, virtualPath.VirtualPath);
                    
return virtualPath;
                }

            }

        }

    }

    
return null;
}


public VirtualPathData GetVirtualPath(RequestContext requestContext, string name, RouteValueDictionary values)
{
    RouteBase base2;
    
bool flag;
    requestContext 
= this.GetRequestContext(requestContext);
    
if (string.IsNullOrEmpty(name))
    
{
        
return this.GetVirtualPath(requestContext, values);
    }

    
using (this.GetReadLock())
    
{
        flag 
= this._namedMap.TryGetValue(name, ref base2);
    }

    
if (!flag)
    
{
        
throw new ArgumentException(string.Format(CultureInfo.CurrentUICulture, RoutingResources.RouteCollection_NameNotFound, new object[] { name }), "name");
    }

    VirtualPathData virtualPath 
= base2.GetVirtualPath(requestContext, values);
    
if (virtualPath == null)
    
{
        
return null;
    }

    virtualPath.VirtualPath 
= GetUrlWithApplicationPath(requestContext, virtualPath.VirtualPath);
    
return virtualPath;
}

 

 

第一个方法,由于没有提供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的查找也同时根据了actionid,因此上面的规则顺序改变下也不会出问题.

但是如果调用:

<%=Url.Action("test") %>

<%=Url.RouteUrl("testroute")%>

这时分别返回的是:

/default.aspx
/demo

这时想想上面的代码,自然可以理解原因了.
修改一下,刚才忘记继续分析UrlHelper.Content方法了,这个方法是用来处理路径的,由于使用UrlRouting,image等资源文件的引用路径就没有以前那么直接了,因此,框架提供了一个Content方法来转换,比如引用css,可以:

 

 

#1楼     回复  引用  查看    
 SZW       | 2008-05-30 12:40
简言之,默认的Action方法是按actionName+controllerName查找,RouteUrl按routeName查找。他们查找的“ 关键字”是不一样的,在Url.Action(包括Html.ActionLink等使用actionName+controllerName方式的)里 面,Preview3的这些方法查找到actionName就算完成了,忽略了一个重要的前提——controllerName。
#2楼 [楼主]    回复  引用  查看    
 Leven       | 2008-05-30 12:44
@SZW
就是如此,刚才不小心把你那边评论弄掉了(汗一个...),干脆一鼓作气记录下研究这个的过程.
#3楼     回复  引用  查看    
 没剑       | 2008-05-30 13:12
@Leven
哇哈哈,楼主偶的偶像啊,偶看不下起,你一下子就分析出来啊,崇拜啊~
#4楼 [楼主]    回复  引用  查看    
 Leven       | 2008-05-30 13:53
..ls太夸张了...
顺便来修改下并补充点东西.
#5楼     回复  引用  查看    
 Jeffrey Zhao       | 2008-05-30 14:37
我比较倾向于将route独立于mvc讨论,呵呵
#6楼     回复  引用  查看    
 没剑       | 2008-05-30 14:59
Action就相当是专门针对Action设计的url解析,而RouteUrl是针对特定命名的route来查询.也正是由于RouteCollection存储了两套路由规则,才导致两种情况出现.
-----
这种结论是什么意思?是bug还是不是bug???
#7楼 [楼主]    回复  引用  查看    
 Leven       | 2008-05-30 15:03
@没剑
我看来,严格来说,这个并未mvc框架的bug,因为查询route是System.Web.Routing来完成的,而且这个遍历也没有问题, 只是查询的时候会返回第一个查到的结果而已,如果要修改,还真比较麻烦,要如何改?如果RouteCollection中根据Action查询出多个结 果,又该返回哪个呢?毕竟计算机不是人脑,它可不知道我们需要哪个.
就目前来说,只能我们迁就下它,使用我文中提到的routing设定原则"一般->特殊",这样就能保证Action找到的都是你需要的.
#8楼     回复  引用  查看    
 没剑       | 2008-05-31 02:48
@Leven
呵呵,其实你的解答也正对了偶的意思:也就是“每一张厕纸,每一条内裤也是有它的用处的”。。。呵呵,所以这个也就不是bug的,严格点说这个问题也可能是官方没有考虑清楚的,因为这个版本本来就是preview,呵呵~~~~
楼主,偶对mvc10分感兴趣,大家有空交流一下吧,呵呵~
#9楼     回复  引用  查看    
 SZW       | 2008-05-31 11:26
@Leven
我在http://www.cnblogs.com/szw/archive/2008/05/29/1209915.html 谈到的应该是个bug,你文中的测试好像并没有针对深入到这个问题的实质,只是上比较了这两种方式,Url.Action其实跟routeName没有直 接的关系,因为Url.Action("Foo")并不提供对routeName索引的支持,而且你也看到了,即使在global里面修改了 routeName,情况一点都没有改变。

其一、Url.Action("Foo")在这种情况下失去了意义,甚至如果不点破不处理的话会使网页在不确定的时间产生不确定的错误,此时这种方法的存在本身就是一个bug。
其二、如果这种方法是有必要存在的(答案因该是肯定的),那么就是Routing内部配对URL规则上的错误,在查找actionName的时 候,忽略了同时判断controllerName这个重要的参数,导致Url.Action("Foo")形同虚设甚至会给程序带来意想不到的错误,我想 这不是官方的本意吧?所以在这种方法有必要存在的情况下,返回错误的结果当然是个bug。
#10楼     回复  引用  查看    
 没剑       | 2008-05-31 12:25
@SZW
我怎么想的就是,Url.Action("Foo")这个方法执行时的优先级是怎么样的,如果计上Routing的话,哪么搜索顺序应该是:
搜索RouteName
|->有(返回)
搜索RouteName---|
|->无->当前ControllerName+ActionName(返回)
------
如果真是这样子的话,这个bug确实是"bug"!!
我在你的文章里也提到过,调换Routing的顺序竟然会造成路径映射混乱(不知道这样子说对不对。。。),哪么这个bug应该可以肯定是bug的!!!!
 
#11楼     回复  引用  查看    
 Q.Lee.lulu       | 2008-05-31 14:55
@Leven
routing设定原则"一般->特殊"

不同意这个!!我觉得刚好反过来才对
#12楼 [楼主]    回复  引用  查看    
 Leven       | 2008-05-31 15:29
@没剑
可以交换联系方式嘛.
你这个说法是正确的,不过这儿他引入了Url.Action(string actionName,string controllerName)的重载,应该同时分析下这个会更清晰.
@SZW
本文是分析了隐藏在Route查找背后的东西,旨在说明为什么当我们修改了Route的设置之后UrlHelper工作看起来就不正常了.SZW所描述的问题,我看了你的代码,可以给出解释:
routes.MapRoute("About","Home/About",new { controller = "Home", action = "About" });
这是你添加的规则.当我使用Url.Action("About")的时候,实际调用的是:GenerateUrl(null , actionName, null , new RouteValueDictionary());由本文给出的查找规则,它会找到RouteCollection中第一个Action="About" 的规则并解析.当使用Url.Action("About","Admin")实际调用的是:GenerateUrl(null , actionName, controllerName, new RouteValueDictionary());,按照文中的分析,它会找到RouteCollection中第一个Action="About"且 Controller="Admin"的规则并解析,注意这儿,这两个是不同的,由于你默认controller是"Home"那么 Url.Action("About")="/Home/About",而Url.Action("About","Admin")由于根本没有添加一个 Controller可能(注意这儿是可能)等于"Admin"的Route,因此应该输出为空,若是 Url.Action("About","Home")则会和Url.Action("About")是一回事.
如果你加上一行:
routes.MapRoute("Admin", "Admin/About", new { controller = "Admin", action = "About" });
则使用Url.Action("About","Admin")="Admin/About"这样就会正常了.
但是如果你把这一行加到最开始,那么Url.Action("About"),按照分析,就会变成"Admin/About"不知道这么说明白没.
这儿的关键是查找Route的方式,它的规则是提供了什么参数就找完全满足参数的第一个路由,如果没有则返回空.
 
#13楼 [楼主]    回复  引用  查看    
 Leven       | 2008-05-31 15:57
@Q.Lee.lulu
--引用--------------------------------------------------
Q.Lee.lulu: @Leven
routing设定原则&quot;一般-&gt;特殊&quot;

不同意这个!!我觉得刚好反过来才对
--------------------------------------------------------
我们用实例说话:
比如两个route:
routes.MapRoute("testdefault", "default.aspx", new { controller = "demo", action = "list", page = "1" });
routes.MapRoute("testroute", "demo/{action}/{page}", new { controller = "demo", action = "list", page="1" });
意图为:如果使用default.aspx访问,则定向到/demo/list
这是按"特殊-一般"
测试页面输出代码:
<%=Url.Action("list", new { page = "2" })%>
<br />
<%=Url.Action("list")%>
期待输出结果:
demo/list/2
demo
实际输出结果:
/demo/list/2
/default.aspx
当更换route的顺序,也就是使用"一般-特殊"的规则后,实际输出结果:
/demo/list/2
/demo
如果有其他的可能性,请提出,个人能力有限,先想到这儿
#14楼     回复  引用  查看    
 SZW       | 2008-05-31 17:41
@Leven
不小心怎么我也弄成“修改”了,汗~

其实现在我们争论的问题最基本的是,我认为应该先“特殊”,也就是特殊的、个别的URL规则,然后“一般”,也就是更加通用的URL规则。而你的 想法相反。这个如果不统一,可能我说的你也更加没法体会了。偷懒一点的话其实也不用去从源代码上面看,看看官方的是用户顺序,然后自己换一下,就已经很明 了一些了。你说我那两个Action不同(http://www.cnblogs.com/szw/archive/2008/05/29 /1209915.html 的20楼),其实并不是不同,而是一个已经涵盖了另外一个:
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" }
他的Action并不是Index,Index只是不提供时候的默认值。
#15楼 [楼主]    回复  引用  查看    
 Leven       | 2008-05-31 21:20
@SZW
可能是这儿你没理解我这边说的意思.注意这儿的一般和特殊是有一个首要条件要满足,那就是你拿起来比较的routing的默认的参数要相同,当然 action也要相同,但是有一些"特殊"的地址是处理可能的请求,我看了你20楼的配置,两个route规则action是不同的,也就是参数不同.不 是我说的这个情况,那情况其实复杂多了.
发现我们各自说的情况不同.我是在默认参数的情况下考虑,参照的url,按照url来,一个规则如果出现特殊情况,那是要先拦截掉,其实我文中没考虑这个情况,这个的查询更加复杂,我考虑的是,默认参数一致,但是url不同的情况.
#16楼     回复  引用  查看    
 SZW       | 2008-06-01 18:54
不小心修改掉了,见14楼,呵呵
#17楼 [楼主]    回复  引用  查看    
 Leven       | 2008-06-01 19:38
@SZW
呵呵,我还以为只有我犯过这错误呢.一不小心就没了...
可以说这是文中的缺陷吧.我当时并没有详细分析route的部署规则,所说的情况实际上是一种极为特殊的情况,真是抱歉,好像有点误导大家了,我再来分析下你说的情况吧.
你这种情况发现一个问题,Routing组件在匹配通用Url规则的时候好像只匹配了controller和action,当一个用户url可能有多个controller和action匹配的时候系统只会返回第一个.
比如你的例子中
Url.Action("About","Home",new {msg="xxx"});
我们主观认为这个提供了3个参数,分别是controller="Home",action="About",Msg="xxx",所以应该匹配 规则"Home/About/{msg}",但是实际上系统却自作主张的给我们匹配了"{controller}/{action}/{id}"这个规 则,这个规则中自然不存在Msg参数,所以那个Msg参数是"不处理"参数,最终url会变成"Home/About?msg=xxx"(系统还认为这儿 隐含了一个id参数,为规则的默认).
但是如果将"Home/About/{msg}"放在前面,第一个找到的是他,肯定能匹配到,所以就返回了我们想要的结果.
我认为如果Routing组件能修改匹配方式,将自定义的参数也加入到匹配中,就不会出现这样的问题了.
#18楼     回复  引用  查看    
 SZW       | 2008-06-01 21:05
@Leven
所以说先"Home/About/{msg},后"{controller}/{action}/{id}"的“特殊”=>“一般”的方法很符合程序的逻辑和实际需要嘛。
#19楼 [楼主]    回复  引用  查看    
 Leven       | 2008-06-01 22:14
@SZW
这也不能完全这么说
如果是
"{controller}/{action}/{msg}"

"{controller}/{action}/{id}"
这也没辙,反正它只会认第一个.所以怎么放就只能根据你业务的原则了
#20楼     回复  引用  查看    
 SZW       | 2008-06-02 09:50
--引用--------------------------------------------------
Leven: @SZW
这也不能完全这么说
如果是
&quot;{controller}/{action}/{msg}&quot;

&quot;{controller}/{action}/{id}&quot;
这也没辙,反正它只会认第一个.所以怎么放就只能根据你业务的原则了
--------------------------------------------------------
这样的用法其实不是“一般”和“特殊”的关系,他们是并列的,所谓“特殊”一般都是controller和action指定的(比如这里的msg 这一条),而“一般”的都是“模糊”的,这时候当你不提供、不符合msg相关参数的时候,它就会认第二个,此时第一个是“特殊”的,第二个是“一般”的。 现在确定了顺序之后,回过头来,看一下Url.Action,里面其实并没有提供RouteName的参数,只有 ActionName/ControllerName及相关的,所以在Url.Action里面,应该是不会涉及到RouteName,那么问题就很简单 了,如果在一个已知的ControllerName里面,Url.Action("Foo")返回了一个别的Controller里面的方法,你觉得算不 算bug呢(如果不知道这个方法的真实意图,可以参考Preview2中的方法,那里面是正确的)?而RouteName相关方法,在Perview3中 交给了Url.RouteUrl去处理,如果没有什么问题的话,这两个方法是井水不犯河水的。
#21楼     回复  引用  查看    
 Elden       | 2008-06-05 14:43
http://blog.jeremyskinner.me.uk/2008/06/03/aspnet-mvc-intercepting-the-routevaluedictionary/

被阅424次, 0投一票Asp.Net MVC
  • 看完了要说点啥么?
  • 昵称 (不填说不了话)
  • 信箱地址 (不会被公开,但是不填也说不了话)
  • 网址 (这个不填也成)

Powered by MiniBoke v2.0.0.8 Build 0828

Copyright © 2008 迷你博客. All rights reserved.

粤ICP备07500939号