丹尼大叔

数学专业毕业,爱上编程的大叔,兴趣广泛。使用博客园这个平台分享我工作和业余的学习内容,以编程交友。有朋自远方来,不亦乐乎。

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

 摘要:

在MVC框架之前,ASP.NET假定在请求的URLs和服务器硬盘文件之间有直接的关系。服务器的职责是接收浏览器请求,从相应的文件发送输出。

这种方法只能工作于Web表单,每一个ASPX页面既是一个文件,也是一个对应请求的自包含响应。而这对于MVC应用程序来说就无效了,因为请求是由控制器类里的行为方法处理的,而且没有磁盘上一对一关系的文件。

ASP.NET平台使用路由系统处理MVC URLs。在这篇文章中,我将向你展示怎样为你的工程使用路由系统来创建和处理强大灵活的URL。你将看到,路由系统让你创建你需要的任何形式的URLs,并且用清楚和简明的方式进行表达。路由系统有两个功能:

检查进来的URL,计算出它的意向是哪个控制器和行为方法。

生成外部URLs。这些URLs显示在视图的HTML,以至当用户点击超链接时,触发一个具体的行为方法(这时候,它又变成了进来的URL)。

在这章,我将介绍路由的定义,以及使用他们处理请求的URL,以至于用户可以调用你的控制器和行为方法。在MVC框架里,有两种方式创建路由。基于传统的路由和特性路由。如果你用过MVC框架早期的版本,你可能对传统路由比较熟悉。特性路由是MVC5新加的功能。这篇文章将分别介绍传统路由,下篇文章介绍特性路由。

以后文章我还将介绍如何使用那些相同的路由生成你需要包含在你的视图里的对外的URLs。以及如何使用一个叫areas的功能实现订制路由系统。

准备示例工程

HomeController:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 
 7 namespace UrlsAndRoutes.Controllers
 8 {
 9     public class HomeController : Controller
10     {
11         public ActionResult Index()
12         {
13             ViewBag.Controller = "Home";
14             ViewBag.Action = "Index";
15             return View("ActionName");
16         }
17     }
18 }

CustomerController:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 
 7 namespace UrlsAndRoutes.Controllers
 8 {
 9     public class CustomerController : Controller
10     {
11         public ActionResult Index()
12         {
13             ViewBag.Controller = "Customer";
14             ViewBag.Action = "Index";
15             return View("ActionName");
16         }
17 
18         public ActionResult List()
19         {
20             ViewBag.Controller = "Customer";
21             ViewBag.Action = "List";
22             return View("ActionName");
23         }
24     }
25 }

AdminController:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 
 7 namespace UrlsAndRoutes.Controllers
 8 {
 9     public class AdminController : Controller
10     {
11         public ActionResult Index()
12         {
13             ViewBag.Controller = "Admin";
14             ViewBag.Action = "Index";
15             return View("ActionName");
16         }
17     }
18 }

在Views的子文件夹Shared内,创建视图文件ActionName.cshtml。

 1 @{
 2     Layout = null;
 3 }
 4 <!DOCTYPE html>
 5 <html>
 6 <head>
 7     <meta name="viewport" content="width=device-width" />
 8     <title>ActionName</title>
 9 </head>
10 <body>
11     <div>The controller is: @ViewBag.Controller</div>
12     <div>The action is: @ViewBag.Action</div>
13 </body>
14 </html>

在工程属性里,选择Web选项。选择Specific Page。

运行程序,得到运行结果:

介绍URL模式

路由系统使用一个路由集合展示他的魔力。这些路由共同地组成了URL模式,或者一个系统的模式(你的应用程序能够认识和响应的URLs的集合)。

我不需要手工地书写在我的应用程序里将要支持的所有的URLs。代替地,每一个路由包含一个URL模式,它跟进入的URLs进行比较。如果URL符合这个模式,它将被路由系统处理这个URL。先来一个示例应用程序的一个URL:

http://mysite.com/Admin/Index

 

URLs能够分割成一些节。它们是URL的部分,除了hostname和查询参数query string以外,它们都是以/字符隔开的。在这个例子中,有两个节:

http://mysite.com/Admin/Index

                            first     second

第一个节包含单词Admin,第二个节包含单词Index。在人眼中,第一个节明细关联于控制器,第二个节关联行为方法。但是,当然地,我需要用一种方法表达能让路由系统理解的这种关系。下面是这样的一个URL模式:

{controller}/{action}

当处理一个进来的请求时,路由系统的工作是将请求的URL匹配到一个模式,并从URL中抽出节中定义在模式种的变量。这些节的变量使用括号{}字符来表达。这个例子有两个节变量,名字分别是controller和action。所以controller节变量的值是Admin,action节变量的值是Index。

我说匹配一个模式,因为MVC系统经常有许多路由,路由系统将比较进入的URL和每一个路由的URL模式,直到找到一个匹配的。

默认地,一个URL模式将匹配相同数量节任何的URL。例如,模式{controller}/{action}将匹配任何有两个节的URL。

请求的URL 节变量
http://mysite.com/Admin/Index controller = Admin action = Index
http://mysite.com/Index/Admin controller = Index action = Admin
http://mysite.com/Apples/Oranges controller = Apples action = Oranges
http://mysite.com/Admin No match—too few segments
http://mysite.com/Admin/Index/Soccer No match—too many segments


URL模式的两个关键行为:

URL模式是保守的,它只匹配相同节数量的模式URL。表中的第四和第五个例子。
URL模式是开放的。如果一个URL确实含有相同数量的节,模式将抽出节变量的值,而不管它可能是什么。

这些事默认行为,也是理解URL模式怎样工作的关键。后面将介绍怎样改变这个默认行为。

就像已经提到的,路由系统不知道关于MVC系统的任何信息。当从URL抽出的变量,不存在对应的controller或action的时候,URL模式仍会进行匹配。表中的第二个例子就是。我转置了在URL中的Admin和Index节,从URL抽取的值也跟着转置了,即使例子工程中没有Index这个控制器。

创建和注册一个简单的路由

一旦你理解了URL模式,你就能够使用它来定义路由。路由定义在App_Start工程文件夹里的RoutConfig.cs代码文件里。你将看到Visual Studio定义的这个文件的初始内容:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 using System.Web.Routing;
 7 
 8 namespace UrlsAndRoutes
 9 {
10     public class RouteConfig
11     {
12         public static void RegisterRoutes(RouteCollection routes)
13         {
14             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
15 
16             routes.MapRoute(
17                 name: "Default",
18                 url: "{controller}/{action}/{id}",
19                 defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
20             );
21         }
22     }
23 }

定义在RoutConfig.cs文件里的静态RegisterRouters方法,在Global.asax.cs文件中调用。Global.asax.cs文件在应用程序启动的时候设置一些MVC核心功能。你将看到Global.asax.cs文件的默认内容。我加粗了Application_Start方法内对RouteConfig.RegisterRoutes方法的调用。

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 using System.Web.Routing;
 7 
 8 namespace UrlsAndRoutes
 9 {
10     public class MvcApplication : System.Web.HttpApplication
11     {
12         protected void Application_Start()
13         {
14             AreaRegistration.RegisterAllAreas();
15             RouteConfig.RegisterRoutes(RouteTable.Routes);
16         }
17     }
18 }

Application_Start方法在MVC应用程序首次启动的时候被后台的ASP.NET平台调用。使得RouteConfig.RegisterRoutes方法被调用。方法的参数是静态属性RouteTable.Routes的值,它是一个RouteCollection类型的实例。

下面代码创建自己的URL模式。

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 using System.Web.Routing;
 7 
 8 namespace UrlsAndRoutes
 9 {
10     public class RouteConfig
11     {
12         public static void RegisterRoutes(RouteCollection routes)
13         {
14             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
15 
16             Route myRoute = new Route("{controller}/{action}", new MvcRouteHandler());
17             routes.Add("MyRoute", myRoute);18         }
19     }
20 }

我使用构造函数字符串参数和一个MvcRouteHandler实例参数创建一个新的路由。不同的ASP.NET技术提供不同的类来应对路由行为。MvcRouteHandler是ASP.NET MVC应用程序默认使用的。创建路由后,我使用Add方法将他添加到RouteCollection对象里。

一个更方便的注册路由的方式是使用定义在RouteCollection里的MapRoute方法。下面的代码跟上面的作用一样。

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 using System.Web.Routing;
 7 
 8 namespace UrlsAndRoutes
 9 {
10     public class RouteConfig
11     {
12         public static void RegisterRoutes(RouteCollection routes)
13         {
14             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
15 
16             routes.MapRoute("MyRoute", "{controller}/{action}");
17         }
18     }
19 }

这个方法更紧凑,主要是因为我不需要创建MvcRouteHandler类的实例了(它是在后台默认创建的)。MapRoute方法只适用于MVC应用程序。ASP.NET Web表单程序使用RouteCollection类中定义的MapPageRoute方法。

使用这个简单路由

启动这个示例应用程序,你可以看到我修改路由后的效果。如果你导航到应用程序的根路径下,你将看到一个显示错误的页面。但是如果你导航到符合{controller}/{action}模式的路由,你将看到下面的结果,说明导航到/Admin/Index的效果。

定义默认值

当我为应用程序请求默认的URL得到一个错误页面的原因,是它不匹配我定义的路由。默认的URL表达式在路由系统里是~/,这个字符串里没有匹配控制器和行为方法的节。

我解释过URL模式是保守的,它们将匹配数量相等节的URLs。我还说了这是默认行为,改变这种行为的方法是使用默认值。当URL不包含匹配值的节时,默认值就被使用。下面代码提供了默认值的例子:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 using System.Web.Routing;
 7 
 8 namespace UrlsAndRoutes
 9 {
10     public class RouteConfig
11     {
12         public static void RegisterRoutes(RouteCollection routes)
13         {
14             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
15 
16             routes.MapRoute("MyRoute", "{controller}/{action}", new { action = "Index" });
17         }
18     }
19 }

默认值以匿名类的属性提供。上面的例子里,我给action变量提供了默认值Index。这个路由将匹配之前的所有的两个节的URLs。例如,URL:http://mydomain.com/Home/Index被请求时,路由将抽取Home作为controller变量值,Index作为action变量值。

既然我给action节提供了默认值,路由系统也将匹配单个节的URLs。当处理一个单个节的URL时,路由系统将从单个的URL节中抽取controller值,另外使用默认值作为action变量的值。在这种方式下,我能够请求URL:http://mydomain.com/Home,触发Home控制器里的Index行为方法。

我也能够走得更远,定义一个不包含任何节变量的URLs,只依赖默认值来识别action和controller。下面的代码展示了怎样提供两个节的默认值,来映射到应用程序的根URL。

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 using System.Web.Routing;
 7 
 8 namespace UrlsAndRoutes
 9 {
10     public class RouteConfig
11     {
12         public static void RegisterRoutes(RouteCollection routes)
13         {
14             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
15 
16             routes.MapRoute("MyRoute", "{controller}/{action}", new { controller = "Home", action = "Index" });
17         }
18     }
19 }

通过给controller和action变量提供默认,我创建的路由将匹配下面的URLs。

Number of Segments Example Maps To
0 mydomain.com controller = Home action = Index
1 mydomain.com/Customer  controller Customer action Index
2 mydomain.com/Customer/List controller Customer action List
3 mydomain.com/Customer/List/All No match—too many segments


接收的进来的URL越少的节,将依赖更多的默认值。直到我收到一个没有节的URL,所有的默认值被使用。启动应用程序,导航到根URL,看到运行结果:

使用静态URL节

不是URL模式中的所有节都必须是变量。你可以创建含有静态节的模式。假如我想匹配一个以Public为前缀的URL:

http://mydomain.com/Public/Home/Index

 

我可以使用下面的代码: 

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 using System.Web.Routing;
 7 
 8 namespace UrlsAndRoutes
 9 {
10     public class RouteConfig
11     {
12         public static void RegisterRoutes(RouteCollection routes)
13         {
14             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
15 
16             routes.MapRoute("MyRoute", "{controller}/{action}", new { controller = "Home", action = "Index" });
17 
18             routes.MapRoute("", "Public/{controller}/{action}", new { controller = "Home", action = "Index" });
19         }
20     }
21 }

新的模式将只匹配含有三个节的URLs,第一个节必须是Public。另两个节可以包含任何值,将用于controller和action变量。如果最后两个节都省略了,将使用默认值。

我也可以创建既含有静态部分,又含有变量的节,例如下面的代码:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 using System.Web.Routing;
 7 
 8 namespace UrlsAndRoutes
 9 {
10     public class RouteConfig
11     {
12         public static void RegisterRoutes(RouteCollection routes)
13         {
14             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
15 
16             routes.MapRoute("", "X{controller}/{action}");
17 
18             routes.MapRoute("MyRoute", "{controller}/{action}", new { controller = "Home", action = "Index" });
19 
20             routes.MapRoute("", "Public/{controller}/{action}", new { controller = "Home", action = "Index" });
21         }
22     }
23 }

这个路由模式匹配任何第一个节以字母X开头的两个节的URL。controller的值从第一个节获取。action的值从第二个节获取。你可以启动应用程序,导航到/XHome/Index看运行效果。

路由顺序

上面的程序,我定义了一个新的路由,并把它放在RegisterRoutes方法中其他所有路由的前面。我这样做,是因为路由是按照它们在RouteCollection对象中出现的顺序应用的。MapRoute方法在collection末尾添加一个路由,这意味着路由应用的顺序大体上是按照它们定义的顺序的。我说“大体上”,是因为还有方法在具体的位置插入路由。我倾向于不使用这些方法,因为按照定义的顺序应用路由,能够更简单地理解应用程序的路由。

路由系统尝试跟第一个定义的路由URL模式进行匹配,如果匹配不成功,则继续匹配下一个路由模式。路由按顺序进行匹配直到找到一个匹配,或者路由集合都找完了。这就导致了最具体的路由必须定义在第一个位置上。上面代码添加的路由比后面的路由更具体。如果我改变了路由的顺序:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 using System.Web.Routing;
 7 
 8 namespace UrlsAndRoutes
 9 {
10     public class RouteConfig
11     {
12         public static void RegisterRoutes(RouteCollection routes)
13         {
14             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
15 
16             routes.MapRoute("MyRoute", "{controller}/{action}", new { controller = "Home", action = "Index" });
17 
18             routes.MapRoute("", "X{controller}/{action}");
19 
20             routes.MapRoute("", "Public/{controller}/{action}", new { controller = "Home", action = "Index" });
21         }
22     }
23 }

第一个路由,它可以匹配零个节、一个节和两个节的任何URL,将被第一个使用。而更具体的路由,现在是列表中的第二个,匹配将永远都到达不了。所以下面的URL

http://www.mydomain.com/XHome/Index

将匹配到名字是XHome的controller,而XHome不存在,将导致一个发送给用户的404-Not Found错误。

我可以组合静态URL节和默认值来创建一个具体URL的别名。如果你已经对外发布了你的URL模式,而且和你的用户已经有个契约,这将非常有用。如果你在这种情况下重构了应用程序,你需要保留之前的URL格式,让任何用户创建的URL都还继续能用。假设我之前创建了一个名叫Shop的控制器,这个控制器现在改名为Home控制器,下面的代码展示了怎样创建一个保留旧的URL格式的路由:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 using System.Web.Routing;
 7 
 8 namespace UrlsAndRoutes
 9 {
10     public class RouteConfig
11     {
12         public static void RegisterRoutes(RouteCollection routes)
13         {
14             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
15 
16             routes.MapRoute("ShopSchema", "Shop/{action}", new { controller = "Home" });
17 
18             routes.MapRoute("", "X{controller}/{action}");
19 
20             routes.MapRoute("MyRoute", "{controller}/{action}", new { controller = "Home", action = "Index" });
21 
22             routes.MapRoute("", "Public/{controller}/{action}", new { controller = "Home", action = "Index" });
23         }
24     }
25 }

这个我添加的路由匹配任何第一个节是Shop的两个节的URL。action值从第二个URL节获取。URL模式不包含controller的变量节,所以将使用我提供的默认值。这就意味着在Shop控制器上的action请求被转成到Home控制器。你可以启动应用程序,导航到/Shop/Index看运行效果。新的路由导致MVC框架目标到Home控制器的Index行为方法。

我可以更进一步,创建已经重构了,并且不存在在控制器里的action方法的别名。要实现它,我创建一个静态URL,提供controller和action的默认值,像下面的代码一样:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 using System.Web.Routing;
 7 
 8 namespace UrlsAndRoutes
 9 {
10     public class RouteConfig
11     {
12         public static void RegisterRoutes(RouteCollection routes)
13         {
14             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
15 
16             routes.MapRoute("ShopSchema2", "Shop/OldAction", new { controller = "Home", action = "Index" });
17 
18             routes.MapRoute("ShopSchema", "Shop/{action}", new { controller = "Home" });
19 
20             routes.MapRoute("", "X{controller}/{action}");
21 
22             routes.MapRoute("MyRoute", "{controller}/{action}", new { controller = "Home", action = "Index" });
23 
24             routes.MapRoute("", "Public/{controller}/{action}", new { controller = "Home", action = "Index" });
25         }
26     }
27 }

注意,又一次,我把新的路由放在定义的第一位。这是因为它比后面的路由更具体。如果对Shop/OldAction的请求被下一个定义的路由处理,我将得到不是我想要的另外的结果。请求将被处理成404-Not Found错误,而不是转成我客户保留的URL。

 定义自定义节变量

controller和action节变量对MVC框架都有具体的含义,很明显地,它们对应于控制器和行为方法,被用来处理请求。但是这些都只是内建的节变量。我也能够定义我自己的变量,像下面的代码一样:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 using System.Web.Routing;
 7 
 8 namespace UrlsAndRoutes
 9 {
10     public class RouteConfig
11     {
12         public static void RegisterRoutes(RouteCollection routes)
13         {
14             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
15 
16             routes.MapRoute("MyRoute", "{controller}/{action}/{id}",
17                             new
18                             {
19                                 controller = "Home",
20                                 action = "Index",
21                                 id = "DefaultId"
22                             });
23         }
24     }
25 }

路由的URL模式定义了标准的controller和action变量,还定义了一个名叫id的定制变量。这个路由将匹配任何零个到三个节的URL。第三个节的内容将赋值给id变量,如果没有第三个节,将使用默认值。

我可以在action方法中使用RoutData.Values属性获取任何节的变量。看下面的代码:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 
 7 namespace UrlsAndRoutes.Controllers
 8 {
 9     public class HomeController : Controller
10     {
11         public ActionResult Index()
12         {
13             ViewBag.Controller = "Home";
14             ViewBag.Action = "Index";
15             return View("ActionName");
16         }
17 
18         public ActionResult CustomVariable()
19         {
20             ViewBag.Controller = "Home";
21             ViewBag.Action = "CustomVariable";
22             ViewBag.CustomVariable = RouteData.Values["id"];
23             return View("ActionName");
24         }
25     }
26 }

这个方法获取了在路由URL中的自定义变量值,使用ViewBag传给了视图。再修改ActionName视图。

 1 @{
 2     Layout = null;
 3 } 
 4 <!DOCTYPE html>
 5 <html>
 6 <head>
 7     <meta name="viewport" content="width=device-width" />
 8     <title>ActionName</title>
 9 </head>
10 <body>
11     <div>The controller is: @ViewBag.Controller</div>
12     <div>The action is: @ViewBag.Action</div>
13     <div>The custom variable is: @ViewBag.CustomVariable</div>
14 </body>
15 </html>

启动应用程序,导航到/Home/CustomVariable看运行效果。

 

 

我给路由的id节提供了默认值,意味如果我导航到/Home/CustomVariable将看到下面的结果:

 

使用自定义变量作为行为方法参数

使用RoutData.Values属性只是访问自定义路由变量的一个方法。另外的方式更优美。如果我给行为方法定义一个名字匹配URL模式变量的参数,MVC框架将从URL获取值并传给行为方法的参数。例如,我定义的路由自定义变量是id,我可以修改Home控制器里的CustomVariable行为方法,让他包含一个匹配的参数。

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 
 7 namespace UrlsAndRoutes.Controllers
 8 {
 9     public class HomeController : Controller
10     {
11         public ActionResult Index()
12         {
13             ViewBag.Controller = "Home";
14             ViewBag.Action = "Index";
15             return View("ActionName");
16         }
17 
18         public ActionResult CustomVariable(string id)
19         {
20             ViewBag.Controller = "Home";
21             ViewBag.Action = "CustomVariable";
22             ViewBag.CustomVariable = id;
23             return View("ActionName");
24         }
25     }
26 }

当路由系统匹配到上面的路由,URL上的第三个节的值传给自定义变量id。MVC框架比较节变量列表和action方法的参数列表,如果名称匹配,则将URL上的值传给方法。

我定义了id参数为字符串类型,但是MVC框架将尝试将URL的值转变成我定义的任意类型。如果我声明这个id参数为int或者DateTime类型,我将从URL获取值并转换成那种类型的实例。这很简洁,省去了我自己去做类型转化。

定义可选URL节

可选URL节是用户不需要指定的节,但是这样就是没有默认值的节。下面的代码使用UrlParameter.Optional指定了一个可选URL节:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 using System.Web.Routing;
 7 
 8 namespace UrlsAndRoutes
 9 {
10     public class RouteConfig
11     {
12         public static void RegisterRoutes(RouteCollection routes)
13         {
14             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
15 
16             routes.MapRoute("MyRoute", "{controller}/{action}/{id}",
17                             new
18                             {
19                                 controller = "Home",
20                                 action = "Index",
21                                 id = UrlParameter.Optional
22                             });
23         }
24     }
25 }

This route will match URLs whether or not the id segment has been supplied. Table 15-4 shows how this works for different

这个路由将匹配id有提供或没提供的URLs。下表列出了几种情况。

Segments Example URL Maps To
0 mydomain.com controller = Home action = Index
1 mydomain.com/Customer  controller Customer action Index
2 mydomain.com/Customer/List controller Customer action List
3 mydomain.com/Customer/List/All  controller Customer action List id All
4 mydomain.com/Customer/List/All/Delete No match—too many segments 


你从表中可以看到,只有当进入的URL包含了一个相应的节的时候,id变量才被添加到变量集合中。如果你需要知道客户是否给这个节变量提供了值,这个功能将非常有用。当没有值提供给可选节变量的时候,相应的参数值将为null。
然后,对于新的路由,我修改了控制器方法:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 
 7 namespace UrlsAndRoutes.Controllers
 8 {
 9     public class HomeController : Controller
10     {
11         public ActionResult Index()
12         {
13             ViewBag.Controller = "Home";
14             ViewBag.Action = "Index";
15             return View("ActionName");
16         }
17 
18         public ActionResult CustomVariable(string id)
19         {
20             ViewBag.Controller = "Home";
21             ViewBag.Action = "CustomVariable";
22             ViewBag.CustomVariable = id ?? "<no value>";
23             return View("ActionName");
24         }
25     }
26 }

启动应用程序,导航到/Home/CustomVariable看运行效果。

使用可选URL节分离关注点

一些关注MVC模式的分离关注点的开发人员不喜欢在应用程序里的路由中定义节变量的默认值。这样的话,你可以使用C#可选参数,以及路由的可选节变量,来定义行为方法的默认值。例如,下面的代码展示了定义将用在不包含值的URL上,id参数的默认值:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 
 7 namespace UrlsAndRoutes.Controllers
 8 {
 9     public class HomeController : Controller
10     {
11         public ActionResult Index()
12         {
13             ViewBag.Controller = "Home";
14             ViewBag.Action = "Index";
15             return View("ActionName");
16         }
17 
18         public ActionResult CustomVariable(string id = "DefaultId")
19         {
20             ViewBag.Controller = "Home";
21             ViewBag.Action = "CustomVariable";
22             ViewBag.CustomVariable = id;
23             return View("ActionName");
24         }
25     }
26 }

id参数将永远有一个非null的值(或者是从URL获取,或者是默认值),所以我可以删除处理null值的代码。这段代码等同于下面的路由器定义代码:

1              routes.MapRoute("MyRoute", "{controller}/{action}/{id}",
2                              new
3                              {
4                                  controller = "Home",
5                                  action = "Index",
6                                  id = UrlParameter.Optional
7                              });

它们的不同在于id节的默认值是定义在控制器里还是定义在路由定义里。

定义可变长度路由

另一个改变默认URL模式的保守性的方法是接受可变URL节数量。这让你在单一个路由中路由任意长度的URLs。你定义其中一个节变量为以星号*作为前缀的catchall来定义支持可变节长度。

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 using System.Web.Routing;
 7 
 8 namespace UrlsAndRoutes
 9 {
10     public class RouteConfig
11     {
12         public static void RegisterRoutes(RouteCollection routes)
13         {
14             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
15 
16             routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
17                             new
18                             {
19                                 controller = "Home",
20                                 action = "Index",
21                                 id = UrlParameter.Optional
22                             });
23         }
24     }
25 }

我扩展了之前的路由,添加了一个catchall节变量,我富于想象地称它是catchall节。这个路由现在将匹配任何URL,而不管它包含的节的数量或者任何这些节的值。前三个节用来各自设置controller、action和id变量的值。如果URL包含额外的节,它们都将赋值给catchall变量。看下面的表:

Segments Example URL Maps To
0 / controller = Home action = Index
1 /Customer controller = Customer action = Index
2 /Customer/List controller = Customer action = List
3 /Customer/List/All controller = Customer action = List id = All
4 /Customer/List/All/Delete controller = Customer action = List id = All catchall = Delete
5 /Customer/List/All/Delete/Perm controller = Customer action = List id = All catchall = Delete/Perm

在这个路由里匹配的URL模式节的数量没有上限。注意catchall捕获的节按segment / segment / segment格式呈现。

设置控制器的优先级

当一个进入的URL匹配一个路由,MVC框架获取controller变量的值,并寻找合适的名字。例如,当controller变量的值是Home,MVC框架就去寻找名叫HomeController的控制器。这是一个不合格的类名,这意味着如果两个或以上的类名称都是HomeController,MVC框架就不知道怎么处理了。

为了演示这个问题,在示例工程的根目录下,创建一个名称是AdditionalController的新文件夹,添加一个新的Home控制器,修改这个控制器的代码:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 
 7 namespace UrlsAndRoutes.AdditionalControllers
 8 {
 9     public class HomeController : Controller
10     {
11         // GET: Home
12         public ActionResult Index()
13         {
14             ViewBag.Controller = "Additional Controllers - Home";
15             ViewBag.Action = "Index";
16             return View("ActionName");
17         }
18     }
19 }

当你启动应用程序的时候,你将看到下面的运行结果:

MVC框架查询一个名称是HomeController的类,它找到两个这样的类:一个在之前的RoutsAndUrls.Controllers名称空间里,另一个在RoutesAndUrls.AdditionalControllers名称空间里。如果你仔细阅读上面运行结果的页面,你能够看到MVC框架帮助报告了它找到了两个这样的类。

这个问题可能比你想象的发生的更多,特别是如果你在一个大的MVC工程里工作,使用了其他开发小组或者第三方供应商提供的控制器类库。很自然给用户账户的控制器取名为AccountController,你迟早会碰到名称冲突。
为了解决这个问题,我可以告诉MVC框架当尝试解析控制器类名称的时候,给某些名称空间更高的优先级。例如下面的代码:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 using System.Web.Routing;
 7 
 8 namespace UrlsAndRoutes
 9 {
10     public class RouteConfig
11     {
12         public static void RegisterRoutes(RouteCollection routes)
13         {
14             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
15 
16             routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
17                             new
18                             {
19                                 controller = "Home",
20                                 action = "Index",
21                                 id = UrlParameter.Optional
22                             }, 
23                             new[] { "URLsAndRoutes.AdditionalControllers" });
24         }
25     }
26 }

我用了一个字符串数组表达了名称空间,在列表里我告诉了MVC框架先去URLsAndRoutes.AdditionalControllers名称空间里去查询,然后再去其他地方查询。

如果不能在那个名称空间里找到合适的控制器,MVC框架将默认执行常规行为,查询所有可用的名称空间。如果你再次启动修改后的应用程序,你将看到下面的运行结果。显示查询根URL,转变成请求Home控制器里的Index行为方法,发送到AdditionalController名称空间的控制器。

添加到路由器的名称空间有相等的优先级。MVC框架不是首先检查第一个名称空间然后再检查第二个名称空间。例如,如果我把两个名称空间都添加进来:

1             routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
2                             new
3                             {
4                                 controller = "Home",
5                                 action = "Index",
6                                 id = UrlParameter.Optional
7                             }, 
8                             new[] { "URLsAndRoutes.AdditionalControllers", "UrlsAndRoutes.Controllers" });

我将得到上面一样的错误结果,因为MVC框架是尝试在添加到路由的所有名称空间里解析控制器类名的。如果我想在一个名称空间里优先解析单独一个控制器,但是所有其他的控制器在另外的名称空间里解析,我需要创建多个路由,像下面的代码这样:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 using System.Web.Routing;
 7 
 8 namespace UrlsAndRoutes
 9 {
10     public class RouteConfig
11     {
12         public static void RegisterRoutes(RouteCollection routes)
13         {
14             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
15 
16             routes.MapRoute("AddContollerRoute", "Home/{action}/{id}/{*catchall}",
17                             new
18                             {
19                                 controller = "Home",
20                                 action = "Index",
21                                 id = UrlParameter.Optional
22                             }, 
23                             new[] { "URLsAndRoutes.AdditionalControllers" });
24 
25             routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
26                            new
27                            {
28                                controller = "Home",
29                                action = "Index",
30                                id = UrlParameter.Optional
31                            },
32                            new[] { "UrlsAndRoutes.Controllers" });
33         }
34     }
35 }

当用户显示地请求一个第一个节时Home的URL时,第一个路由将被运用。将目标到在AdditionalController文件夹里的Home控制器。所有其他的请求,包括那些第一个节没有指定的,将被Controller文件夹里的控制器处理。

我能够告诉MVC框架只在我指定的名称空间里去查询。如果找不到匹配的控制器,框架将不会去其他地方寻找。像下面的代码:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 using System.Web.Routing;
 7 
 8 namespace UrlsAndRoutes
 9 {
10     public class RouteConfig
11     {
12         public static void RegisterRoutes(RouteCollection routes)
13         {
14             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
15 
16             Route myRoute = routes.MapRoute("AddContollerRoute", "Home/{action}/{id}/{*catchall}",
17                             new
18                             {
19                                 controller = "Home",
20                                 action = "Index",
21                                 id = UrlParameter.Optional
22                             }, 
23                             new[] { "URLsAndRoutes.AdditionalControllers" });
24 
25             myRoute.DataTokens["UseNamespaceFallback"] = false;
26         }
27     }
28 }

MapRoute方法返回一个Route对象。我在之前的例子中都忽略了这个,是因为我不需要调整创建了的路由。为了让框架不寻找其他的名称空间,我获得Route对象,并设置DataTokens集合里的UseNamespaceFallback键属性为false。

这个设置将传递给负责查找控制器的组件,这个组件是控制器工厂。我以后的文章中将要介绍它。这个代码的作用是如果在AdditionalController文件夹内找不到适合的Home控制器,这个请求将失败。

路由约束

之前我介绍过URL模式在匹配节的时候是保守的,而在匹配节的时候是大方的。前面的部分介绍了控制保守性的不同技术:使用默认值,可选变量等方法,让一个路由匹配更多或更少的节。
现在来看一下如何控制匹配URL节内容的自由度:怎样限制路由可以匹配的URLs集合。一旦我可以控制路由行为的这两个方面,我就可以创建像激光镭射一样精确的URL格式。

第一个技术是使用正则表达式限制路由。像下面的代码一样:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 using System.Web.Routing;
 7 
 8 namespace UrlsAndRoutes
 9 {
10     public class RouteConfig
11     {
12         public static void RegisterRoutes(RouteCollection routes)
13         {
14             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
15 
16             Route myRoute = routes.MapRoute("AddContollerRoute", "Home/{action}/{id}/{*catchall}",
17                             new
18                             {
19                                 controller = "Home",
20                                 action = "Index",
21                                 id = UrlParameter.Optional
22                             },
23                             new { controller = "^H.*" },
24                             new[] { "URLsAndRoutes.AdditionalControllers" });
25 
26             myRoute.DataTokens["UseNamespaceFallback"] = false;
27         }
28     }
29 }

向MapRoute方法传递参数来定义限制。像默认值一样,限制也是匿名类表达的,类型的属性对应他们限制的节变量的名字,我使用了一个用正则表达式的限制,使得URL只匹配以H字母开头的controller变量值。

具体值的集合的路由约束

正则表达式可以约束一个路由,使得只有具体URL节的一些值才可以匹配。我使用杠(|)字符来创建这样一个约束。像下面的代码一样:

1             Route myRoute = routes.MapRoute("AddContollerRoute", "Home/{action}/{id}/{*catchall}",
2                             new
3                             {
4                                 controller = "Home",
5                                 action = "Index",
6                                 id = UrlParameter.Optional
7                             },
8                             new { controller = "^H.*", action = "^Index$|^About$" },
9                             new[] { "URLsAndRoutes.AdditionalControllers" });

这个约束只允许路由匹配那些action节的值是Index或者About的URLs。

约束是一齐应用的,因此加在action变量值的约束是和加在controller变量的约束一齐起作用的。这就意味着上面的路由只匹配controller变量以H字母开头并且action变量是Index或About的URLs。因此现在你应该明白了我说的创建精确路由的意思了。

使用HTTP方法的路由约束

路由可以限制成让他们只匹配使用具体HTTP方法的请求。像下面的代码一样:

 1             Route myRoute = routes.MapRoute("AddContollerRoute", "Home/{action}/{id}/{*catchall}",
 2                             new
 3                             {
 4                                 controller = "Home",
 5                                 action = "Index",
 6                                 id = UrlParameter.Optional
 7                             },
 8                             new { controller = "^H.*", action = "^Index$|^About$",
 9                                 httpMethod = new HttpMethodConstraint("GET") },
10                             new[] { "URLsAndRoutes.AdditionalControllers" });

指定一个HTTP方法约束的格式有一点点奇怪。什么的名字传给属性不重要,只要它是传给HttpMethodContraint类的对象。

我传递了我想要支持的HTTP方法的名字的字符串给HttpMethodConstraint类的构造函数参数中。我限制这个路由为只匹配GET请求,但是我可以很容易地添加对其他方法的支持:

httpMethod = new HttpMethodConstraint("GET", "POST") }

使用类型和值的约束

MVC框架包含大量的内建的约束,可以用来约束URL,这些是路由根据类型和节变量的值匹配的URL。下面的代码:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 using System.Web.Mvc.Routing.Constraints;
 7 using System.Web.Routing;
 8 
 9 namespace UrlsAndRoutes
10 {
11     public class RouteConfig
12     {
13         public static void RegisterRoutes(RouteCollection routes)
14         {
15             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
16 
17             Route myRoute = routes.MapRoute("AddContollerRoute", "Home/{action}/{id}/{*catchall}",
18                             new
19                             {
20                                 controller = "Home",
21                                 action = "Index",
22                                 id = UrlParameter.Optional
23                             },
24                             new { controller = "^H.*", action = "^Index$|^About$",
25                                 httpMethod = new HttpMethodConstraint("GET"),
26                                 id = new RangeRouteConstraint(10, 20)
27                             },
28                             new[] { "URLsAndRoutes.AdditionalControllers" });
29 
30             myRoute.DataTokens["UseNamespaceFallback"] = false;
31         }
32     }
33 }

在约束类(在System.Web.Mvc.Routing名称空间里)里面,检查节变量是否是不同C#类型的值,能够做基础的检查。在上面的代码里,我使用了RangeRouteContraint类,它检查提供给节变量的值是否是合法的落在两个界限中间的值,在这里是10和20。

定义自定义约束

如果对于你的需求标准的约束还不够,你可以定义你自己的实现IRouteContraint接口的约束。作为例子,我添加了一个Infrastructure文件夹,在里面创建一个名叫UserAgentContraint.cs的代码文件。

 1 using System.Web;
 2 using System.Web.Routing;
 3 
 4 namespace UrlsAndRoutes.Infrastructure
 5 {
 6     public class UserAgentConstraint : IRouteConstraint
 7     {
 8         private string requiredUserAgent;
 9         public UserAgentConstraint(string agentParam)
10         {
11             requiredUserAgent = agentParam;
12         }
13         public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
14         {
15             return httpContext.Request.UserAgent != null && httpContext.Request.UserAgent.Contains(requiredUserAgent);
16         }
17     }
18 }

IRouteContraint接口定义了一个Match方法,它的实现方法可以用来指示路由系统它的约束是否被满足了。Match方法的参数提供访问客户端的请求、正在被计算的路由、约束的参数名、从URL抽取出来的节变量,以及请求是否是检查进入还是出去的URL的详细信息。例如,我检查客户端请求的UserAgent属性,来看它是否包含传给构造器的一个值。下面的代码展示了在一个路由器中使用这个自定义约束。

 1             Route myRoute = routes.MapRoute("AddContollerRoute", "Home/{action}/{id}/{*catchall}",
 2                             new
 3                             {
 4                                 controller = "Home",
 5                                 action = "Index",
 6                                 id = UrlParameter.Optional
 7                             },
 8                             new { controller = "^H.*", action = "^Index$|^About$",
 9                                 httpMethod = new HttpMethodConstraint("GET"),
10                                 id = new RangeRouteConstraint(10, 20),
11                                 customConstraint = new UserAgentConstraint("Chrome")
12                             },
13                             new[] { "URLsAndRoutes.AdditionalControllers" });

在上面的代码中,我限制了这个路由,它将只匹配从那些user-agent字符串含有Chrome的浏览器发送来的请求。如果路由匹配,请求将发送出去。

posted on 2018-05-29 22:14  丹尼大叔  阅读(900)  评论(0编辑  收藏  举报