第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所示的“添加引用”对话框。

active=true

图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所示的路由监测运行界面。

active=true

图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所示的路由监测运行界面。

active=true

图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所示的路由监测运行界面。

active=true

图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所示。

active=true

图2-5 Route类的UML类图

Route类是抽象类RouteBase的子类,在Route类中,设置了路由中的5个基本属性,它们分别是路由的约束Constraints、路由的命名空间DataTokens、路由参数的默认值Defaults、路由处理程序RouteHandler及路由URL;定义了4个重载的构造函数。

Route类的构造函数列表见表2-1。

表2-1 Route类的构造函数列表

active=true

从表中可以看出,在最简单的构造函数中,需要输入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所示。

active=true

图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所示。

active=true

图2-7 RouteCollectionExtensions类的UML类图

在RouteCollectionExtensions类中,针对路由集合类RouteCollection扩展了两类方法,它们分别是IgnoreRoute()方法和MapRoute()方法。

IgnoreRoute()方法主要用于设置不需要使用路由解析的URL地址,有两个重载的方法;MapRoute()方法则用于设置各种路由,一共有6个重载的方法。

RouteCollectionExtensions类的扩展方法列表,见表2-2。

表2-2 RouteCollectionExtensions类的扩展方法列表

active=true

从上述表中可以看出,各种扩展方法中的输入参数仍然是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所示。

active=true

图2-8 Route Constraint Sample项目的运行界面

从图2-8中可以看出,只要输入正确的年、月、日,就会得到正确的解析结果。

如果输入不正确的年、月、日,如输入“Archive/2009/04/31”,就不会满足自定义的路由约束条件,是一个无效的路由,就会出现错误的运行界面,如图2-9所示。

active=true

图2-9 不正确的年、月、日

2.5 路由解析分析

路由解析分析,主要说明路由解析的主要流程,也就是路由解析管道,实现路由解析的UrlRoutingModule类、IRouteHandler接口和IHttpHandler接口。

2.5.1 路由解析管道

在路由的解析过程中,UrlRoutingModule类扮演着非常重要的角色,一个典型的路由解析管道图,如图2-10所示。

active=true

图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所示。

active=true

图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所示。

active=true

图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所示。

active=true

图2-13 首页的运行界面

在图2-13中,设置了两个链接,它们分别是“foo/bar”和“one/two/three”,实际上是被设置的路由,而不是传统的Web Form页面链接,单击其中的“foo/bar”路由链接,打开如图2-14所示的运行界面。

active=true

图2-14 Blech.aspx页面的运行界面

从图2-14中可以看出,打开的页面为Blech.aspx,但浏览器中显示的地址为“foo/bar”,通过使用自定义的路由处理程序WebFormRouteHandler,可以设置更加人性化的URL地址,使得URL地址变得有意义。

在图2-13中,如果单击“one/two/three”路由链接,则会打开如图2-15所示的运行界面。

active=true

图2-15 Haha.aspx页面的运行界面

从图中可以看出,被运行的页面为Haha.aspx,但浏览器中显示的地址为“one/two/three”,通过使用路由处理程序WebFormRouteHandler,可以设置任何所需要的URL地址。

2.7 思考与提高

本章说明了如何利用MapRoute()扩展方法设置路由,如何自定义路由约束,分析了路由的解析过程,并通过实例实现了如何在Web Form项目中使用路由。令人欣喜的是,在即将发布的ASP.NET 4.0中,开发者将能够在传统的Web Form项目中,非常方便地设置个性化的路由。

在Web Form项目中使用路由的时候,在设置个性化的路由到页面的映射过程中,并没有考虑到被映射页面的安全问题,也就是说,如果浏览被映射的页面需要安全验证,那么请读者思考在相关的位置,如何添加相关的代码而实现。