理解ASP.NET MVC的路由系统

引言

路由,正如其名,是决定消息经由何处被传递到何处的过程。也正如网络设备路由器Router一样,ASP.NET MVC框架处理请求URL的方式,同样依赖于一张预定义的路由表。以该路由表为转发依据,请求URL最终被传递给特定Controller的特定Action进行处理。而在相反的方向上,MVC框架的渲染器同样要利用这张路由表,生成最终的HTML页面并返回URL。所以,理解整个ASP.NET MVC的路由系统,有两个必须出现的关键元素:Controller与Action,有两个方向的操作:传入的路径解析与传出的路径生成。

以下整理自《Pro ASP.NET MVC 3 Framework》学习笔记。

路由解析

一个URL由以下三部分组成:

http://www.website.com/Admin/Index

协议                   主机                   查询串

路由解析,就是要将Admin/Index这部分片段Segment传递给适当Controller的Action进行处理。至于路由何方,则又依赖于预置的路由表项进行指引。所以,在一切开始之前,需要通过添加路由项来完成路由表的配置。

路由项的添加,由RouteCollection.MapRoute()或RouteCollection.Add()方法实现,并通常在在MVC项目入口Global.asax中的RegisterRoutes()方法中使用。其中,MapRoute()更为常用和直接。

MapRoute(string routeName,            //路由项的名称,用于路径模式匹配 

         string urlPattern,           //路径模式 

         object defaultProperties,    //路径元素与参数的默认值  

         object constraints,          //条件满足时才适用本路由项的约束 

         string[] namespaces)         //界定Controller位置的命名空间 

换言之,路由的最终结果就是得到controller.action(parameters)这三项内容。而这三项内容,都将出现在MapRoute()的defaultProperties中。defaultProperties是一个匿名类的对象,其属性包括controller名、action名,以及其他参数在内。

routes.MapRoute("routeA", 

                "{controller}/{action}/{page}", 

                new {controller = "Admin", action = "Index", page = 1}) 

就象上面这个示例一样,第2个参数urlPattern对应的那个模式串中,{}包围的部分是一个占位符,类似string.Format()中的格式化模式串。整个模式串,用于匹配URL中主机名后的全部内容。而占位符对应的元素,则将完整出现在之后的defaultProperties中,映射到相应的属性上。{}里的这个元素,被称为Segment Variable。

在用urlPattern匹配URL时,会象方法的参数表一样进行严格匹配。对请求URL没有提供的项,将自动使用defaultProperties中提供的默认值。最终,匹配的结果,是将请求传递到类似这样的一个方法上,匿名类中作为属性的Controller名、Action名以及参数名page都将严格与实际的方法原型匹配(匹配将忽略名称的大小写)。而且匹配的过程呈现出两面性,一面是它只会按Pattern里Segment Variable的数量进行匹配(死板的),另一面是它不会关心每个Segment能否解析出恰当的内容(开明的),比如某个位置本该对应一个整数而不是字符串。

public class SomeController 

{ 

    public ViewResult SomeAction(int page) { .... } 

}

占位片段过多的URL则会失配。正如下表:

URL                                           controller    action    page 

http://www.website.com/                         Admin        Index     1 

http://www.website.com/Product                  Product      Index     1 

http://www.website.com/Admin/Index              Admin        Index     1 

http://www.website.com/Admin/Logon              Admin        Logon     1 

http://www.website.com/Product/List             Product      List      1 

http://www.website.com/Admin/Index/5            Admin        Index     5 

http://www.website.com/Admin/Index/5/detail     null         null     null 

其次,路由表项的排列顺序直接影响路由结果。因为MVC框架按路由项添加的先后顺序进行匹配,而非匹配程度高低,所以越特殊的路由项需要越早被定义。反观URL模式,它将严格按格式串的语义进行解析。如"Prefix/{controller}/{action}",将匹配到类似http://www.website.com/Prefix/Product/List"这样的URL。如果URL指向特定的文件,比如"download/somefile.pdf",同样也可以设置RouteCollection.RouteExistFiles = true,使文件也被路由系统接管,传递给特定的controller。

除去上述这种在路由中指定参数默认值的方式外,还可以利用UrlParameter.Optional声明可选参数来影响路由。与路由定义中的默认参数将始终存在不同,可选参数意味着仅当请求URL中出现对应于该可选参数的片段时,才会有一个相应的参数被构造并被传递给action,否则就当这个参数从来没有被定义和存在过。

routes.MapRoute("routeA", 

                "{controller}/{action}/{page}", 

                new { controller = "Admin", action = "Index", page = UrlParameter.Optional }) 

对于上例中的可选参数page,可以在定义action时指定其默认值。如果从默认值的角度看,默认参数与可选参数起到的作用非常近似,但可选参数对于action而言并非固定的存在。

public class AdminController 

{ 

    public ViewResult Index(int page = 5) { .... } 

}

此外,还有可变长度的路由变量,类似于用C#中params关键字定义的可变长度的方法参数。具体地,是在占位符名称前加上*,就象{*arguments}这样。之后,路由框架会按照路径模式对URL逐段进行匹配,并将末端失配的所有片段作为一个由/分隔的串传递给参数arguments。在action内部,可以对arguments进行分割与解析,做出自己需要的其他处理。

接下来,是路由项中的namespace,该参数确定该路由项在查找controller时可以参考的命名空间。MVC将先在该参数给定的命名空间中查找controller,查找未果后才去其可触及的其他命名空间查找。可以通过设置路由项的DataTokens["UseNamespaceFallback"] = false来强迫只在指定空间查找。

namespaces参数给出的命名空间,无论其顺序如何,其拥有的controller具有同等的优先权。如果不同空间下有同名controller存在,将直接导致同名冲突。为避免冲突,可以分别为这些冲突的controller所属命名空间定义不同的路由项,并适当考虑将哪一条路由项放在更前面。

最后还有一个略复杂些的参数constraints,它决定了该路由项适用的条件。若干条件之间,为AND的关系,即须同时满足。约束既可以是简单地利用正则匹配,就象下面这样仅当controller名以H开头,action为Index或About时才适用此路由项。

MapRoute("MyRoute", 

         "{controller}/{action}/{id}/{*arguments}",

         new { controller = "Home", action = "Index", id = UrlParameter.Optional },

         new { controller = "^H.*", action = "^Index$|^About$" },

         new[] { "SomeNamespace.Controllers" });

更复杂一些的约束,则要通过实现了IRouteConstraint接口的约束对象来提供,其重点是实现该接口中的bool Match()方法。然后往上面的约束表中简单地加入一段ieConstraint = new AgentIEConstraint()即可实现对浏览器内核的筛选。

public class AgentIEConstraint : IRouteConstraint

{

    public bool Match(HttpContextBase context, Route route, string name, RouteValueDictionary values, RouteDirection direction)

    {

        return (context.Request.UserAgent != null)

               && (context.Request.UserAgent.Contains("IE");

    }

}

输出URL

路由系统的另一重要功能,是生成URL。这主要是通过Html.ActionLink()、Html.RouteLink()、Url.Action()、Url.RouteUrl()等方法实现的,他们作用相当、参数近似,又以ActionLink()生成<a>标签指向特定的action较为常用,RouteLink()则可以直接选择特定路由并指向具体的文件、文件夹等资源。

尽量避免用手工方式定义URL,这实在太危险!

ActionLink(string text,             //链接显示的文本

           string action,           //action名称

           string controller,       //controller名称

           string protocol,         //URL 协议,如“http”或“https”

           string host,             //URL中 的主机名

           string fragment,         //URL 片段名称(定位点名称)

           object routeValues,      //一个包含路由参数的对象

           object htmlAttributes)   //一个对象,其中包含要为该元素设置的 HTML 特性

URL的解析与输出,并不当然的是一个互逆的过程。

引入Areas进行分区

分区,是对URL更精细的一种路由手段。

上一篇:关于解决asp.net mvc网站页面Banner图片即时更换css里背景图片url相对路径问题的新方案


下一篇:C#编程(四十二)----------委托和事件