在ASP.NET WebForm(.NET 3.5 SP1)中使用ASP.NET路由(Routing)-第二版
看了几节asp.net mvc的教程,又回过头来反思WebForm。忽然觉得,其实WebForm也不是像我原来理解的那么糟糕;而MVC模式,也并不是像我原来想象的那么完美的。任何事物都是多面性的,这句话一点也不差。所以,在IT行业里,就是不能太完美主义。
进入正题,System.Web.Routing的确是个好东西,可以映射出各式各样好看、容易理解的URL。据说本来是给asp.net mvc 框架写的,后来以独立程序集的方式发布在于.net 3.5 下面。所以,第一个准备工作:要安装完整的.net 3.5 sp1。
第二步呢,就是创建一个基于.Net Framework 3.5的Asp.Net WebApplication吧。创建完之后,要添加对“System.Web.Routing”程序集的引用。
然后在这个应用程序中添加一个“全局应用程序类”,也就是传说中的Global.asax。为什么要用到这个呢?因为路由规则是先于应用程序请求被处理的,而且只注册一次;应用程序会在启动的时候,把你的路由规则添加到路由表。当接收到“Request”的时候,会从路由表中寻找匹配的第一个路由规则,找到了则把当前请求的上下文内容路由到那个规则指定的aspx页面去处理。并且可以封装一些规则中定义的参数,以便在aspx页面的代码中获取。
这里需要声明一点,WebForm使用asp.net路由和mvc使用路由的方法是不一样的。mvc中讲URL Routing的章节都有一个“hello world”式的用法那就是“{controler}/{action}/{id}”。这条规则是一个非常典型的asp.net mvc路由规则,其实很容易理解,路由在mvc模式的asp.net web应用程序中,会根据规则中指定的“controler”去执行指定的action,然后会传一个值给参数id。但是这种规则在WebForm里显然没用,因为WebForm都是Page类,没有所谓的控制器跟行为。所以WebForm中的路由规则,都需要对应一个相应的虚拟路径,比如我的新闻页面的动态地址是“news.aspx?id=123“,那么我就可以写一个“news/{id}”规则去对应。这跟配置UrlRewrite规则中的lookfor、sendto是一个道理。(“{}”,就是“URL参数变量”的标识,也就是说,用“{}”括起来的,就是地址参数,而里面的内容就是参数名称。如果我要实现“/2011/05/10“这种格式的地址,那么规则就应该是“{year}/{month}/{day}”,相邻的参数变量用“/”分隔。)
好吧,我承认我有点啰嗦,现在就开始设计你们路由规则吧。比如我想实现几种典型的规则,在Demo中都用Default.aspx页面来做最终的实现。我想到这四种(带$号的为变量):http://localhost/category/$id、http://localhost/news/$id/$page、http://localhost/news/$year/$month/$day、http://localhost/search?q=$keywords 怎么样,都很拉风吧!要是你自己的网站上这么实现了!@#¥……先别急着得意,跟着往下一步一步来。
现在根据上面几种效果,依次得到下面的这些规则:“category/{id}”、“news/{id}/{page}”、"news/{year}/{month}/{day}"、“search”。写到这里,喜欢思考的朋友一般都该有两个疑问了。
第一个肯定是“为什么所有的规则前面都没有加~或者/来指定根目录?”,因为Web应用程序可能作为虚拟目录存在,所以遇到这种情况很显然用“/”肯定不行了吧。再一个,如果是不是虚拟目录的情况,“~/”的效果相当于这个虚拟目录本身的路径,而不加“~/”事实上也指的这个路径,所以加“~”也没有什么必要了,因为规则就是从当前应用程序的所在目录开始的。
第二个问题应该是关于“{search}”这个规则的。效果中明明是“search?q=$keywords”那按理说规则应该写成“search?q={keywordss}”才对啊?!再问问自己,对吗?我们知道在互联网的网址规则中“?”问号是用来分割请求的文件或目录与参数的,如果请求“search?q=aaa”最终到达的,应该是那个路径呢?当然是“search”,而“q”这个参数,根本就不需要写到规则中来,因为它本身就是作为参数用的。所以,“search?q=$keywords”这个效果其实就是要把“Default.aspx”映射成“search”,现在明白了吧!
Go on,规则都已经准备好了之后,首先在Global.asax.cs代码中using “System.Web.Routing”这个命名空间。在其代码中添加一个自定义方法,用来创建路由规则,约定的写法是这样的(MSDN上的), public static void RegisterRoutes( RouteCollection routes) { }。现在就在这个方法里编写路由规则注册的过程。
using System; using System.Web; using System.Web.Routing; namespace UrlRoutingDemo { public class Global : HttpApplication { public static void RegisterRoutes(RouteCollection routes) { routes.RouteExistingFiles = false; //注册路由规则 routes.Add(new Route("category/{id}", new RouteHandler("~/Default.aspx"))); routes.Add(new Route("news/{id}/{page}", new RouteHandler("~/Default.aspx"))); routes.Add(new Route("news/{year}/{month}/{day}", new RouteHandler("~/Default.aspx"))); routes.Add(new Route("search", new RouteHandler("~/Default.aspx"))); } protected void Application_Start(object sender, EventArgs e) { //注册自定义路由规则到路由表 RegisterRoutes(RouteTable.Routes); } } }
停下了吧!我就知道,你的System.Web.Routing里面一定没有RouteHandler这个类型。这个类型是个什么东东?既然你的程序集里没有,那肯定是个自定义类型。这个类型怎么创建呢?(不想了解真相的朋友可以忽略此段了,直接按下一段的代码创建)别急,先去MSDN上查查Route类的两个参数的构造函数 public Route( string url, IRouteHandler routeHandler);原来RouteHandler是一个实现了 IRouteHandler接口的类型,但是这个接口的作用是什么呢?又该怎么来实现它呢?那我们再看看MSDN上对 IRouteHandler接口的解释:定义类必须实现(此接口)才能处理匹配路由模式的请求的协定。 不难理解,也就是说,实现了IRouteHandler才能对匹配了某条路由规则的请求做处理。再来看看此接口都有哪些成员……
using System.Web; namespace System.Web.Routing { public interface IRouteHandler { IHttpHandler GetHttpHandler(RequestContext requestContext); } }
它只有一个方法成员 IHttpHandler GetHttpHandler( RequestContext requestContext)。看上去好像牵扯的东西越来越多了,实际上,已经开始渐渐明朗了。MSDN上说 RequestContext 是封装与所定义路由匹配的 HTTP 请求的(跟路由相关的)相关信息。因为基于WebForm的路由是对应到真实的路径的(虚拟路径),也就是说我们知道符合哪条规则的那个路由应该由哪个页面进行处理,利用.net的反射机制,我们就可以直接根据那个真实的aspx文件的路径反射出一个相应的页面对象。Page类肯定是实现了IHttpHandler接口的,而RequestContext中,封装着路由获取到的参数键值等信息。再结合IRouteHandler接口的解释来理解,就不难明白,实现这个 IRouteHandler接口的意义就在于对Routing获取到的,与那一条路由规则所匹配的请求的相关信息和相应的Page类做合并处理,并返回这个处理过的Page对象。
原理就是这样,可是RequestContext中有用的信息怎么放到Page对象里面呢?我们可以写一个继承自Page类的自定义Page类型,就叫它RoutePage吧,在这个RoutePage中增加一个RequestContext类型的属性就可以了。来看RoutePage类型的实现代码:先来往System.Web.Routing命名空间中补一个IRouteablePage接口(可以降低其它类型对RoutePage的依赖),
namespace System.Web.Routing { public interface IRouteablePage { RequestContext ReqeustContext { get; set; } } }
然后再在System.Web.UI命名空间中实现RoutePage类(我确实比较喜欢污染系统命名空间 )
using System.Web.Routing; namespace System.Web.UI { public class RoutePage : Page, IRouteablePage { public RequestContext ReqeustContext { get; set; } /// <summary> /// 获取当前请求附带的路由参数的集合 /// </summary> public RouteValueDictionary RouteValues { get { if (ReqeustContext != null) return ReqeustContext.RouteData.Values; return null; } } /// <summary> /// 获取指定名称的路由地址参数的值 /// </summary> /// <param name="key">名称</param> /// <returns></returns> public object GetRouteValue(string key) { object resultValue = null; if (RouteValues != null && RouteValues.Count > 0) RouteValues.TryGetValue(key, out resultValue); return resultValue; } } }
这样一来,我的Default.aspx的页面类就要继承System.Web.UI.RoutePage了(意思是说,所有要处理路由的aspx页面类都要继承System.Web.UI.RoutePage)
OK,万事俱备,只欠东风RouteHandler类型的实现了!这个我同样采用污染系统命名空间的方式实现到System.Web.Routing命名空间下面。
using System.Web.Compilation; using System.Web.UI; namespace System.Web.Routing { public class RouteHandler : IRouteHandler { public string VirtualPath { get; private set; } /// <param name="virtualPath">指定当前路由请求处理程序的目标虚拟路径</param> public RouteHandler(string virtualPath) { this.VirtualPath = virtualPath; } public IHttpHandler GetHttpHandler(RequestContext requestContext) { var originalPage = BuildManager.CreateInstanceFromVirtualPath( VirtualPath, typeof(Page)) as IHttpHandler; if (originalPage != null) { var routePage = originalPage as IRouteablePage; if (routePage != null) routePage.ReqeustContext = requestContext; } return originalPage; } } }
用BuildManager.CreateInstanceFromVirtualPath()方法根据指定的要处理那条路由请求的页面的虚拟路径来创建一个相应的Page对象,这个Page对象当然是我们前面自定义的RoutePage的实例了。(这里用 as 的好处是,如果类型转换不成功,不会产生异常,而返回一个null值)
呵,从一开始就说起的Global.asax,到这儿,总算可以继续下去了。:)
有了RouteHandler就可以Global.asax.cs中的RegisterRoutes方法写完了,写完之后,要在Application_Start方法中对它做一个调用。 RouteTable.Routes就是路由规则的集合,RouteTable就是系统定义的全局的静态路由表了。
最后,像使用UrlRewrite控件那样,也需要在<system.web></system.web>节的<httpModules></httpModules>子节点中添加对路由模块的引用。细心的朋友可以发现了:哎,你里面怎么还注册了Session?没错,据说使用Routing会导致路由页面取不到Sessiong值,在这里注册一下就可以了!虽然我还没遇到过,不过也跟着加吧,反正不要钱!Orz
<httpModules> <add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/> <add name="Session" type="System.Web.SessionState.SessionStateModule"/> <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule,System.Web.Routing,Version=3.5.0.0,Culture=neutral,PublicKeyToken=31BF3856AD364E35"/> </httpModules>
最最后,(|||- -!真的),要不然你怎么尝试你的成果啊!在Demo中打开Default.aspx,在页面里添加几行测试代码。
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="UrlRoutingDemo._Default" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" /> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> </head> <body> <form id="form1" runat="server"> <div> <ul> <li><a href="/category/13">第13个类别</a></li> <li><a href="/news/1333/1">第1333个新闻的第1页</a></li> <li><a href="/news/2011/5/10">2011年5月10日的新闻</a></li> <li><a href="/search?q=<%=Server.UrlEncode("拉登之死") %>">搜索:拉登之死</a></li> </ul> <asp:Literal ID="Literal1" runat="server"></asp:Literal> </div> </form> </body> </html>
在代码页中,写一个用于获取数据并输出的方法,然后在合适的地方调用它。那怎么在处理路由的页面获取请求过来的路由URL参数值呢?我上面提到了处理路由的页面类要继承我们之前自定义的RoutePage类吧,把页面往上拉,看看都有些什么有用的成员呢,其中直接提供了RouteValueDictionary类型的RouteValues,也提供了获取单个URL参数的值的方法GetRouteValue(string key)。
using System; using System.Web.UI; namespace UrlRoutingDemo { public partial class _Default : RoutePage { protected void Page_Load(object sender, EventArgs e) { Doit(); } void Doit() { string tmp = string.Empty; if (GetRouteValue("page") != null) { tmp = string.Format("第{0}条新闻的第{1}页", RouteValues["page"]); } else { if (GetRouteValue("id") != null) tmp = string.Format("第{0}个类别", GetRouteValue("id")); if (GetRouteValue("year") != null) tmp = string.Format("{0}年 {1}月 {2}日的新闻", RouteValues["year"], RouteValues["month"], RouteValues["day"]); if (Request.QueryString["q"] != null) tmp = "您搜索的是: " + Server.UrlDecode(Request.QueryString["q"]); } Literal1.Text = tmp; } } }
完了?按下F5让自己“惊喜”一下吧,哈!
当我们的UrlRoting项目发布后部署到IIS中的时候,遇到的第一个问题可能会是访问路由页面后会返回404错误!
IIS6的解决办法:选择相应的网站,在右键“属性”界面中选择“主目录”(网站)或“虚拟目录”(虚拟目录)选项卡;单击配置按钮,并在“应用程序配置”界面的“映射”选项卡的“插入通配符应用程序映射(执行顺序)”处点击“插入”,然后选择.net的isapi,通常是“C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll”,去掉“确认文件是否存在”前面的勾,然后确定。
IIS7+的解决办法:在应用程序的Web.Config文件“system.webServer”节下面的modules和handlers中分别添加下面的内容,保存后重启网站即可。
modules中添加
<remove name="Session"/> <add name="Session" type="System.Web.SessionState.SessionStateModule" preCondition="" /> <remove name="UrlRoutingModule"/> <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule,System.Web.Routing,Version=3.5.0.0,Culture=neutral,PublicKeyToken=31BF3856AD364E35" />
handlers中添加
<add name="UrlRoutingHandler" preCondition="integratedMode" verb="*" path="UrlRouting.axd" type="System.Web.HttpForbiddenHandler, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
PS:
此文章原文于2011年在我的百度空间发表,重新编排后转到此处!
近期有精力会再再介绍一下设置路由参数默认值、路由约束等方面的内容。
参考:
http://msdn.microsoft.com/zh-cn/library/cc668201(v=VS.90).aspx 感谢MSDN
http://deepumi.wordpress.com/2010/02/27/url-routing-in-asp-net-web-forms/ 感谢此文作者
http://www.cnblogs.com/jailu/archive/2010/07/02/how_to_use_url_routing_in_aspnet_web_form.html (这篇文章中提到的方法也不错,不用自定义Page类)