对于ASP.NET WebForm来说,每一个请求都对应一个物理文件。而在ASP.NET MVC中,请求路径和物理文件是分离的,也就是说我们请求的路径并不一定是真实存在的。ASP.NET路由模块负责将即将到来的浏览器请求映射到特定的MVC控制器动作。学完这篇教程之后,你将会理解标准的路由表是如何将请求映射到控制器动作的。
默认路由表
当你创建一个新的ASP.NET MVC应用程序时,应用程序已经被配置为使用ASP.NET路由。ASP.NET路由在两个地方设置。
第一点,在你的应用程序Web配置文件(Web.config文件)中启用ASP.NET路由。在配置文件中有四个节点与路由有关:sytem.web.httpModules节,system.web.httpHandlers节,system.webserver.modules节,以及system.webserver.handlers节。特别要小心不要删除了这些节点,因为没有它们路由将不能工作。
第二点,在应用程序的Global.asax文件中创建了一个路由表。Global.asax文件是一个特殊的文件,它包含了作用于ASP.NET应用程序生命周期事件的事件处理程序。路由表在Application Start事件期间创建。
创建默认路由表的代码如下:
代码清单1:默认的路由
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
当一个MVC应用程序首次运行时,会调用Application_Start()方法。这个方法随后调用了RegisterRoutes()方法。RegisterRoutes()方法创建了路由表。
默认的路由表包含了一个路由(名叫Default)。Default路由将URL的第一部分映射到控制器名,URL的第二部分映射到控制器动作,第三个部分映射到一个叫做id的参数。
假设你在浏览器的地址栏输入了下面的URL:
/Home/Index/3
默认的路由将这个URL映射为下面的参数:
Controller = Home
Action = Index
id = 3
当你请求URL /Home/Index/3 时,将会执行下面的代码:
HomeController.Index(3)
Default路由包含了所有三个参数的默认值。如果你不提供控制器,那么控制器参数默认值为Home。如果你不提供动作,动作参数默认为值Index。最后,如果你不提供id,id参数默认为空字符串。
创建自定义路由
对于简单的ASP.NET MVC应用程序,默认的路由表已经可以很好的完成工作了。然而,你可能发现会存在特定的路由需求。在这种情况下,你可以创建一个自定义路由。
设想一下,举个例子,你正在创建一个博客应用程序。你可能想要像这样处理即将到来的请求:
/Archive/12-25-2009
当用户输入这一请求,你想要返回对应于日期12/25/2009的博客条目。为了处理这种类型的请求,你需要创建一个自定义路由。
代码清单1中的Global.asax包含了一个新的自定义路由,命名为了Blog,它处理了类似/Archive/条目日期 这样的请求。
代码清单2:添加自定义路由
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Blog", // Route name
"Archive/{entryDate}", // URL with parameters
new { controller = "Archive", action = "Entry" } // Parameter defaults
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
添加到路由表中的路由顺序非常重要。我们的新自定义Blog路由在现有的Default路由前面。如果你将这个顺序颠倒过来,那么Default路由将总是被调用,而不是自定义路由。
自定义Blog路由匹配任何以/Archive/作为开始的请求。因此,它匹配所有下面的URL:
/Archive/12-25-2009
/Archive/10-6-2004
/Archive/apple
自定义路由将即将到来的请求映射到名为Archive的控制器,并且调用了Entry()动作。当调用Entry()方法时,条目日期作为entryDate参数进行了传递。
代码清单3:ArchiveController
using System;
using System.Web.Mvc;
namespace MvcApplication1.Controllers
{
public class ArchiveController : Controller
{
public string Entry(DateTime entryDate)
{
return "You requested the entry from " + entryDate.ToString();
}
}
}
注意到代码清单2中的Entry()方法接受一个DateTime类型的参数。MVC框架非常的聪明,足以自动地将URL中的条目日期转换为DateTime值。如果URL中的条目日期参数不能转换为DateTime,将会引发错误
创建路由约束
你可以使用路由约束来限制匹配特定路由的浏览器请求。可以使用正则表达式来指定一个路由约束。
举个例子,假设你已经在Global.asax文件中定义了一个路由。
代码清单4:Product Routing
routes.MapRoute(
"Product",
"Product/{productId}",
new {controller="Product", action="Details"}
);
代码清单5:ProductController
using System.Web.Mvc;
namespace MvcApplication1.Controllers
{
public class ProductController : Controller
{
public ActionResult Details(int productId)
{
return View();
}
}
}
定义在代码清单4中的路由将会匹配下面的任意URL:
/Product/23
/Product/7
不幸的是,路由也会匹配下面的URL:
/Product/blah
/Product/apple
因为Details()动作期望的是一个整数值,发起一个含有非整数值的请求将会导致错误。举个例子,如果你在浏览器中输入/Product/apple URL,那么你将会得到错误页。
你实际想做的是只匹配包含合适整数productId的URL。当定义路由来限制与路由相匹配的URL时,你可以使用约束。代码清单6中的修改后的Product路由包含了一个正则表达式,它限制了只匹配数字。
代码清单6:修改后的Product Routing
routes.MapRoute(
"Product",
"Product/{productId}",
new {controller="Product", action="Details"},
new {productId = @"d+" }
);
正则表达式d+匹配一个或多个整数。这个限制使得Product路由匹配了下面的URL:
/Product/3
/Product/8999
但是不匹配下面的URL:
/Product/apple
/Product
这些浏览器请求将由另外的路由处理,或者,如果没有匹配的路由,将会返回一个“The resource could not be found”错误。
创建一个自定义路由约束
在这部分内容中,我们创建了一个Localhost路由约束。Localhost路由约束只匹配本地计算机发出的请求。通过互联网发出的远程请求不会被匹配。
你可以通过实现IRouteConstraint接口来实现一个自定义路由。这是一个极其简单的接口,它只描述了一个方法:
代码清单7:Match方法定义
bool Match(
HttpContextBase httpContext,
Route route,
string parameterName,
RouteValueDictionary values,
RouteDirection routeDirection
)
我们定义一个Localhost约束,实现IRouteConstraint接口,其代码如下:
代码清单8:Localhost约束
using System.Web;
using System.Web.Routing;
namespace MvcApplication1.Constraints
{
public class LocalhostConstraint : IRouteConstraint
{
public bool Match
(
HttpContextBase httpContext,
Route route,
string parameterName,
RouteValueDictionary values,
RouteDirection routeDirection
)
{
return httpContext.Request.IsLocal;
}
}
}
代码清单8中的约束利用了HttpRequest类公布的IsLocal属性。当发出请求的IP地址是127.0.0.1或者与服务器的IP地址相同时,这个属性返回true。
你在定义于Global.asax的路由中使用了自定义约束。代码清单9中的RegisterRoutes方法使用了Localhost约束来阻止任何人请求Admin页面,除非他们从本地服务器发出请求。举个例子,当请求来自远程服务器时,对于/Admin/DeleteAll的请求将会失败。
代码清单9:使用了Localhost约束的Routing
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Admin",
"Admin/{action}",
new { controller = "Admin" },
new { isLocal = new LocalhostConstraint() }
);
}
Localhost约束使用在了Admin路由的定义中。这个路由不会被远程浏览器请求所匹配。然而,应该意识到定义在RegisterRoutes方法中的其他路由可能会匹配相同的请求。理解这一点很重要:约束阻止了特定路由匹配某一请求,而不是所有定义在RegisterRoutes方法中的路由。
注意到Default路由在代码清单9中的RegisterRoutes方法中被删掉了。如果你包含Default路由,那么Default路由将会匹配对Admin控制器的请求。在这种情况下,远程用户仍然可以调用Admin控制器的动作,即使他们的请求不匹配Admin路由。