ASP.NET MVC:第 2 部分
继续探讨 ASP.NET MVC 和 URL 路由。
引言
本文是 ASP.NET MVC 第 1 部分 的续篇,我将尝试详细阐述 MVC 的 URL 路由部分。不过,请注意,上一篇文章基于 ASP.NET MVC 的 Preview 2 版本;而本文使用的是 Preview 3 版本。这两个版本之间有一些变化。
必备组件
- ASP.NET 3.5 扩展
- ASP.NET MVC Preview 3;http://www.asp.net/downloads/3.5-extensions/
- MusicCatalog 数据库,包含在下载中
模型-视图-控制器模式回顾
为了回顾本文第一部分的一些内容,我们简要讨论一下 MVC 模式。MVC 是一种将应用程序划分为独立职责区域的模式:模型、视图和控制器。
- 模型:模型负责维护应用程序的状态,通常通过使用数据库。
- 视图:这些组件严格用于显示数据,除了格式化数据以供显示外,不提供任何功能。
- 控制器:控制器是 MVC 应用程序中的中央通信机制。它们将视图的操作传递给模型,然后再传回。
MVC 模式的主要一点是模型和视图之间没有直接通信。本文的目的是详细说明这种通信如何通过 URL 路由进行。
URL 路由
URL 路由是 MVC Web 应用程序框架的一部分。但是,Microsoft 选择将其也作为 .NET Framework 3.5 Service Pack 1 的一部分,尽管其功能可能与 MVC 中的实现有所不同。我们可以看看 URL 路由在 MVC 中是如何实现的,以便了解它如何在非 MVC Web 应用程序中实现。URL 路由组件(恰当地)位于 System.Web.Routing
命名空间中。下图显示了每个命名空间的两个版本。
URL 路由利用下面描述的 HTTPHandlers 和 HTTPModules 来执行所需的操作,我们将看到。
路由 HTTPModule
简要回顾一下,HTTPModules 被插入到 ASP.NET 管道中,作为预处理请求的一种方式。用于身份验证、会话状态等的 HTTPModules 已经内置于 ASP.NET 应用程序中。由于这些模块在请求到达页面级别之前对其进行处理,因此它是 URL 路由的完美解决方案;请求由模块处理,然后适当地路由。将以下条目添加到 web.config 文件中,在 httpModules
元素下,将 UrlRoutingModule
放入 Web 应用程序的管道中。
<add name="UrlRoutingModule"
type="System.Web.Routing.UrlRoutingModule,
System.Web.Routing, Version=3.5.0.0,
Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
提醒一下,Preview 3 使用以下方法来避免与 SP1 冲突
<add name="UrlRoutingModule"
type="System.Web.Routing.UrlRoutingModule,
System.Web.Routing, Version=0.0.0.0, Culture=neutral,
PublicKeyToken=31BF3856AD364E35"/>
该模块在其 Init
方法中注册了两个事件处理程序:OnApplicationPostMapRequestHandler
和 OnApplicationPostMapRequestHandler
。后者当然用于处理 HttpApplication
对象上的 PostMapRequestHandler
事件。此事件在 ASP.NET 引擎将请求映射到事件处理程序后触发。前者用于处理 PostResolveRequestCache
事件,该事件在 ASP.NET 引擎绕过事件处理程序从缓存中提供请求时发生。此事件的处理程序是我们可以看到 URL 路由发生魔力的地方。在这里,创建了一个新的 RequestData
对象并将其添加到 HttpContext
集合中。添加了一个 IRouteHandler
接口的实例,并使用 HttpContext.RewritePath
将请求重定向到 UrlRouting.axd。
RequestData data2 = new RequestData();
data2.OriginalPath = context.Request.Path;
data2.HttpHandler = httpHandler;
context.Items[_requestDataKey] = data2;
context.RewritePath("~/UrlRouting.axd");
路由 HttpHandler
再次简要回顾一下,HttpHandlers 仅用于接收请求并进行处理。在 URL 路由的情况下,UrlRoutingHandler
将用于处理来自我们上面看到的 UrlRouting.axd 的请求。在 UrlRoutingHandler
的 ProcessRequest
方法中,尝试从 IControllerFactory
实例创建正在处理的请求的控制器实例,然后调用 Execute
方法。
string controllerName = RequestContext.RouteData.GetRequiredString("controller");
// Instantiate the controller and call Execute
IControllerFactory factory = ControllerBuilder.GetControllerFactory();
IController controller =
factory.CreateController(RequestContext, controllerName);
...
controller.Execute(controllerContext);
控制器 (Controller)
正如我们在第 1 部分中看到的,控制器就像 ASP.NET MVC Web 应用程序的中间件。它们接收对某个操作的请求,执行任何处理,例如从模型检索数据,然后调用视图。如上所述,找到请求的控制器,然后调用其 Execute
方法。在 Execute
方法中,找到请求的 Action
并尝试调用它。
string actionName = RouteData.GetRequiredString("action");
if (!InvokeAction(actionName))
{
HandleUnknownAction(actionName);
}
InvokeAction
方法使用一些反射来查找操作的方法并传递任何必要的参数。
这是对 URL 路由机制内部工作原理的简要介绍。更详细的介绍可以在这里找到:http://www.cnblogs.com/shanyou/archive/2008/03/22/1117573.html。
定义和创建路由
现在我们已经了解了幕后发生的事情,我们可以专注于创建和定义要在应用程序中使用的路由。路由在 Application_Start
事件中添加到应用程序的 RouteTable
中。
protected void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
}
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute("Default", "{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);
}
这是 ASP.NET MVC 应用程序的默认实现。路由按照它们进入 RouteTable
的顺序进行评估。第一个条目 IgnoreRoute
是 MVC 提供的 RouteCollection
上的扩展方法。
public static void IgnoreRoute(this RouteCollection routes, string url, object constraints)
{
if (routes == null)
{
throw new ArgumentNullException("routes");
}
if (url == null)
{
throw new ArgumentNullException("url");
}
Route route2 = new Route(url, new StopRoutingHandler());
route2.Constraints = new RouteValueDictionary(constraints);
Route item = route2;
routes.Add(item);
}
此方法创建一个新的 Route
,它使用 StopRoutingHandler
停止处理 HTTPHandlers 的请求。这对于在 MVC 中处理请求至关重要,因为正如我们上面看到的,请求被重定向到 UrlRouting.axd。路由使用 MapRoute
添加到集合中,MapRoute
是 MVC 提供的 RouteCollection
的扩展方法,具有三个重载。
public static void MapRoute(this RouteCollection routes, string name, string url)
public static void MapRoute(this RouteCollection routes, string name,
string url, object defaults)
public static void MapRoute(this RouteCollection routes, string name,
string url, object defaults, object constraints)
前两个重载调用最后一个,它创建一个使用 MvcRouteHandler
的 Route
,然后将其添加到集合中。
Route route = new Route(url, new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(defaults),
Constraints = new RouteValueDictionary(constraints)
};
if(String.IsNullOrEmpty(name))
{
// Add unnamed route if no name given
routes.Add(route);
}
else
{
routes.Add(name, route);
}
如果它是一个不使用 MvcRouteHandler
的 Route
,当然可以直接将其添加到集合中。
routes.Add(new Route("Default.aspx",
new{ controller = "Home", action = "Index"}, new MyRouteHandler);
路由实战
通过 URL 路由,您可以接受诸如 http://www.mysite.com/Products/Bikes/Schwinn 之类的请求,并让它显示由 Schwinn 制造的所有自行车。与使用查询字符串参数相比,这会生成一个更简洁的 URL,并且路由详细信息可以对普通用户隐藏。ASP.NET MVC 期望 URL 至少包含两个元素:controller
和 action
。在 URL http://www.mysite.com/Home/Index 中,Home 是应该使用的控制器,Index 是要调用的操作。为此的 Route
将如下所示
Route("{controller/{action}",
new RouteValueDictionary( new { controller = "Home", action = "Index" }),
new MvcRouteHandler());
第一个参数是用于匹配请求的 URL 模式。第二个参数是一个 RouteValueDictionary
,如果请求的 URL 中未找到,则可用于提供默认值。对 http://www.mysite.com 的请求将被重写为 http://www.mysite.com/Home/Index。对 http://www.mysite.com/Home 的请求也将被重写为 http://www.mysite.com/Home/Index。
路由按照它们添加到 RouteCollection
的顺序进行评估。例如,给定按此顺序添加的路由
routes.MapRoute("DefaultRoute", "{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = 0 }
);
routes.MapRoute("NameRoute", "Artist/{name}",
new { controller = "Music", action = "ArtistsByName", name = "AC/DC" }
);
诸如 http://www.mysite.com/Artist/Elvis 之类的 URL 将由第一个路由匹配,尽管它包含单词 Artist。第一个 Route
假定 Artist
应该是 controller
,Elvis
是 action
,并将插入默认 ID 0。为了获得所需的结果,需要反转集合中的路由。给定下面的两个路由,问题在于确定哪个用于诸如 http://www.mysite.com/Artist/1394 和 http://www.mysite.com/Artist/U2 之类的请求。
routes.MapRoute("ArtistByID", "Artist/{id}",
new { controller = "Artist", action = "AlbumsByArtistId", id = 0 }
);
routes.MapRoute("ArtistByName", "Artist/{name}",
new { controller = "Artist", action = "AlbumsByArtistName", name = "" }
);
在前一个 URL 中,显然提供的是一个 ID;然而,在后一个 URL 中,引擎假定 U2 是一个 ID,并将尝试按照 AlbumsByArtistId
操作方法的要求将其转换为整数。如果路由颠倒,1394 将根据 AlbumsByArtistName
的要求转换为字符串。为了解决这个问题,我们可以在创建 Route
时使用 Constraints
参数。此参数是一个 RouteValueDictionary
,用作正则表达式,用于评估请求 URL 中的指定参数。
protected virtual bool ProcessConstraint(HttpContextBase httpContext,
object constraint, string parameterName,
RouteValueDictionary values, RouteDirection routeDirection)
{
object obj2;
IRouteConstraint constraint2 = constraint as IRouteConstraint;
if (constraint2 != null)
{
return constraint2.Match(httpContext, this,
parameterName, values, routeDirection);
}
string str = constraint as string;
if (str == null)
{
throw new InvalidOperationException(
string.Format(CultureInfo.CurrentUICulture,
RoutingResources.Route_ValidationMustBeStringOrCustomConstraint,
new object[] { parameterName, this.Url }));
}
values.TryGetValue(parameterName, out obj2);
string input = Convert.ToString(obj2, CultureInfo.InvariantCulture);
string pattern = "^(" + str + ")$";
return Regex.IsMatch(input, pattern,
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
}
上面的路由可以修改如下
routes.MapRoute("ArtistByID", "Artist/{id}",
new { controller = "Artist", action = "AlbumsByArtistId", id = 0 },
new { id = @"\d{1,}" }
);
routes.MapRoute("ArtistByName", "Artist/{name}",
new { controller = "Artist", action = "AlbumsByArtistName", name = "" },
new { name = @"[a-zA-Z]{1,}" }
);
现在,对 http://www.mysite.com/Artist/1394 的请求将被评估为匹配 ArtistByID Route
,而对 http://www.mysite.com/Artist/U2 的请求将被 ArtistByName Route
匹配,无论它们在 RouteTable
中出现的顺序如何。
未完待续...
ASP.NET MVC 是一项非常丰富的技术,无法在一篇文章中涵盖。希望本文已经阐明了基本概念,并可用于评估该技术的潜力。本系列的未来文章将涵盖更多方面,例如单元测试和表单。