Routing In WebForm——不仅仅是mvc
2009-11-28 23:04 by Creason New, 1694 visits, 收藏, 编辑Introduction
mvc出来好长时间了,它也带红了另一个明星——Routing,Routing是一个不错的解决方案,但是它也很不完美,可谓是爱恨交加啊,爱的是它可以它是双向的,恨的是它支持双向并不很友好。比如在asp.net mvc中生成页面的url,这个话题自从Jeffrey Zhao的一篇《请别埋没了URL Routing》后似乎变的异常尖锐,自此,Jeffrey便尝试了各种方法,并努力进行了测试工作,我很佩服这种对待学问的求真精神。好,我们回到本话题。其实Routing并不局限于在mvc中使用,在传统的WebForm中也可以应用,当然你可以采用其它方案进行重写url,把xxxx.aspx?id=3这样的url重写成xxxx/3.aspx或类似的搜索引擎友好的URL,但是使用Routing还是很方便的。当然Routing还可以在Dynamic Data上使用,本文着重WebForm上的应用,暂且不讨论Dynamic Data中的情况。
理解UrlRoutingModule
说起Routing不能不说UrlRoutingModule,我们知道在asp.net的整个生命周期中Module和Handler是非常重要的,不管是webform还是mvc都是请求经过一些Module的处理,最终到达Handler并由其进行处理返回html数据。
这是来自msdn的一幅图片,它很好的解释了module和handler以及asp.net的关系,handler是一个实现了System.Web.IHttpHandler的类,module是一个实现了System.Web.IHttpModule的类,下面是它们的签名:
public interface IHttpHandler
{
bool IsReusable { get; }
void ProcessRequest(HttpContext context);
}
public interface IHttpModule
{
void Dispose();
void Init(HttpApplication context);
}
IHttpModule在管道中处理请求是从注册application事件下手的,而当请求到达IHttpHandler这里后,它就把所有的后续工作交给ProcessRequest去处理。我们说UrlRoutingModule是一个module,那么它就一定实现了IHttpModule接口了。
public class UrlRoutingModule : IHttpModule
那么到底UrlRoutingModule使用了那些application事件呢?在这之前我们先看看有哪些application事件,它们的顺序是什么样的?
_2.gif)
下面看看通过反编译System.Web.Routing程序集我得到的代码:
protected virtual void Init(HttpApplication application)
{
application.PostResolveRequestCache += new EventHandler(this.OnApplicationPostResolveRequestCache);
application.PostMapRequestHandler += new EventHandler(this.OnApplicationPostMapRequestHandler);
}
从上面的代码中不难看出UrlRoutingModule是利用了PostResolveRequestCache和PostMapRequestHandler来达到它的目的的。那么它在这两个事件里做了什么呢?接着看Reflector的代码:
private void OnApplicationPostResolveRequestCache(object sender, EventArgs e)
{
HttpContextBase context = new HttpContextWrapper(((HttpApplication) sender).Context);
this.PostResolveRequestCache(context);
}
public virtual void PostResolveRequestCache(HttpContextBase context)
{
RouteData routeData = this.RouteCollection.GetRouteData(context);
if (routeData != null)
{
IRouteHandler routeHandler = routeData.RouteHandler;
if (routeHandler == null)
{
throw new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, RoutingResources.UrlRoutingModule_NoRouteHandler, new object[0]));
}
if (!(routeHandler is StopRoutingHandler))
{
RequestContext requestContext = new RequestContext(context, routeData);
IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext);
if (httpHandler == null)
{
throw new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, RoutingResources.UrlRoutingModule_NoHttpHandler, new object[] { routeHandler.GetType() }));
}
RequestData data2 = new RequestData();
data2.OriginalPath = context.Request.Path;
data2.HttpHandler = httpHandler;
context.Items[_requestDataKey] = data2;
context.RewritePath("~/UrlRouting.axd");
}
}
}
上面代码中红色部分是“目的性”代码,它说明了UrlRoutingHandler使用PostResolveRequestCache的目的是分析请求上下文,得到RouteData,继而得到处理请求的IHttpHandler,找到这个Handler一切就好说了,调用IHttpHandler的ProcessRequest方法就大事告成了。下面是PostMapRequestHandler事件:
private void OnApplicationPostResolveRequestCache(object sender, EventArgs e)
{
HttpContextBase context = new HttpContextWrapper(((HttpApplication) sender).Context);
this.PostResolveRequestCache(context);
}
public virtual void PostMapRequestHandler(HttpContextBase context)
{
RequestData data = (RequestData) context.Items[_requestDataKey];
if (data != null)
{
context.RewritePath(data.OriginalPath);
context.Handler = data.HttpHandler;
}
}
它的作用就是把处理当前请求的IHttpHandler交给当前上下文。在mvc中,这个IHttpHandler是MvcRouteHandler,而在webform中就是其它的Handler了,MvcRouteHandler实现了IRouteHandler,它的GetHttpHandler方法返回一个MvcHandler对象,下面是MvcHandler对象的ProcessRequest方法的主要代码:
string controllerName = RequestContext.RouteData.GetRequiredString("controller");
IControllerFactory factory = ControllerBuilder.GetControllerFactory();
IController controller = factory.CreateController(RequestContext, controllerName);
从上面的语句中我们可以看出,它从我们的请求中得到Controller的名字,然后通过一个ControllerFactory生产一个controller对象,然后调用controller对象的action方法……,下面就是后话了。
下面我们总结一下,UrlRoutingModule的工作就是截获请求上下文,根据请求上下文匹配得到相应的RouteData,把处理请求的Handler交给请求上下文,在合适的时候调用Handler的ProcessRequest方法,返回结果。OK,下面就是真刀实枪把Routing用到webform上了。
项目结构
首先我们用vs新建一个web site项目。在vs帮我们做了模板后我们还要做下面几件事情:
1.引用程序集
引用System.Web.Abstractions和System.Web.Routing程序集
2.修改Web.config配置文件
在httpHandlers节点下添加一个handler,如下:
<add verb="*" path="UrlRouting.axd" type="System.Web.HttpForbiddenHandler, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/>
在httpModules节点下添加一个module,如下:
<add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
3.添加几个页面
为了能使用Routing功能,我们需要建立几个页面来做测试,下面是项目的组织结果。
4.写一些代码
为了实现想要的功能,我们要写一个自定义的Handler,命名为CustomerRouteHandler。
下面是项目的组织结构:

进一步分析
首先我们要在Global.asax.cs中找到Application_Start事件,并添加如下代码:
protected void Application_Start(object sender, EventArgs e)
{
RegisterRoutes();
}
private static void RegisterRoutes()
{
RouteTable.Routes.Add(
"subpage",
new Route(
"{folder}/{page}",
new RouteValueDictionary { { "folder", "subpage" }, { "page", "index" } },
new CustomerRouteHandler(null)));
RouteTable.Routes.Add(
"subpagewithid",
new Route(
"{folder}/{page}/{id}",
new RouteValueDictionary { { "folder", "subpage" }, { "page", "index" }, { "id", 3 } },
new CustomerRouteHandler(new string[] { "id" })));
RouteTable.Routes.Add(
"default",
new Route(
"{page}",
new RouteValueDictionary { { "page", "Default" } },
new CustomerRouteHandler(null)));
}
在程序启动时,我们注册了三条路由规则。你是否上面代码跟mvc中的有些相似呢?对的,就是这样。我们来看看Route的构造函数,其中有这么一个重载:
public Route(string url, RouteValueDictionary defaults, IRouteHandler routeHandler);
在注册的三条规则中,名为subpage是调用像xxx/xxx.aspx这样的页面用的,名为subpagewithid是调用xxx/xxx.aspx?id=xxx这样的页面用的,名为default是调用xxx.aspx这样的页面用的。第一个和第二个参数很好明白,分别是url和默认值,从构造函数中我们得知第三个参数是一个实现了IRouteHandler接口的对象,那么代码中的CustomerRouteHandler就是这个对象,它是我们自己实现的一个自定义Handler。
那么CustomerRouteHandler需要完成什么使命呢?在理解UrlRoutingModule一节我们讨论了UrlRoutingModule的职能:截获上下文,匹配RouteData,绑定Handler到上下文,执行ProcessRequest。还记得在mvc里的MapRoute方法为我们添加的MvcRouteHandler吗?还记得MvcRouteHandler.GetHttpHandler()返回一个MvcHandler对象吗?这里其实是一个道理,CustomerRouteHandler只不过是一个生产类似于MvcHandler的Handler对象的一个工厂,它根据RouteData生产相应的Handler对象。
那么为什么要生成不同的Handler,为什么不像mvc中那样使用通用的MvcHandler呢?因为在WebForm中,我们使用的是CodeBehind,每一个页面对应一个Handler,不信看看Page的签名:
public class Page : TemplateControl, IHttpHandler
而我们页面的CodeBehind类是这样的:
public partial class page2 : System.Web.UI.Page
可见page2这个页面要由page2这个实现了IHttpHandler的Handler来处理,所以要想返回page2的html,就要生成一个page2的Handler,因此返回什么样的Handler就由CustomerRouteHandler根据RouteData决定了。下面是CustomerRouteHandler的代码:
public class CustomerRouteHandler:IRouteHandler
{
private string _virtualPath;
private string[] _keys;
public CustomerRouteHandler(string[] keys)
{
_keys = keys;
}
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
if (_keys != null)
{
foreach (string str in _keys)
{
if (!requestContext.RouteData.Values.ContainsKey(str))
throw new ArgumentNullException("key");
else
requestContext.HttpContext.Items.Add(str, requestContext.RouteData.Values[str] as string);
}
}
string folder = string.Empty;
if (requestContext.RouteData.Values.ContainsKey("folder"))
folder = requestContext.RouteData.Values["folder"] as string;
string page = requestContext.RouteData.Values["page"] as string;
if (folder == string.Empty)
_virtualPath = @"~/" + page + ".aspx";
else
_virtualPath = @"~/" + folder + "/" + page + ".aspx";
IHttpHandler display;
try
{
display = BuildManager.CreateInstanceFromVirtualPath(_virtualPath, typeof(Page)) as IHttpHandler;
}
catch
{
display = BuildManager.CreateInstanceFromVirtualPath(@"~/Error.aspx", typeof(Page)) as IHttpHandler;
}
return display;
}
}
CustomerRouteHandler的设计也很简单,_virtualPath字段代表当前请求的页面的路径,而_key字段则是需要传递的参数,比如你xxxx/xxxx.aspx?id=4时,实际上就是Querystring一个id,那么_key中就会有"id”这个值,这个值由注册路由时确定,为了让CustomerRouteHandler添加相应的传值到请求上下文中。(这里实在是捉襟见肘的很,功能十分有限)
CustomerRouteHandler会检查folder和page值(就像mvc中的Controller和Action,不过这里表示的意思是:请求的页面是那个文件夹下的那个文件),来找到请求对应的页面,而其它的key则是对该页面的传值。
在构建好的_virtualPath之后,我们就可以创建一个Handler了,因为这时我们已经知道该创建那个页面的Handler了。这就需要调用BuildManager.CreateInstanceFromVirtuanPath()方法了,最后返回这个IHttpHandler实例就大功告成了。
在page1.aspx和page2.aspx页面我们写一段代码来显示querystring的值,测试的结果如下:

推荐文章:
《请别埋没了URL Routing》《关于ASP.NET URL Routing的几点内容》
《各种URL生成方式的对比》
《为URL生成设计流畅接口(Fluent Interface)》
作者:Creason New(Creason's Blog)
出处: http://www.cnblogs.com/niuchenglei
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。