定制路由系统
路由系统是灵活可配置的,当然还可以通过下面这两种方式定制路由系统,来满足其他需求。
1、 通过创建自定义的RouteBase实现;
2、 通过创建自定义路由处理程序实现。
创建自定义的RouteBase实现
创建自定义的RouteBase实现,需要实现一个RouteBase的派生类,而这需要实现以下两个方法:
- GetRouteData(HttpContextBase httpContext):这是入站URL进行匹配的工作机制。框架依次对RouteTable.Routes的每个条目调用这个方法,直到其中之一返回一个非空值。
- GetVirtualPath(RequestContext requestContext,RouteValueDictionary values):这是出站URL生成的工作机制。框架依次对RouteTable.Routes的每一个条目调用这个方法,直到其中之一返回一个非空值。
为了演示这种自定义方式,这里创建了一个RouteBase的派生类。我们假设这样的一个需求环境:需要把一个现有的应用程序迁移到MVC框架,但不论出于什么原因,我们需要兼容之前的URL,那就可以通过这种方式来实现,当然可以通过规则的路由系统来处理——这里不对这种方式进行讨论。
首先,创建一个处理旧式路由请求的控制器,将其命名为:LegacyController,如:
using System.Web.Mvc; namespace UrlsAndRoutes.Controllers { /// <summary> /// 用以处理旧式 URL 请求的控制器 /// </summary> public class LegacyController : Controller { public ActionResult GetLegacyURL(string legacyURL) { // 应用程序迁移到 MVC 之前,请求是针对文件的,因此,实际上是需要在这里处理被请求的文件。但这里 // 只简单说明一下自定义 RouteBase 的实现原理,所以,此处仅在视图中显示这个 URL。 return View((object)legacyURL); } } }
上面代码对View方法中的参数做了转换,如果不转换,则C#编译器会误认为要将参数作为要指定渲染的视图的名称的字符串(View方法的一个重载版本的实现)。下面是这个动作方法的视图GetLegacyURL.cshtml:
@model string @{ ViewBag.Title = "GetLegacyURL"; Layout = null; } <h2>GetLegacyURL</h2> The URL requested was:@Model
1、对输入URL进行路由
在Infrastructure文件夹中创建一个LegacyRoute类,其内容如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; namespace UrlsAndRoutes.Infrastructure { public class LegacyRoute : RouteBase { private string[] urls; public LegacyRoute(params string[] targetUrls) { } public override RouteData GetRouteData(HttpContextBase httpContext) { RouteData result = null; string requestedURL = httpContext.Request.AppRelativeCurrentExecutionFilePath; if (urls.Contains(requestedURL, StringComparer.OrdinalIgnoreCase)) { result = new RouteData(this, new MvcRouteHandler()); result.Values.Add("controller", "Legacy"); result.Values.Add("action", "GetLegacyURL"); result.Values.Add("legacyURL", "requestedURL"); } return result; } public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) { return null; } } }
注册一条路由,以使其使用新建的这个RouteBase派生类:
public static void RegisterRoutes(RouteCollection routes) { // 注册自定义的 RouteBase 实现 routes.Add(new LegacyRoute("~/articles/Windows_3.1_Overview.html", "~/old/.NET_1.0_Class_Library")); }
2、生成输出URL
在LegacyRoute中实现GetVirtualPath方法以使其能够支持输出URL的生成。如:
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) { VirtualPathData result = null; if (values.ContainsKey("legactURL") && urls.Contains((string)values["legacyURL"], StringComparer.OrdinalIgnoreCase)) { // 如果存在一个匹配,将会创建一个 VirtualPathData 对象,在其中传递一个对当前对象的引用和出站 URL。由于路由系统已经预先将 // 字符“/”附加到了这个URL,因此,必须从生成的 URL 上删除这个前导字符。 result = new VirtualPathData(this, new UrlHelper(requestContext).Content((string)values["legacyURL"]).Substring(1)); } return null; }
在ActionName.cshtml视图中添加下面这段代码,以使其能礼仪自定义路由生成输出URL:
<div> @* 经由自定义路由生成一个输出 URL *@ This is a URL: @Html.ActionLink("Click me", "GetLegacyURL", new { legacyURL = "~/articles/Windows_3.1_Overview.html" }) </div>
上面代码将产生一个这样的a元素:
<a href=”/articles/Windows_3.1_Overview.html”>Click me</a>
用legacyURL属性创建的匿名类型被转换到了含有同名键的RouteValueDictionary类中。
创建自定义路由处理程序
路由已经依赖这个MvcRouteHandler了,因为MvcRouteHandler把路由系统连接到了MVC框架。但通过实现IRouteHandler接口,路由系统仍允许自定义自己的路由处理程序,如下面的示例:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Routing; namespace UrlsAndRoutes.Infrastructure { public class CustomRouteHandler : IRouteHandler { public IHttpHandler GetHttpHandler(RequestContext requestContext) { return new CustomHttpHandler(); } } public class CustomHttpHandler : IHttpHandler { public bool IsReusable { get { return false; } } public void ProcessRequest(HttpContext context) { context.Response.Write("Hello"); } } }
IRouteHandler接口的目的是提供生成IHttpHandler接口的实现,且由它负责对请求进行处理。在该接口的MVC实现中,主要负责这几项工作:查找控制器、调用动作方法、渲染视图,并将结果写入到响应中。当然,这里的实现要简单的多,此处仅将单词“Hello”写到客户端,且只是文本形式。要想得到最终效果,需要在RouteConfig.cs文件中注册这个自定义处理程序:
public static void RegisterRoutes(RouteCollection routes) { // 注册自定义路由处理程序 routes.Add(new Route("SayHello", new CustomRouteHandler())); }
使用区域
MVC框架支持将Web应用程序组织成一些区域(Area),每个区域代表应用程序的一个功能端,如管理、结算、客户支持等等。这使得代码的管理很有用,尤其是大型项目,如果对所有控制器、视图和模型只使用一组文件夹,那将会是很难于管理的。
创建区域
可以直接对项目右键,选择“添加”->“区域”进行添加。还可以在当前的区域中创建其他区域。在刚刚的操作之后,项目中将会出现如下这样的区域文件夹结构:
通过Areas/Admin文件夹,可以看出这是一个小型的MVC项目。其中有“Controllers”、“Models”和“Views”的文件夹。前两个是空的,但“Views”文件夹含有一个“Shared”文件夹和一个Web.config视图引擎配置文件(这里暂不对视图引擎进行讨论)。
另外,这里还多了一个AdminAreaRegistration.cs文件,如:
using System.Web.Mvc; namespace UrlsAndRoutes.Areas.Admin { public class AdminAreaRegistration : AreaRegistration { public override string AreaName { get { return "Admin"; } } public override void RegisterArea(AreaRegistrationContext context) { context.MapRoute( "Admin_default", "Admin/{controller}/{action}/{id}", new { action = "Index", id = UrlParameter.Optional } ); } } }
从清单中可以看出,该类中的RegisterArea方法注册了一个URL模式为Admin/{controller}/{action}/{id}的路由。当然,也可以在该方法中定义该区域专用的其他路由。
注意:如果要给路由赋名,必须确保这些名称在整个应用程序而不仅仅是某一区域中是唯一的。
由于在Global.asax的Application_Start方法中已经对路由的注册进行过处理,因此,不需要在开发的过程中采取其他措施来确保该注册方法会被调用:
public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); } }
上面代码中对静态方法AreaRegistration.RegisterAllAreas的调用,会导致MVC框架对应用程序的所有类进行遍历,找出派生于AreaRegistration的所有类,并调用这些类上的RegisterArea方法。
注意:不用修改Application_Start方法中与路由相关的语句顺序。如果在AreaRegistration.RegisterAllAreas之前调用RegisterRoutes,那么会在区域路由之前定义路由。由于路由系统是按顺序评估的,这意味着对区域控制器的请求有可能会用不正确的路由进行匹配。
注:AreaRegistrationContext类中的MapRoute方法会自动把注册的路由限制到包含该区域控制器的命名空间。也就是说,当某区域创建控制器时,必须把它放在其默认的命名空间中;否则,路由系统将无法找到它。
填充区域
在上一节“创建区域”一节中,已经知道在一个区域中可以创建控制器、视图以及模型等。下面,通过创建一个名为HomeController的控制器类,来演示应用程序中区域之间的分离:
在下图中的Controllers文件夹中右键添加一个空的控制器:HomeController
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace UrlsAndRoutes.Areas.Admin.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); } } }
为了演示,对该控制器中Index动作方法右击,并添加相应的视图,添加后的视图将在:Areas/Admin/View/Home路径中。
视图内容如下:
@{ ViewBag.Title = "Index"; Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-with" /> <title>Index</title> </head> <body> <div> <h2>Admin Area Index</h2> </div> </body> </html>
从上面介绍可以看出,在一个区域内的工作方式与在一个MVC项目主区中工作是相当类似的。在项目中创建某个项的工作流也是相同的。其效果如下(导航路径:Admin/Home/Index):
解析不明确的控制器问题
区域可能不像它们所展示的那样是自包含的。当一个区域被注册时,所定义的任何路由都被限制到与这个区域关联的命名空间之中。这也是能够请求/Admin/Home/Index,并得到WorkingWithAreas.Admin.Controllers命名空间中HomeController类的原因。
然而,在RouteConfig.cs的RegisterRoutes方法中定义的路由却不受类似的限制。作为提醒,这里给出了示例应用程序此时的路由配置:
public static void RegisterRoutes(RouteCollection routes) { routes.Add(new Route("SayHello", new CustomRouteHandler())); routes.Add(new LegacyRoute("~/articles/Windows_3.1_Overview.html", "~/old/.NET_1.0_Class_Library")); routes.MapRoute("MyRoute", "{controller}/{action}"); routes.MapRoute("MyOtherRoute", "App/{action}", new { controller = "Home" }); }
名为“MyRoute”的路由把来自浏览器的输入URL转换为Home控制器上的Index动作。这时会收到一个错误的消息,因为没有为这条路由设置命名空间的约束,所以MVC框架会看到两个HomeController类。为了解决这一问题,需要在所有可能导致冲突的路由中,将主控制器命名空间列为优先,如:
public static void RegisterRoutes(RouteCollection routes) { routes.Add(new Route("SayHello", new CustomRouteHandler())); routes.Add(new LegacyRoute("~/articles/Windows_3.1_Overview.html", "~/old/.NET_1.0_Class_Library")); routes.MapRoute("MyRoute", "{controller}/{action}",null,new[] {“UrlsAndRoutes.Controllers”}); routes.MapRoute("MyOtherRoute", "App/{action}", new { controller = "Home" }, new[] {“UrlsAndRoutes.Controllers”}); }
上面代码中加粗部分将项目控制器作为了优先。当然也可以对某个区域中的控制器实现优先。
生成对区域动作的链接
对与同一区域中的动作,无需采取特殊的步骤来创建指向这些动作的链接。MVC框架会检测当前请求涉及的特定区域,然后出站URL生成将只在该区域定义的路由中查找一个匹配。如将下面代码添加到Admin区域的视图。
@Html.ActionLink("Click me", "About")
会生成以下HTML:
<a href=”/Admin/Home/About”>Click me</a>
为了对不同区域中的动作或根本无区域的动作创建一条链接,必须创建一个名为“area”的变量,并用它指定区域名,如:
@Html.ActionLink("Click me to go to another area", "Index", new { area = "Support" })
因此,area被保留为片段变量名。假设创建了名为Support的区域,并有对应的标准路由定义,则将生成如HTML:
<a href=”/Support/Home”>Click me to go to another area</a>
如果想链接到顶级控制器(/Controllers文件夹中的一个控制器)上的一个动作,那么应该把area指定为空字符串,如:
@Html.ActionLink("Click me to go to another area", "Index", new {area = ""})
URL方案最佳做法
1、 使URL整洁和人性化
下面摘抄一些生成友好URL的简单的纲要:
- 设计URL来描述它们的内容,而不是应用程序的实现细节。使用/Articles/AnnualReport,而不是使用/Website_v2/CachedContentServer/FromCache/AnnualReport。
- 尽可能采用内容标题而不是ID号,使用/Articles/AnnualReport,而不是/Articles/2392。如果必须使用一个ID号(以区别具有同样标题的条目或避免通过标题查找一个条目时,需要多余的数据库查询步骤),那么两者都有(如:/Articles/2392/AnnualReport)。这需要多打一些字符,但更要意义,并会改善搜索引擎排列。
- 不用对HTML页面使用文件扩展名(如,.aspx或.mvc),但对特殊文件类型要用扩展名(如,.jpg、.pdf、.zip等)。如果是适当的设置了MIME类型,Web浏览器不会在意文件的扩展名,但人们却希望对PDF文件用.pdf扩展名。
- 创建一种层次感(如:/Products/Menswear/Shirts/Red),这样,容易让人猜出父目录的URL。
- 不区分大小写。ASP.NET路由系统默认是不区分大小写的。
- 避免使用符合、代码和字符序列。需要用单词分隔符时,可以使用短横(如:/my-great-article)。下划线是不友好的,而URL编码的空格是奇特的(/my+great+article)或令人讨厌的(/my%20great%20article)。
- 不用修改URL。打破链接等于失去商务。当确实需要修改URL时,通过永久重定向(301)尽可能长时间的继续支持旧式的URL方案。
- 具有一致性。在整个应用程序中采用一种URL格式。URL应简短、易于输入、可剪辑(人性化可剪辑),且持久稳定,而且它们应该形象化网站结构。
2、GET和POST:选用正确的一个
一般来说,GET请求应该被用于所有只读信息检索,而POST请求应该被用于各种修改应用程序状态的操作。用标准的术语说,GET请求用于安全交互(除信息检索外无其他影响),而POST请求用于不安全交互(作出决定或修改某些东西)。GET请求是可设定地址的——所有信息都包含在URL中,因此它可以设为书签并链接到这些地址。(这些约定是由全球互联网联盟(W3C)在http://www.w3.org/Products/rfc2616/rfc2616-sec9.html上设定的)