第2章 路由进阶
路由是开发ASP.NET 3.5 MVC网站所必须深入理解的一个概念,而路由处理模块System.Web.Routing则并不依附于ASP.NET 3.5 MVC框架,在传统的Web Form项目中,同样可以使用路由处理模块。
本章首先说明了设置路由的基本原理,如何利用MapRoute()扩展方法设置路由,如何自定义路由约束,然后分析了路由的解析过程,最后通过实例实现了如何在Web Form项目中使用路由。
本章要点:
● 解读Default.aspx页面
● 路由匹配监测
● 设置路由的扩展方法
● 自定义路由约束
● 路由解析分析
● 在Web Form项目中使用路由
2.1 解读Default.aspx页面
在ASP.NET 3.5 MVC框架所创建的默认项目“MvcApplication1”中,打开Default.aspx页面的后置代码文件Default.aspx.cs,代码如下:
public void Page_Load(object sender, System.EventArgs e) { string originalPath = Request.Path; HttpContext.Current.RewritePath(Request.ApplicationPath, false); IHttpHandler httpHandler = new MvcHttpHandler(); httpHandler.ProcessRequest(HttpContext.Current); HttpContext.Current.RewritePath(originalPath, false); }
在上述代码中,当运行“MvcApplication1”MVC网站项目时,被请求执行的页面为Default.aspx,此时请求的路径被重写为“/”,并被传递到MvcHttpHandler,而路径“/”匹配默认的路由“{controller}/{action}/{id}”,因此最后被定位的控制器为HomeController,动作方法为Index(),从而在浏览器中输出对应视图Index.aspx页面的内容。
令人疑问的是,为什么Default.aspx页面被当做普通的Web Form页面来执行,而ASP.NET 3.5 MVC框架没有将该请求转入路由解析呢?
2.1.1 DefauIt.aspx页面不被路由解析
打开项目“MvcApplication1”中的Global.asax.cs文件,在Application_Start()中添加相关代码,最后的实现代码如下:
protected void Application_Start() { RegisterRoutes(RouteTable.Routes); RouteTable.Routes.RouteExistingFiles =false; }
在上述代码中,设置了RouteExistingFiles的属性为false,实际上,这是RouteExistingFiles的默认值。表明ASP.NET 3.5 MVC框架中的路由,并不处理MVC网站中的现有Web文件页面,也就是说,在默认情况下,路由没有解析Default.aspx页面,把该页面当做普通的Web Form页面来执行。
正是由于这个特性,开发者在开发ASP.NET 3.5 MVC网站时,并不是必须实现全部的MVC网站页面,而是可以选择某些页面使用传统的Web Form页面,可以在一个网站中,部分页面采用MVC的方式开发,另外一部分页面采用传统的Web Form技术开发。例如当修改或者迁移传统的Web Form网站到MVC网站时,就可以逐步实现ASP.NET 3.5 MVC网站。
2.1.2 路由解析DefauIt.aspx页面
如果将RouteExistingFiles的属性设置为true,然后运行“MvcApplication1”MVC网站项目,此时就会打开一个提示错误信息的页面。
当RouteExistingFiles的属性被设置为true,表明ASP.NET 3.5 MVC框架中的路由将要解析被请求的Default.aspx页面,尽管被请求的Default.aspx页面匹配默认的路由设置“{controller}/{action}/{id}”,但由于并不存在一个“Default.aspx”控制器,所以最后导致ASP.NET 3.5 MVC框架定位不到适当的控制器,从而出现提示错误信息的页面。
如果在路由设置中添加相关的路由,就可以正常运行Default.aspx页面了。全局应用程序类Global.asax.cs的设置代码见代码清单2-1。
代码清单2-1全局应用程序类的配置代码
1: public static void RegisterRoutes(RouteCollection routes) 2: { 3: routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 4: 5: routes.MapRoute( "Start", "Default.aspx", new { controller = "Home", action = "Index", id = "" } ); 6: 7: routes.MapRoute( "Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "" } ); 8: } 9: protected void Application_Start() 10: { 11: RegisterRoutes(RouteTable.Routes); 12: RouteTable.Routes.RouteExistingFiles =true; 13: }
在上述代码中,第5行专门针对Default.aspx页面设置了新的路由。需要注意的是,该路由一定要位于第7行的Default路由之前,路由是按照路由的设置次序解析的,一旦发现有匹配的路由,就不会再进行后面的路由检索,直接结束路由的解析过程。
打开Default.aspx页面的后置代码文件Default.aspx.cs,删除或者注释掉其中的代码,此时仍然可以正常运行“MvcApplication1”MVC网站项目。此时的Default.aspx页面不再当做传统的Web Form页面,而被路由解析,从而找到匹配的路由Start,打开指定的视图Index.aspx页面。
2.2 路由匹配监测
2.2.1 路由匹配监测器
为了深入理解路由解析的匹配原则,在“MvcApplication1”MVC网站项目中,用鼠标右键单击“解决方案资源管理器”中的“引用”节点,在弹出的快捷菜单中选择“添加引用”命令,打开如图2-1所示的“添加引用”对话框。
图2-1 “添加引用”对话框
在图2-1中,选择“MvcApplication1”项目根目录下的“RouteMonitor.dll”文件,单击“确定”按钮,此时的“MvcApplication1”MVC网站,就可以利用可视化的界面,监测路由解析的匹配原则。
打开“MvcApplication1”项目下的全局应用程序类Global.asax.cs,设置的代码见代码清单2-2。
代码清单2-2全局应用程序类的配置代码
1: public static void RegisterRoutes(RouteCollection routes) 2: { 3: routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 4: 5: routes.MapRoute( "Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "" } ); 6: } 7: 8: protected void Application_Start() 9: { 10: RegisterRoutes(RouteTable.Routes); 11: RouteTable.Routes.RouteExistingFiles =false ; 12: RouteMonitor.RouteDebugger.RewriteRoutesForTesting( RouteTable.Routes); 13: }
在上述代码中,设置了RouteExistingFiles的属性为false,表明此项目根目录下的Default.aspx页面被当做一个普通的Web Form页面,Default.aspx页面的后置代码文件仍然为默认的状态。第12行注册了路由监测程序,从而开发者可以在浏览器中键入相关的路由请求,可视化地监测路由解析的匹配原则。
运行“MvcApplication1”项目,打开如图2-2所示的路由监测运行界面。
图2-2 Default.aspx页面的路由监测一
从图中可以看出,被请求的页面为Default.aspx页面,由于该页面被当做一个普通的Web Form页面,所以该页面的后置代码文件被执行,请求的路径被重写为“/”,路由解析后,匹配的路由为“{controller}/{action}/{id}”;被定位的控制器名称为Home,动作方法为Index();因此“MvcApplication1”项目打开的页面应该是视图Index.aspx页面。
2.2.2 路由解析DefauIt.aspx页面的监测
如果修改代码清单2-2中的第11行,将RouteExistingFiles的属性设置为ture,由于不再是ASP.NET 3.5 MVC框架中的默认值,此时被请求的Default.aspx页面,将会被路由解析,而不会执行该页面的后置代码。
再次运行“MvcApplication1”项目,打开如图2-3所示的路由监测运行界面。
图2-3 Default.aspx页面的路由监测二
从图中可以看出,被请求的页面为Default.aspx页面,被路由解析后,匹配的路由为“{controller}/{action}/{id}”;被定位的控制器名称为Default.aspx,动作方法为Index()。由于“MvcApplication1”项目中不存在名称为Default.aspx的控制器,因此项目运行后,将会打开一个提示错误信息的页面。
这里需要说明的是,在路由的解析过程中,首先是在路由表集合中,寻找匹配的路由定义;然后是解析匹配路由定义的各个参数,获得匹配路由的控制器、动作方法和参数;最后是定位到指定的控制器、动作方法和参数,打开指定的视图页面。
如果在项目中寻找不到指定的控制器,或者动作方法等,如Default.aspx控制器,那么ASP.NET 3.5 MVC框架将会打开提示错误信息的页面。
2.2.3 添加路由
如果在代码清单2-2中,添加新的路由,设置的代码见代码清单2-3。
代码清单2-3全局应用程序类的配置代码
1: public static void RegisterRoutes(RouteCollection routes) 2: { 3: routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 4: 5: routes.MapRoute( "Start", "Default.aspx", new { controller = "Home", action = "Index", id = "" } ); 6: 7: routes.MapRoute( "Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "" } ); 8: } 9: 10: protected void Application_Start() 11: { 12: RegisterRoutes(RouteTable.Routes); 13: RouteTable.Routes.RouteExistingFiles =true ; 14: RouteMonitor.RouteDebugger.RewriteRoutesForTesting( RouteTable.Routes); 15: }
在上述代码中,专门为Default.aspx页面设置了一个新的路由Start,此时Default.aspx页面的后置代码文件不再被执行,此时如果运行“MvcApplication1”项目,就会打开如图2-4所示的路由监测运行界面。
图2-4 Default.aspx页面的路由监测三
从图中可以看出,被请求的页面为Default.aspx页面,被路由解析后,匹配的路由有两个,它们分别为“Default.aspx”和“{controller}/{action}/{id}”,由于路由“Default.aspx”的设置次序在“{controller}/{action}/{id}”的前面,因此最后被匹配的路由是最前面的一个匹配路由,即“Default.aspx”。
找到了匹配的路由之后,解析该路由中的各个参数,被定位的控制器名称为Home,动作方法为Index();因此“MvcApplication1”项目打开的页面应该是视图Index.aspx页面。
2.3 设置路由的扩展方法
在ASP.NET 3.5的SP1版本中,包括了一个新的命名空间System.Web.Routing,该程序集下的各个类主要实现路由的定义、解析、匹配等功能。也就是说,路由并不是专门服务于ASP.NET 3.5 MVC框架,它同样可以运用于传统的Web Form程序。
2.3.1 Route类
Route类的UML类图,如图2-5所示。
图2-5 Route类的UML类图
Route类是抽象类RouteBase的子类,在Route类中,设置了路由中的5个基本属性,它们分别是路由的约束Constraints、路由的命名空间DataTokens、路由参数的默认值Defaults、路由处理程序RouteHandler及路由URL;定义了4个重载的构造函数。
Route类的构造函数列表见表2-1。
表2-1 Route类的构造函数列表
从表中可以看出,在最简单的构造函数中,需要输入URL路由和路由处理程序两个参数;在最复杂的构造函数中,则需要输入Route类中的5个基本属性。
以下说明如何使用包括Route类中5个基本属性的构造函数,代码如下:
Route route=new Route("Archive/{entryDate}", new RouteValueDictionary{ {"controller", "Blog"}, {"action", "Archive" }}, new RouteValueDictionary{ {"entryDate", @"\d{2}-\d{2}-\d{4}"}}, new RouteValueDictionary{ {"namespaces", "Spencer.Route"}}, new MvcRouteHandler () );
在上述代码中,定义了一个路由“Archive/{entryDate}”、一个URL路由参数的默认值new RouteValueDictionary{ {"controller", "Blog"}, {"action", "Archive" }}、一个利用正则表达式定义输入参数entryDate为指定日期格式的约束new RouteValueDictionary{ {"entryDate",@"\d{2}-\d{2}-\d{4}"}}、一个定义命名空间的new RouteValueDictionary{ {"namespaces","Spencer.Route"}}及路由处理程序new MvcRouteHandler()。
这里需要说明的是,在定义路由中相关参数的约束时,除了使用正则表达式设置字符串的模式之外,还可以使用HttpMethodConstraint和自定义路由约束。
使用HttpMethodConstraint的代码如下:
new RouteValueDictionary{ {"method", new HttpMethodConstraint("POST") } }
通过设置上述约束,只有执行POST方法时,相关的路由才能被执行。
2.3.2 RouteCoIIection类
在实际的路由运用中,需要创建多个路由,而RouteCollection类就是用来管理这些路由集合的,RouteCollection类的UML类图,如图2-6所示。
图2-6 RouteCollection类的UML类图
从图中可以看出,通过RouteTable类的静态属性Routes,可以获得RouteCollection类的实例化对象。利用这一特性,可以在Global.asax文件中设置多个路由,设置的代码见代码清单2-4。
代码清单2-4全局应用程序类的配置代码
1: protected void Application_Start() 2: { 3: RegisterRoutes(RouteTable.Routes); 4: } 5: 6: public static void RegisterRoutes(RouteCollection routes) 7: { 8: routes.Add(new Route("{controller}/{action}/{id}", new RouteValueDictionary { { "controller", "Home" }, { "Action", "Index" }, { "id", "" } }, new MvcRouteHandler()) ); 9: 10: routes.Add(new Route("Category/{Action}/{categoryName}", new RouteValueDictionary {{"categoryName", "food"}, {"Action", "show"}}, new MvcRouteHandler() ) ); 11: }
在上述代码中,第3行中的方法参数使用了RouteTable.Routes属性,以便获得Route Collection类的实例化对象,然后通过第8行、第10行RouteCollection类中的Add()方法,在集合类中添加新的路由。
2.3.3 MapRoute()扩展方法
正如前面所述,路由程序集(System.Web.Routing)是在2008年8月11日更新的.NET 3.5框架SP1版本中发布的,而ASP.NET 3.5 MVC 1.0版本则直到2009年3月18日才正式发布。在ASP.NET 3.5 MVC版本的不断改进中,微软的开发团队感觉到上述的路由设置,给开发者带来了不便,但是路由程序集已经发布,如何在路由程序集中添加新的功能呢?
.NET 3.5框架中支持新的特性——扩展方法,就可以实现在原有的类别中添加新的实现方法,从而实现新的功能。
位于命名空间System.Web.Mvc中的RouteCollectionExtensions类,就是一个静态类,其中定义的方法就是扩展方法。
RouteCollectionExtensions类的UML类图,如图2-7所示。
图2-7 RouteCollectionExtensions类的UML类图
在RouteCollectionExtensions类中,针对路由集合类RouteCollection扩展了两类方法,它们分别是IgnoreRoute()方法和MapRoute()方法。
IgnoreRoute()方法主要用于设置不需要使用路由解析的URL地址,有两个重载的方法;MapRoute()方法则用于设置各种路由,一共有6个重载的方法。
RouteCollectionExtensions类的扩展方法列表,见表2-2。
表2-2 RouteCollectionExtensions类的扩展方法列表
从上述表中可以看出,各种扩展方法中的输入参数仍然是Route类中的5个基本属性,不过通过定义新的扩展方法,这些基本属性的变量类型有所改变,以便开发者更加方便地设置这些属性。如路由的默认值由原有的RouteValueDictionary类型,改变为现有的object类型;命名空间也由原有的RouteValueDictionary类型,改变为现有的string[]类型。
利用扩展方法MapRoute(),可以在Global.asax文件中重新设置代码清单2-4中的多个路由,设置的代码见代码清单2-5
代码清单2-5全局应用程序类的配置代码
1: protected void Application_Start() 2: { 3: RegisterRoutes(RouteTable.Routes); 4: } 5: 6: public static void RegisterRoutes(RouteCollection routes) 7: { 8: routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 9: 10: routes.MapRoute( "Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "" } ); 11: 12: routes.MapRoute( "Category", "Category/{Action}/{categoryName}", new {categoryName="food", Action="show"} ); 13: }
从上述代码中可以看出,利用MapRoute()扩展方法来设置路由,相对原有的路由设置方法来说,设置的语句相对简单、明了。
2.3.4 优化路由设置
在路由的设置中,路由名称是可选的输入参数,路由名称可以用来生成URL路由,但是在路由解析中没有什么作用。
不过这里需要说明的是,当开发者使用路由名称来生成URL路由的时候,路由模块将快速定位到指定名称的路由。也就是说,如果该路由位于路由表中的第100个位置,则路由模块将直接跳转到路由表的第100个位置,定位到指定名称的路由,否则将会通过查询的方式,一个接一个的查询,一直查询到第100个位置的路由。
对于如下被设置的路由:
routes.MapRoute( "products-route", "products/{category}", new { controller = "products", action = "category", } );
上述被设置的路由名称为“products-route”,如果在视图中生成相关的路由链接,建议使用如下代码:
<%= Html.RouteLink("Show Beverages", "products-route", new { category = "beverages" }) %>
在上述生成的URL代码中,指定了路由的名称“products-route”。假如不设置路由的名称,有可能寻找到其他匹配的路由。使用指定路由的好处,可以不必指明路由的其他参数,例如该路由的控制器、动作方法等。
优化路由设置的第2个方法,将最常用的路由存放在路由表的最前面。该方法不仅提高生成URL路由的效率,而且还提高路由解析的效率。这是因为在解析路由的过程中,一旦选找到匹配的路由,就停止路由解析。假如匹配的路由存放在路由表中的第100个位置,那么路由模块将会通过查询、比较的方式,直到第100个位置的路由;而如果将匹配的路由存放在路由表的最前面,很显然将节省寻找匹配路由的时间。
但是需要说明的是,在改变路由的存放位置时,需要注意路由的次序改变是否实质性影响匹配结果。
2.4 自定义路由约束
要设置自定义路由约束,需要实现接口IRouteConstraint中的Match()方法。在自定义路由约束“RouteConstraintSample”项目中,设置的路由格式为“archive/{year}/{month}/{day}”,其中的year、month及day参数是有约束条件的,它们不仅要求是数字,而且这些数字还有一定的取值范围,如year参数的取值范围为1900~2100,设定近200年的范围;month参数的取值范围为1~12; day参数的取值范围下限为1,上限则分别为28、30或者31。
根据上述参数的约束条件,这里设置了专门的自定义路由约束,它们分别是YearRouteConstraint类、MonthRouteConstraint类和DayRouteConstraint类。
2.4.1 添加自定义路由约束类
1.YearRouteConstraint类
首先添加YearRouteConstraint类,YearRouteConstraint类的实现代码见代码清单2-6。
代码清单2-6 YearRouteConstraint类的实现代码
1: public class YearRouteConstraint : IRouteConstraint 2: { 3: public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) 4: { 5: if ((routeDirection == RouteDirection.IncomingRequest) && (parameterName.ToLower(CultureInfo.InvariantCulture) == "year")) 6: { 7: try 8: { 9: int year = Convert.ToInt32(values["year"]); 10: if ((year >= 1900) && (year <= 2100)) 11: return true; 12: } 13: catch 14: { 15: return false; 16: } 17: } 18: return false; 19: } 20: }
在上述代码中,第5行判断输入的参数是否为year,第9行得到参数year的取值,第10行判断参数year的取值是否在合理的范围内,否则就返回false。
2.MonthRouteConstraint类
然后添加MonthRouteConstraint类,MonthRouteConstraint类的实现代码见代码清单2-7。
代码清单2-7 MonthRouteConstraint类的实现代码
1: public class MonthRouteConstraint : IRouteConstraint 2: { 3: public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) 4: { 5: if ((routeDirection == RouteDirection.IncomingRequest) && (parameterName.ToLower(CultureInfo.InvariantCulture) == "month")) 6: { 7: try 8: { 9: int month = Convert.ToInt32(values["month"]); 10: if ((month >= 1) && (month <= 12)) 11: return true; 12: } 13: catch 14: { 15: return false; 16: } 17: } 18: return false; 19: } 20: }
在上述代码中,第5行判断输入的参数是否为month,第9行得到参数month的取值,第10行判断参数month的取值是否为1到12的整数,否则就返回false。
3.DayRouteConstraint类
最后添加DayRouteConstraint类,DayRouteConstraint类的实现代码见代码清单2-8。
代码清单2-8 DayRouteConstraint类的实现代码
1: public class DayRouteConstraint : IRouteConstraint 2: { 3: public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) 4: { 5: if ((routeDirection == RouteDirection.IncomingRequest) && (parameterName.ToLower(CultureInfo.InvariantCulture) == "day")) 6: { 7: try 8: { 9: int month = Convert.ToInt32(values["month"]); 10: int day = Convert.ToInt32(values["day"]); 11: 12: if (day < 1) 13: return false; 14: 15: switch (month) 16: { 17: case 1: 18: case 3: 19: case 5: 20: case 7: 21: case 8: 22: case 10: 23: case 12: 24: if (day <= 31) return true; 25: break; 26: case 2: 27: if (day <= 28) return true; 28: break; 29: case 4: 30: case 6: 31: case 9: 32: case 11: 33: if (day <= 30) return true; 34: break; 35: } 36: } 37: catch 38: { 39: return false; 40: } 41: } 42: return false; 43: } 44: }
在上述代码中,第5行判断输入的参数是否为day,第9行得到参数month的取值,第10行得到参数day的取值,然后通过月份、日期共同判断参数day是否为正确的日期天,例如2月份中的天必须小于或者等于28;1月份、3月份、5月份、7月份、8月份、10月份和12月份的天必须小于或者等于31;而4月份、6月份、9月份和11月份的天必须小于或者等于30等;否则就返回false。
2.4.2 设置路由
成功添加3个自定义路由约束类之后,还需要在全局应用程序类Global.asax.cs中配置路由,见代码清单2-9。
代码清单2-9全局应用程序类的配置代码
1: void Application_Start(object sender, EventArgs e) 2: { 3: RegisterRoutes(RouteTable.Routes); 4: } 5: 6: public static void RegisterRoutes(RouteCollection routes) 7: { 8: routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 9: 10: routes.MapRoute( "Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "" } ); 11: 12: routes.MapRoute( "Archive", "archive/{year}/{month}/{day}", new { controller = "Archive", action = "Index", year = "", month = "", day = "" }, new { year = new YearRouteConstraint(), month = new MonthRouteConstraint(), day = new DayRouteConstraint() } ); 13: }
在上述代码中,第12行设置了路由“archive/{year}/{month}/{day}”,并在其中定义了路由约束类,它们分别是YearRouteConstraint类、MonthRouteConstraint类和DayRouteConstraint类,检查路由中年、月、日的数值是否在正确的范围内。
2.4.3 运行界面
“RouteConstraintSample”项目的运行界面,如图2-8所示。
图2-8 Route Constraint Sample项目的运行界面
从图2-8中可以看出,只要输入正确的年、月、日,就会得到正确的解析结果。
如果输入不正确的年、月、日,如输入“Archive/2009/04/31”,就不会满足自定义的路由约束条件,是一个无效的路由,就会出现错误的运行界面,如图2-9所示。
图2-9 不正确的年、月、日
2.5 路由解析分析
路由解析分析,主要说明路由解析的主要流程,也就是路由解析管道,实现路由解析的UrlRoutingModule类、IRouteHandler接口和IHttpHandler接口。
2.5.1 路由解析管道
在路由的解析过程中,UrlRoutingModule类扮演着非常重要的角色,一个典型的路由解析管道图,如图2-10所示。
图2-10 路由解析管道
从图中可以看出,UrlRoutingModule类是ASP.NET 3.5 MVC网站中处理程序的入口,每当用户在浏览器中键入一个URL地址,就发出一个用户请求,UrlRoutingModule类就响应用户的请求,处理用户的请求;检索RoutTable类中的RoutCollection集合,获得匹配的路由;通过路由解析,得到Route类的实例化对象;将用户的请求分发到实现接口IRouteHandler的路由处理程序,并输入RequestContext参数;最后再次分发到实现接口IHttpHandler的MvcHandler处理程序,定位到相关的控制器,从而执行控制器中的相关动作方法,实现响应的输出。
2.5.2 UrIRoutingModuIe类
UrlRoutingModule类主要实现路由的处理,如检索、匹配等解析过程,需要说明的是,要在ASP.NET 3.5 MVC网站中使用UrlRoutingModule类处理路由,必须在配置文件web.config中,配置如下代码:
<httpModules> <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" /> </httpModules>
上述代码将UrlRoutingModule类配置到<httpModules>…</httpModules>节点中,将路由处理模块设置为网站处理程序的入口,从而实现路由解析的管道。
UrlRoutingModule类的UML类图,如图2-11所示。
图2-11 UrlRoutingModule类的UML类图
下面简要说明UrlRoutingModule类中各种方法的执行流程。
首先执行IHttpModule.Init()方法,调用Init()方法,然后触发被注册的事件OnApplicationPostResolveRequestCache(),执行事件处理程序PostResolveRequestCache();触发被注册的事件OnApplicationPostMapRequestHandler(),执行PostMapRequestHandler()事件处理程序。
UrlRoutingModule类的实现代码见代码清单2-10。
代码清单2-10 UrlRoutingModule类的实现代码
1: public class UrlRoutingModule : IhttpModule 2: { 3: private static readonly object _requestDataKey = new object(); 4: private RouteCollection _routeCollection; 5: 6: protected virtual void Dispose() 7: { 8: } 9: 10: protected virtual void Init(HttpApplication application) 11: { 12: application.PostResolveRequestCache += new EventHandler( this.OnApplicationPostResolveRequestCache); 13: application.PostMapRequestHandler += new EventHandler( this.OnApplicationPostMapRequestHandler); 14: } 15: 16: private void OnApplicationPostMapRequestHandler(object sender, EventArgs e) 17: { 18: HttpContextBase context = new HttpContextWrapper(((HttpApplication) sender).Context); 19: this.PostMapRequestHandler(context); 20: } 21: 22: private void OnApplicationPostResolveRequestCache(object sender, EventArgs e) 23: { 24: HttpContextBase context = new HttpContextWrapper(((HttpApplication) sender).Context); 25: this.PostResolveRequestCache(context); 26: } 27: 28: public virtual void PostMapRequestHandler(HttpContextBase context) 29: { 30: RequestData data = (RequestData) context.Items[_requestDataKey]; 31: 32: if (data ! = null) 33: { 34: context.RewritePath(data.OriginalPath); 35: context.Handler = data.HttpHandler; 36: } 37: } 38: 39: public virtual void PostResolveRequestCache(HttpContextBase context) 40: { 41: RouteData routeData = this.RouteCollection.GetRouteData(context); 42: 43: if (routeData ! = null) 44: { 45: IRouteHandler routeHandler = routeData.RouteHandler; 46: if (routeHandler == null) 47: { 48: throw new InvalidOperationException(string.Format( CultureInfo.CurrentUICulture, RoutingResources. UrlRoutingModule_NoRouteHandler, new object[0])); 49: } 50: if (! (routeHandler is StopRoutingHandler)) 51: { 52: RequestContext requestContext = new RequestContext(context, routeData); 53: IHttpHandler httpHandler = routeHandler. GetHttpHandler(requestContext); 54: if (httpHandler == null) 55: { 56: throw new InvalidOperationException(string.Format( CultureInfo.CurrentUICulture, RoutingResources. UrlRoutingModule_NoHttpHandler, new object[] { routeHandler.GetType() })); 57: } 58: RequestData data2 = new RequestData(); 59: data2.OriginalPath = context.Request.Path; 60: data2.HttpHandler = httpHandler; 61: context.Items[_requestDataKey] = data2; 62: context.RewritePath("~/UrlRouting.axd"); 63: } 64: } 65: } 66: 67: void IHttpModule.Dispose() 68: { 69: this.Dispose(); 70: } 71: 72: void IHttpModule.Init(HttpApplication application) 73: { 74: this.Init(application); 75: } 76: 77: public RouteCollection RouteCollection 78: { 79: get 80: { 81: if (this._routeCollection == null) 82: { 83: this._routeCollection = RouteTable.Routes; 84: } 85: return this._routeCollection; 86: } 87: set 88: { 89: this._routeCollection = value; 90: } 91: } 91: 92: private class RequestData 93: { 94: private IHttpHandler httpHandler; 95: private string originalPath; 96: 97: public IHttpHandler HttpHandler 98: { 99: get 100: { 101: return this.httpHandler; 102: } 103: set 104: { 105: this.httpHandler = value; 106: } 107: } 108: 109: public string OriginalPath 110: { 111: get 112: { 113: return this.originalPath; 114: } 115: set 116: { 117: this.originalPath = value; 118: } 119: } 120: } 121: }
在上述代码中,第72行到第75行所定义的IHttpModule.Init()方法,是路由处理模块的程序入口,其中又调用第10行到第14行所定义的Init()方法。
在Init()方法中定义了两个事件,它们分别是PostResolveRequestCache事件和PostMapRequestHandler事件,这两个事件发生在诸多事件BeginReques、AuthenticateRequest、AuthorizeRequestCache、ResolveRequestCache之后。
需要说明的是,PostResolveRequestCache事件是在还没有给HttpContent设置HTTP处理程序的前一个事件;而PostMapRequestHandler事件发生时,则可以在该事件中设置自定义的HTTP处理程序。
第16行到第20行定义了事件PostMapRequestHandler;第22行到第26行则定义了事件PostResolveRequestCache。
第28行到第37行定义了事件处理程序PostMapRequestHandler(),根据PostResolveR equestCache事件处理程序中所设置的路由信息(RequestData),重写URL,并设置HTTP处理程序为MvcHandler对象。
第39行到第65行所定义了事件处理程序PostResolveRequestCache()。第41行调用Route Collection类中的GetRouteData()方法,获得匹配的路由数据对象RouteData,否则就返回null;第45行获得RouteData的RouteHandler属性,也就是MvcRouteHandler对象;第53行获得实现接口IHttpHandler的实例化对象MvcHandler,而且其中包括requestContext对象;而第62行所定义的URL重写,这是为了使路由处理模块能够在IIS 7.0中实现路由所采用的一种简单解决方法。
第77行到第91行设置了RouteCollections属性;第92行到第120行定义了一个嵌套类RequestData,主要封装了两个属性,它们分别是HttpHandler和OriginalPath。
2.5.3 IRouteHandIer接口
UrlRoutingModule类在获得URL路由后,将用户的请求分发到实现接口IRouteHandler的MvcRouteHandler类,并传入RequestContext参数。
MvcRouteHandler类的实现代码,见代码清单2-11。
代码清单2-11 MvcRouteHandler类的实现代码
1: public class MvcRouteHandler : IRouteHandler 2: { 3: protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext) 4: { 5: return new MvcHandler(requestContext); 6: } 7: 8: IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext) 9: { 10: return GetHttpHandler(requestContext); 11: } 12: }
在上述代码中,MvcRouteHandler的主要功能就是获得实现IHttpHandler接口的MvcHandler类的实例化对象。这里需要说明的是,在这个实例化对象中,传入了请求的上下文requestContext参数,而RequestContext类(位于命名空间System.Web.Routing),封装了RouteData对象,包括的路由数据有:DataTokens、Route、RouteHandler和Values。
2.5.4 IHttpHandIer接口
MvcHandler类实现了IHttpHandler接口,并保存了请求的上下文requestContext参数,MvcHandler类的UML类图,如图2-12所示。
图2-12 MvcHandler类的UML类图
在MvcHandler类中,首先执行IHttpHandler.ProcessRequest()方法,调用ProcessRequest()方法,获得指定名称的控制器,执行该控制器中的相关方法。
MvcHandler类的实现代码,见代码清单2-12。
代码清单2-12 MvcHandler类的实现代码
1: public class MvcHandler : IHttpHandler, IRequiresSessionState 2: { 3: private ControllerBuilder _controllerBuilder; 4: private static string MvcVersion = GetMvcVersionString(); 5: 6: public static readonly string MvcVersionHeaderName ="X-AspNetMvc-Version"; 7: 8: public MvcHandler(RequestContext requestContext) 9: { 10: if (requestContext == null) 11: { 12: throw new ArgumentNullException("requestContext"); 13: } 14: RequestContext = requestContext; 15: } 16: 17: protected virtual bool IsReusable 18: { 19: get 20: { 21: return false; 22: } 23: } 24: 25: internal ControllerBuilder ControllerBuilder 26: { 27: get 28: { 29: if (_controllerBuilder == null) 30: { 31: _controllerBuilder = ControllerBuilder.Current; 32: } 33: return _controllerBuilder; 34: } 35: set 36: { 37: _controllerBuilder = value; 38: } 39: } 40: 41: public static bool DisableMvcResponseHeader 42: { 43: get; 44: set; 45: } 46: 47: public RequestContext RequestContext 48: { 49: get; 50: private set; 51: } 52: 53: protected internal virtual void AddVersionHeader(HttpContextBase httpContext) 54: { 55: if (! DisableMvcResponseHeader) 56: { 57: httpContext.Response.AppendHeader(MvcVersionHeaderName, MvcVersion); 58: } 59: } 60: 61: private static string GetMvcVersionString() 62: { 63: return new AssemblyName(typeof(MvcHandler).Assembly.FullName). Version.ToString(2); 64: } 65: 66: protected virtual void ProcessRequest(HttpContext httpContext) 67: { 68: HttpContextBase iHttpContext = new HttpContextWrapper(httpContext); 69: ProcessRequest(iHttpContext); 70: } 71: 72: protected internal virtual void ProcessRequest(HttpContextBase httpContext) 73: { 74: AddVersionHeader(httpContext); 75: 76: string controllerName = RequestContext.RouteData. GetRequiredString("controller"); 77: IControllerFactory factory = ControllerBuilder. GetControllerFactory(); 78: IController controller = factory.CreateController(RequestContext, controllerName); 79: if (controller == null) 80: { 81: throw new InvalidOperationException( String.Format( CultureInfo.CurrentUICulture, MvcResources.ControllerBuilder_FactoryReturnedNull, factory.GetType(), controllerName)); 82: } 83: try 84: { 85: controller.Execute(RequestContext); 86: } 87: finally 88: { 89: factory.ReleaseController(controller); 90: } 91: } 92: 93: bool IHttpHandler.IsReusable 94: { 95: get 96: { 97: return IsReusable; 98: } 99: } 100: 101: void IHttpHandler.ProcessRequest(HttpContext httpContext) 102: { 103: ProcessRequest(httpContext); 104: } 105: }
在上述代码中,首先执行第101行到第103行所定义的IHttpHandler.ProcessRequest()方法,调用第66行到第70行所定义的ProcessRequest()方法,执行第72行到第91行所定义的ProcessRequest()内部方法。
第76行获得指定控制器的名称;第77行得到控制器工厂的实例化对象factory;第78行得到实例化的控制器对象controller;第85行执行控制器的Execute()方法,定位到该控制器中的相关动作方法。
2.6 在Web Form项目中使用路由
路由程序集System.Web.Routing位于.NET框架3.5的SP1版本中,是与ASP.NET 3.5 MVC框架分离的,因此,在传统的Web Form项目中也可以使用路由。
要实现在Web Form项目中使用路由,首先需要创建实现IRouteHandler接口的WebFormRouteHandler类,然后在全局应用程序类中配置路由的映射即可。
2.6.1 WebFormRouteHandIer类
WebFormRouteHandler类,实现了IRouteHandler接口,最终返回一个实现接口IHttpHandler的实例化对象,而需要说明的是,在传统的Web Form项目中,任何一个页面都是HttpHandler的实例化对象。
WebFormRouteHandler类的实现代码,见代码清单2-13。
代码清单2-13 WebFormRouteHandler类的实现代码
1: public class WebFormRouteHandler : IRouteHandler 2: { 3: public WebFormRouteHandler(string virtualPath) 4: { 5: this.VirtualPath = virtualPath; 6: } 7: 8: public string VirtualPath { get; private set; } 9: 10: public IHttpHandler GetHttpHandler(RequestContext requestContext) 11: { 12: var page = BuildManager.CreateInstanceFromVirtualPath(VirtualPath, typeof(Page)) as IHttpHandler; 13: return page; 14: } 15: }
在上述代码中,第3行到第7行定义了WebFormRouteHandler类的构造函数,主要用于初始化虚拟路径属性VirtualPath(第8行),第10行到第14行实现了接口IHttpHandler中的GetHttpHandler()方法,通过第12行调用位于命名空间System.Web.Compilation中BuildManager类的CreateInstanceFromVirtualPath()方法,得到实例化的页面对象。
2.6.2 配置全局应用程序类
成功创建WebFormRouteHandler类之后,还需要在全局应用程序类Global.asax.cs中配置路由,实现路由到传统Web Form页面的映射,见代码清单2-14。
代码清单2-14全局应用程序类的配置代码
1: void Application_Start(object sender, EventArgs e) 2: { 3: RegisterRoutes(RouteTable.Routes); 4: } 5: 6: public static void RegisterRoutes(RouteCollection routes) 7: { 8: routes.Add("Named", new Route("foo/bar", new WebFormRouteHandler("~/forms/blech.aspx"))); 9: 10: routes.Add("Numbers", new Route("one/two/three", new WebFormRouteHandler("~/forms/haha.aspx"))); 11: }
在上述代码中,第8行、第10行是配置路由的关键代码。第8行设置了一个名称为“Named”的路由,将被添加的路由“foo/bar”映射为传统的“~/forms/blech.aspx”页面;也就是说,当在浏览器中键入相关的路由“foo/bar”,浏览器中返回的页面是页面“~/forms/blech.aspx”。
第10行设置了一个名称为“Numbers”的路由,将被添加的路由“one/two/three”映射为传统的“~/forms/haha.aspx”页面。
2.6.3 运行界面
运行“WebFormRouting”网站,首页的运行界面如图2-13所示。
图2-13 首页的运行界面
在图2-13中,设置了两个链接,它们分别是“foo/bar”和“one/two/three”,实际上是被设置的路由,而不是传统的Web Form页面链接,单击其中的“foo/bar”路由链接,打开如图2-14所示的运行界面。
图2-14 Blech.aspx页面的运行界面
从图2-14中可以看出,打开的页面为Blech.aspx,但浏览器中显示的地址为“foo/bar”,通过使用自定义的路由处理程序WebFormRouteHandler,可以设置更加人性化的URL地址,使得URL地址变得有意义。
在图2-13中,如果单击“one/two/three”路由链接,则会打开如图2-15所示的运行界面。
图2-15 Haha.aspx页面的运行界面
从图中可以看出,被运行的页面为Haha.aspx,但浏览器中显示的地址为“one/two/three”,通过使用路由处理程序WebFormRouteHandler,可以设置任何所需要的URL地址。
2.7 思考与提高
本章说明了如何利用MapRoute()扩展方法设置路由,如何自定义路由约束,分析了路由的解析过程,并通过实例实现了如何在Web Form项目中使用路由。令人欣喜的是,在即将发布的ASP.NET 4.0中,开发者将能够在传统的Web Form项目中,非常方便地设置个性化的路由。
在Web Form项目中使用路由的时候,在设置个性化的路由到页面的映射过程中,并没有考虑到被映射页面的安全问题,也就是说,如果浏览被映射的页面需要安全验证,那么请读者思考在相关的位置,如何添加相关的代码而实现。