《Pro ASP.NET MVC 3 Framework》学习笔记之十八【URL和Routing】

整个项目范围的依赖注入(Project-Wide Dependency Injection)

在书接下来的章节里面,我们会看到MVC框架提供的很多不同的方式来让我们扩展和自定义对请求的处理,每一种方式都会用一个实现的接口或一个派生的基类来定义。

在第一部分的SportsStore项目实例里面已经有过引入。我们从DefaultControllerFactory类派生了一个NinjectControllerFactory类,以至于我们能够创建Controller,并使用Ninject来管理DI(依赖注入)。如果使用这种方法针对MVC里面每一个自定义的点,最终会让我们将DI贯彻到整个应用程序,但是这样会复制很多代码,会有比我们想象得多的Ninject kernel。

当MVC框架需要创建一个类的实例时,它会调用System.Web.Mvc.DependencyResolver类的一个静态方法。我们可以通过实现IDependencyResolver接口并使用DependencyResolver注册我们的实现来达到将DI贯彻整个应用程序的效果。这样做以后,无论MVC框架什么时候需要创建一个类的实例,它都会调用我们定义的类,我们就可以调用Ninject来创建对象。

下面的代码展示了在SportsStore里面如何实现IDependencyResolver接口,如下:

View Code

using System; using System.Collections.Generic; using System.Web.Mvc; using Ninject; using Ninject.Parameters; using Ninject.Syntax; using SportsStore.Domain.Abstract; using SportsStore.Domain.Concrete; using SportsStore.WebUI.Infrastructure.Abstract; using SportsStore.WebUI.Infrastructure.Concrete; using System.Configuration; 

namespace SportsStore.WebUI.Infrastructure { public class NinjectDependencyResolver : IDependencyResolver { private IKernel kernel; 

public NinjectDependencyResolver() {             kernel = new StandardKernel();             AddBindings();         } 

public object GetService(Type serviceType) { return kernel.TryGet(serviceType);         } 

public IEnumerable<object> GetServices(Type serviceType) { return kernel.GetAll(serviceType);         } 

public IBindingToSyntax<T> Bind<T>() { return kernel.Bind<T>();         } 

public IKernel Kernel { get { return kernel; }         } 

private void AddBindings() { 

// put additional bindings here 

Bind<IProductRepository>().To<EFProductRepository>();             Bind<IAuthProvider>().To<FormsAuthProvider>(); 

// create the email settings object             EmailSettings emailSettings = new EmailSettings {                 WriteAsFile = bool.Parse(                     ConfigurationManager.AppSettings["Email.WriteAsFile"] ?? "false")}; 

Bind<IOrderProcessor>()                 .To<EmailOrderProcessor>()                 .WithConstructorArgument("settings", emailSettings);         }     } }

上面的类里面,当MVC请求一个新的类的实例的时候会调用开始的两个方法,经历这样的请求也会很简单的调用Ninject kernel。我们添加了一个用来绑定来自类外面绑定的方法AddBindings()方法。接着注册IDependencyResolver接口实现,如下所示:

View Code

protected void Application_Start() { 

AreaRegistration.RegisterAllAreas(); 

RegisterGlobalFilters(GlobalFilters.Filters);     RegisterRoutes(RouteTable.Routes); 

DependencyResolver.SetResolver(new NinjectDependencyResolver()); 

ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder()); }

所有这些修改让我们将Ninject放在了MVC非常核心的位置上,我们仍然能够利用MVC扩展性强的优势。但是如果我们要做的全部就是在请求管2道的一些部分引入DI的话,我们就不用再做了,因为前面已经完成了,呵呵。

接下来的笔记主要是关于URL和Routing的(非常重要的,webform里面是基于事件驱动的,MVC里面是基于URL驱动的)

在WebFrom里面我们知道,在请求的URL和服务器磁盘上的文件是有直接的对应关系的,要是没有这种关系,通常都会报404错误。而在MVC里面是没有这种一对一的对应关系的,URL请求都是通过Controller里面的Action方法来处理的。为了处理MVC的URL,asp.net平台使用了routing system,路由系统不是MVC特有的,而是asp.net平台的功能,也就是说在WebFrom里面也是存在路由系统的。routing system的类包含在了System.Web命名空间下,从这里也可以看出它不是MVC特有的。

MVC的Routing System(路由系统)具有两个功能:

1.检查传入的URL,并计算出该请求需要的Controller和Action
2.创建输出的URL,这些URL是在从View呈现的html里面出现的,为了能够在用户点击一个URL时能调用一个具体的Action(当用户点击时又变成一个传入的URL了)

下面创建一个MVC3的项目UrlsAndRoutes来具体说明MVC里面的routing。

这次我们选择Internet Application Template,它会帮我们生成一个实例程序,方便我们学习。MVC里面的路由定义在Global.asax.cs里面

View Code

using System.Web.Mvc; using System.Web.Routing; 

namespace UrlsAndRoutes { 

public class MvcApplication : System.Web.HttpApplication {protected void Application_Start() {             AreaRegistration.RegisterAllAreas(); 

RegisterGlobalFilters(GlobalFilters.Filters);             RegisterRoutes(RouteTable.Routes);         } 

public static void RegisterRoutes(RouteCollection routes) {              路由定义在这里...        } 

public static void RegisterGlobalFilters(GlobalFilterCollection filters) {             filters.Add(new HandleErrorAttribute());             }     } }

Application_Start()方法在程序第一次启动时被底层的asp.net平台调用,进而导致RegisterRoutes方法也会被调用。方法的参数是一个静态属性RouteTable.Routes,它是RouteCollection类的一个实例。

这里我们可以先删除在RegisterRoutes里面生成的默认的路由值,在此之前,我们要先了解下对routing system非常重要的东西:

URL模式(URL patterns)

routing system一个不可思议的地方是它使用了一套routes,这些routes组合构成一个应用程序的URL架构或模式,这些URL模式是程序将要识别和响应的。每一个路由包含一个URL模式,将它跟请求过来的URL进行比较。如果能够匹配,routing system就会用它来处理这个URL。

URL可以分割为几个不同的段(Segment)(不包含主机名和query string),通过"/"分隔,例如下面的一个URL。

分隔的第一个小段是Admin,第二个是Index。

我们能够看出这个对应了Controller和Action,但是我们需要把这种对应关系表达为routing system能够理解的形式,也就是URL模式(URL pattern)。这里的URL模式就是{controller}/{action}。

当处理请求URL时,routing system的工作就是匹配该URL到一个URL模式并提取在模式里面定义的segment变量的值(segment变量就是这里的controller和action,它们对应的值分别是Admin和Index)。

Note:routing system没有任何关于controller和action的特别的知识点,仅仅是当请求到达MVC框架本身时提取segment变量的值并在请求管道(request pipeline)中传递,这意味着会给controller和action变量赋值。这也就是为什么routing system能够在WebFrom里面使用以及我们如何能够自定义变量。

默认情况下,一个URL模式会匹配任何一个跟它具有相同segment数量的URL。例如{controller}/{action}会匹配如下URL

图中高亮的部分说明了URL模式的两个关键的行为:

1.URL模式是保守的:只匹配具有同等segment数量的URL  

2.URL模式同时又是开放的:只有segment数量相等,不过具体的是什么,都能够提取segment变量对应的值。

前面有提到过,routing system是独立于MVC的,对MVC应用程序没有任何了解。所以,当没有对应的controller和action能够对从URLsegment变量提取的值进行响应的时候,URL模式仍会进行匹配。

上图中的第二种情况,就是把Admin和Index颠倒了,也能够提取值,只不过提取出来的值也是颠倒的。这里并没有一个Index的controller和Admin的Action。

下面创建一个简单的route,代码如下:

View Code

public static void RegisterRoutes(RouteCollection routes) { 

Route myRoute = new Route("{controller}/{action}", new MvcRouteHandler());     routes.Add("MyRoute", myRoute); }

我们创建了一个新的route对象,并将URL模式作为一个构造器参数传递,同时还传递了一个MvcRouteHandler的实例参数。不同的ASP.NET技术提供不同的类来处理路由的行为,MvcRouteHandler类是MVC里面使用的类。一旦创建了一个路由(Route),可以使用Add方法添加到RouteCollection对象里面,同时可以给我们创建的路由传递一个名字。在Add方法里面传递路由的名字是可选的,也可以为空。这里使用的是Add方法将定义的路由注册到RouteCollection里面,还有一种更加简便的方式就是使用MapRoute方法。如下所示:

View Code

public static void RegisterRoutes(RouteCollection routes) { 

routes.MapRoute("MyRoute", "{controller}/{action}"); }

这个方法更加精简,主要是因为不需要创建MvcRouteHandler类的实例。MapRoute仅仅是MVC使用的,在ASP.NET WebFrom里面使用的是MapPageRoute(也是定义在RouteCollection里面的)

要对routes进行单元测试,需要mock三个类 HttpRequestBase,  HttpContextBase, 和HttpResponseBase。为了方便测试,我们创建几个通用的方法如下:

View Code

private HttpContextBase CreateHttpContext(string targetUrl = null, string httpMethod = "GET")        {//create the mock request            Mock<HttpRequestBase> mockRequest = new Mock<HttpRequestBase>();            mockRequest.Setup(m => m.AppRelativeCurrentExecutionFilePath).Returns(targetUrl);            mockRequest.Setup(m => m.HttpMethod).Returns(httpMethod);

//create the mock response            Mock<HttpResponseBase> mockResponse = new Mock<HttpResponseBase>();            mockResponse.Setup(m => m.ApplyAppPathModifier(It.IsAny<string>())).Returns<string>(s => s);

//create the mock context,using the request and response            Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>();            mockContext.Setup(m => m.Request).Returns(mockRequest.Object);            mockContext.Setup(m => m.Response).Returns(mockResponse.Object);

//return the mocked context            return mockContext.Object;        }

private void TestRouteMatch(string url, string controller, string action, object routeProperties = null, string httpMethod = "GET")        {//Arrange            RouteCollection routes = new RouteCollection();            MvcApplication.RegisterRoutes(routes);//Act- process the route            RouteData result = routes.GetRouteData(CreateHttpContext(url, httpMethod));//Assert            Assert.IsNotNull(result);            Assert.IsTrue(TestIncomingRouteResult(result, controller, action, routeProperties));        }

private bool TestIncomingRouteResult(RouteData routeResult, string controller, string action, object propertySet = null)        {            Func<object, object, bool> valCompare = (v1, v2) => { return StringComparer.InvariantCultureIgnoreCase.Compare(v1, v2) == 0; };bool result = valCompare(routeResult.Values["controller"], controller) && valCompare(routeResult.Values["action"], action);if (propertySet != null)            {                PropertyInfo[] propInfo = propertySet.GetType().GetProperties();foreach (PropertyInfo pi in propInfo)                {if (!(routeResult.Values.ContainsKey(pi.Name) && valCompare(routeResult.Values[pi.Name], pi.GetValue(propertySet, null))))                    {                        result = false;break;                    }                }            }return result;        }

private void TestRouteFail(string url, string httpMethod = "GET")        {// Arrange            RouteCollection routes = new RouteCollection();            MvcApplication.RegisterRoutes(routes);// Act - process the route            RouteData result = routes.GetRouteData(CreateHttpContext(url, httpMethod));// Assert            Assert.IsTrue(result == null || result.Route == null);        }

下面是一个测试路由的例子:

View Code

[TestMethod] public void TestIncomingRoutes() { 

// check for the URL that we hope to receive     TestRouteMatch("~/Admin/Index", "Admin", "Index"); // check that the values are being obtained from the segments     TestRouteMatch("~/One/Two", "One", "Two"); 

// ensure that too many or too few segments fails to match     TestRouteFail("~/Admin/Index/Segment");     TestRouteFail("~/Admin"); }

测试里面有两个方法TestRouteMatch和TestRouteFail,TestRouteMatch确保URL正确的格式以及能够使用URL segments取到controller和action恰当的值;TestRouteFail确保我们的程序不接受不匹配URL模式的URL。测试时,在URL前面加上了"~"的前缀。因为ASP.NET框架就是这样将URL呈现给routing system的。

如果我们运行程序会报404错误,因为直接运行时,地址栏里面的URL是http://localhost:<port>/ ,而我们没有创建针对该URL的路由。当我们输入URL:http://localhost:<port>/Home/Index就可以看到示例效果了。

这里URL模式处理URL并提取Home这个controller变量的值和Index这个action变量的值(Home和Index就是segment变量)

定义默认值

在上面我们第一次运行程序的时候报了404错误,是因为"~/"没有匹配我们定义的URL模式{controller}/{action}。一种解决的方式就是定义默认值,当URL里面不包含要匹配的segment时,默认值就被使用了。把路由做如下更改:

View Code

public static void RegisterRoutes(RouteCollection routes) {     routes.MapRoute("MyRoute", "{controller}/{action}", new { action = "Index" }); }

默认值是作为一个匿名类型的属性提供的,上面的我们提供了action变量Index的默认值。这时路由就可以匹配两种URL了:

1.http://localhost:<port>/Home/Index;2.http://localhost:<port>/Home.

第一种不必说当然可以,因为我们定义action变量的默认值所以当URL里面没有action这个segment时,我们定义的默认值就被使用了。

我们进一步提供默认值,修改如下:

View Code

public static void RegisterRoutes(RouteCollection routes) {  routes.MapRoute("MyRoute", "{controller}/{action}",  new { controller = "Home", action = "Index" }); }

我们提供了两个默认值,这个时候运行程序就不会报错了。因为此时"http://localhost:<port>/"里面既没有controller这个segment,也没有action这个segment。但是我们定义的它们的默认值会被使用。这个路由可以匹配的URL如下所示:

可以做如下测试:

View Code

[TestMethod] public void TestIncomingRoutes() {     TestRouteMatch("~/", "Home", "Index");     TestRouteMatch("~/Customer", "Customer", "Index");     TestRouteMatch("~/Customer/List", "Customer", "List");     TestRouteFail("~/Customer/List/All");  }

使用静态的URL Segment

并不是在一个URL模式里面所有的segments都需要是变量。也可以定义不变的segment,即静态segment。
假设我们需要匹配这样一个URL:http://mydomain.com/Public/Home/Index 。可以添加一个路由如下:

public static void RegisterRoutes(RouteCollection routes) {     routes.MapRoute("MyRoute", "{controller}/{action}",  new { controller = "Home", action = "Index" }); 

 routes.MapRoute("", "Public/{controller}/{action}",  new { controller = "Home", action = "Index" }); }

Public/{controller}/{action}匹配包含3个segment的URL,第一个必须是Public.也可以创建一个具有静态元素也有变量的segment的URL模式.如下所示:

public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("", "X{controller}/{action}");     routes.MapRoute("MyRoute", "{controller}/{action}",  new { controller = "Home", action = "Index" });     routes.MapRoute("", "Public/{controller}/{action}", new { controller = "Home", action = "Index" });  }

如果http://mydomain.com/XHome/Index,而取第一个segment变量的值是不会包含X的,仍然取的是Home而不是XHome

Route排序

当使用MapRoute方法添加routes时,被使用的顺序一般情况下是按照它在RouteCollection对象里面出现的顺序。之所以这里说"一般情况",是因为有方法能够让我们选择插入的位置,但是不提倡这样做,因为直接使用MapRoute插入route是按照它们被定义的顺序,这样更容易理解程序的路由。路由系统通过URL模式匹配请求的URL,并且在当前路由不匹配的情况下才处理下一个路由,会一直匹配到最后一个路由为止。这样的结果就要求我们必须按照从具体到抽象的顺序来排列路由,这里类似于我们处理异常的顺序。如果我们将上面的路由排列顺序做如下改动:

public static void RegisterRoutes(RouteCollection routes) { 

routes.MapRoute("MyRoute", "{controller}/{action}",  new { controller = "Home", action = "Index" }); 

routes.MapRoute("", "X{controller}/{action}"); 

routes.MapRoute("", "Public/{controller}/{action}", new { controller = "Home", action = "Index" });  }

因为第一个路由会匹配具有0,1,2个segment的URL,其实也就是全部的情况了。这个时候运行程序输入:http://mydomain.com/XHome/Index,程序会报404错误。

我们能够联合静态URL和默认值来创建一个具体URL的别名,这会在需要公开的发布我们的URL架构并且与用户制定了一个契约时非常有用。如果我们在这种情形下重构一个应用程序,我们需要保护之前的URL格式。让我们设想这样一个场景:我们有一个ShopController,现在被HomeController代替,下面的代码展示如何保护老的URL架构:

View Code

public static void RegisterRoutes(RouteCollection routes) {     routes.MapRoute("ShopSchema", "Shop/{action}", new { controller = "Home" });     ...other routes... }

这个路由可以匹配以Shop为第一个segment并包含两个segment的URL。该URL模式没有包含一个controller的segment变量,所以这里的默认值会被使用。这意味着对ShopController里面Action请求转换成了对HomeController里面的Action请求。

我们可以进一步深入,为我们要重构的Action方法创建一个别名并且不用在呈现controller了,达到这样的效果只需要我们提供controller和action的默认值就可以了。如下:

public static void RegisterRoutes(RouteCollection routes) { 

 routes.MapRoute("ShopSchema2", "Shop/OldAction",  new { controller = "Home", action = "Index" }); 

routes.MapRoute("ShopSchema", "Shop/{action}", new { controller = "Home" });     其他路由...}

这时我们只需要输入http://localhost:port/Shop就行了,输入Shop/OldAction是会报404的。

自定义segment变量

MVC里面不是只能定义controller和action两个segment变量,我可以定义自己的segment变量,如下所示:

View Code

public static void RegisterRoutes(RouteCollection routes) {     routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "DefaultId" }); }

上面的路由定义了一个id的segment变量,可以匹配0-3个segment的URL。对于我们自己定义segment变量时,变量名不能是controller,action,area。
我们可以使用RouteData.Values访问任何segment变量,这里我们添加一个CustomVariable的方法到HomeController里面,如下所示:

View Code

public ViewResult CustomVariable() { ViewBag.CustomVariable = RouteData.Values["id"]; return View(); }

添加一个视图CustomVariable.cshtml将id的值显示出来,如下:

View Code

@{     ViewBag.Title = "CustomVariable"; } 

<h2>Variable: @ViewBag.CustomVariable</h2> 

下面是针对上面路由的单元测试,如下:

View Code

[TestMethod] public void TestIncomingRoutes() { TestRouteMatch("~/", "Home", "Index", new {id = "DefaultId"});     TestRouteMatch("~/Customer", "Customer", "index", new { id = "DefaultId" });     TestRouteMatch("~/Customer/List", "Customer", "List", new { id = "DefaultId" });     TestRouteMatch("~/Customer/List/All", "Customer", "List", new { id = "All" });     TestRouteFail("~/Customer/List/All/Delete"); }

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

使用RouteData.Values访问自定义的segment变量的值仅仅是一种方式,有更加优雅的方式来访问segment变量的值。如果我们在Action里面定义参数匹配URL模式的变量,MVC框架会将从URL获取的值作为参数传递给action方法。修改CustomVariable action方法以至于它有一个匹配的参数,如下:

View Code

        public ViewResult CustomVariable(string id)        {            ViewBag.CustomVariable = id;return View();        }

运行程序输入http://localhost:<port>/Home/CustomVariable/page_1/d   会显示id的值page_1.

当然也可以在路由里面定义多个segment参数,同样也在action方法里面定义多个同样的参数,这时就可以取多个自定义的segment变量的值。自定义的segment变量与action方法的参数要保持一样。

上面定义的id参数是一个字符串的类型,但是MVC框架会试图将URL值转换为任何我们定义在action方法里面的参数类型。如果我们这里定义id为Datetime类型,这时从URL传过来的就是Datetime类型。

下面修改路由增加一个segment变量为flag,并且修改action方法参数如下:

        //修改路由  可能有些参数的作用前面还没有介绍,不用急,后面会有说明。        routes.MapRoute("AddContollerRoute", "{controller}/{action}/{id}/{flag}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new[] { "AdditionalControllers" });//这个地方要改成你自己对应的,我这里用的不是                                                                                                           URLsAndRoutes.Controllers。这里后面有介绍的        public ViewResult CustomVariable(string id, DateTime flag)        {            ViewBag.CustomVariable = "id的值为:"+id +"——"+"flag的值为:"+ flag.ToString();            return View();        }  

运行程序如下图所示:

注意:如果这里的flag不能被转换为DateTime,则会报如下错误:

MVC框架使用模型绑定系统(model binding system)将URL里面包含的指转换为.NET类型。并且可以处理比上面例子里面更加复杂的情形,这一部分在书里面的后面章节会介绍。

在上面定义路由时会有id = UrlParameter.Optional。这个表示该URL中的id segment变量是可选的。关于匹配情况如下所示:

从上面的图可以看出,当URL里面有了对应id的segment时,id就有了相应的值。当没有相应的URL segment来对应id时,并不是表示值为null,而是没有定义。

当你需要知道用户是否提供了一个值时,这点就非常有用。当我们给action里面的参数id提供一个默认值时,我们就不能分别是默认值使用了还是用户输入的URL本身就恰好包含了默认值。

对应测试的代码如下:

View Code

[TestMethod] public void TestIncomingRoutes() { TestRouteMatch("~/", "Home", "Index");     TestRouteMatch("~/Customer", "Customer", "index");     TestRouteMatch("~/Customer/List", "Customer", "List");     TestRouteMatch("~/Customer/List/All", "Customer", "List", new { id = "All" });     TestRouteFail("~/Customer/List/All/Delete"); } 

前面的URL模式里面有包含/{*catchall}。以"*"开始的segment,这意味着{controller}/{action}/{id}/{flag}/后面可以匹配多个segment。

对于{controller}/{action}/{id}/{*catchall}(这里不一定要写成catchall,其他的以*开始也OK)可以匹配的情况,如下图所示:

对于*catchall的segment的数量是没有上限的。对应的单元测试如下:

View Code

[TestMethod] public void TestIncomingRoutes() { TestRouteMatch("~/", "Home", "Index");     TestRouteMatch("~/Customer", "Customer", "Index");     TestRouteMatch("~/Customer/List", "Customer", "List");     TestRouteMatch("~/Customer/List/All", "Customer", "List", new { id = "All" });     TestRouteMatch("~/Customer/List/All/Delete", "Customer", "List",  new { id = "All", catchall = "Delete" });     TestRouteMatch("~/Customer/List/All/Delete/Perm", "Customer", "List", new { id = "All", catchall = "Delete/Perm" }); }

定义优先使用的Controller所属的命名空间

在上面有定义路由时,有这样一个参数new[] { "AdditionalControllers" },AdditionalControllers是另外建的一个类库里面添加了一个HomeController。所以要添加一个这样的参数,是因为当有几个HomeController分别属于不同的命名空间。这个时候如果不指定一个优先的命名空间,就会抛异常。你可以添加一个AdditionalControllers类库项目,然后添加一个HomeController类,代码如下:

View Code

namespace AdditionalControllers{public class HomeController : Controller    {public ViewResult CustomVariable(string id, DateTime flag)        {            ViewBag.CustomVariable = "id的值为:"+id +"——"+"flag的值为:"+ flag.ToString();return View();        }    }}

当一个传入的URL匹配了一个路由,MVC框架会取controller变量的值并寻找合适的controller。例如:当controller变量的值是Home,MVC就会寻找名为HomeController的Controller。这是一个没有限制的类名,当有两个或以上的HomeController时,MVC是不知道怎么处理的,会报错。

这种在大的MVC项目里面发生的概率会大些,所以这就有必要指定一个优先的Controller。具体的使用在上面的定义的路由里面已经有了,定义一个路由如下:

View Code

public static void RegisterRoutes(RouteCollection routes) {      routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new[] { "URLsAndRoutes.Controllers"});  } 

我们将命名空间表达成一个字符串数组,在上面的代码里面,我们告诉MVC框架首先在URLsAndRoutes.Controllers里面寻找,如果没有找到合适的,则会在其他的命名空间里面寻找。所有添加到这个字符串数组里面的命名空间具有相同的优先级,也就是说不会因为URLsAndRoutes.Controllers在new[] { "URLsAndRoutes.Controllers", "AdditionalControllers"}的最前面而具有更高的优先级。

这样定义以后运行程序仍然会报错的,因为面对两个同名的同优先级的Controller,MVC是不知道怎么处理的。

那我们想要倾向性的使用一个命名空间中的Controller要怎么做呢?使用多个routes,如下所示:

View Code

public static void RegisterRoutes(RouteCollection routes) {     routes.MapRoute("AddContollerRoute", "Home/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new[] { "AdditionalControllers" }); 

routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new[] { "URLsAndRoutes.Controllers"}); }

在后面的章节会介绍如何对整个应用程序设置命名空间的优先级。

我们能够告诉MVC框架仅仅在指定的命名空间寻找,如果没有找到匹配的也不去其他的命名空间寻找。只需要添加一个属性值,如下所示:

public static void RegisterRoutes(RouteCollection routes) { Route myRoute = routes.MapRoute("AddContollerRoute", "Home/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new[] { "AdditionalControllers" }); 

myRoute.DataTokens["UseNamespaceFallback"] = false; }

使用正则表达式约束路由(Constraining Routes)

public static void RegisterRoutes(RouteCollection routes) {      routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new { controller = "^H.*"}, new[] { "URLsAndRoutes.Controllers"}); } 

我们通过传递给MapRoute方法一个参数来定义约束,也是一个匿名类型的。要约束的变量跟URL模式里面的segment变量保持一致。上面的路由对controller变量进行约束,必须是H开头的(当然,如果你跟我一样对正则不熟悉,那就去找相关的资料学习下,至少能够明白本书里面的所有正则表达式的含义)。默认值会在约束检查之前被使用。

将约束定义为具体的值:new { controller = "^H.*", action = "^Index$|^About$"}意思是:路由的controller要以H开头,并且action是Index或About。
使用HTTP方法约束:new { controller = "^H.*", action = "Index|About", httpMethod = new HttpMethodConstraint("GET") },这里是不是httpMethod不重要,只要我们实例化HttpMethodConstraint("")并赋值给一个变量(自定义,这里是httpMethod).

注意:这里的约束HTTP方法与在Action方法上面添加特性HttpGet和HttpPost是没有关联的。对路由的限制比在Action上面添加HttpGet和HttpPost限制会在请求管道(request pipeline)的更早的时候被处理。对处理不同的HTTP方法会在后面的章节详细介绍的。我们也可以添加几个HTTP方法如:httpMethod = new HttpMethodConstraint("GET", "POST") }。

对应的单元测试如下:

View Code

[TestMethod] public void TestIncomingRoutes() {     TestRouteMatch("~/", "Home", "Index");     TestRouteMatch("~/Home", "Home", "Index");     TestRouteMatch("~/Home/Index", "Home", "Index"); 

TestRouteMatch("~/Home/About", "Home", "About");     TestRouteMatch("~/Home/About/MyId", "Home", "About", new { id = "MyId" });     TestRouteMatch("~/Home/About/MyId/More/Segments", "Home", "About",  new {              id = "MyId",             catchall = "More/Segments"         }); 

TestRouteFail("~/Home/OtherAction");     TestRouteFail("~/Account/Index");     TestRouteFail("~/Account/About"); }

自定义约束

如果标准的约束不能满足我们的需求,可以自己实现约束,必须继承IRouteConstraint接口。下面的示例实现了对游览器的约束,当然实际情况不提倡限制浏览器的。这里仅仅为了说明功能。代码如下:

View Code

using System.Web; using System.Web.Routing; 

namespace URLsAndRoutes.Infrastructure { public class UserAgentConstraint : IRouteConstraint     { private string requiredUserAgent; 

public UserAgentConstraint(string agentParam)         {             requiredUserAgent = agentParam;         } 

public bool Match(HttpContextBase httpContext, Route route, string parameterName,                            RouteValueDictionary values, RouteDirection routeDirection) { 

return httpContext.Request.UserAgent != null &&                 httpContext.Request.UserAgent.Contains(requiredUserAgent);          }     } }

IRouteConstraint接口定义了一个Match方法,用来实现路由系统的约束是否被满足。如下:

View Code

public static void RegisterRoutes(RouteCollection routes) {     routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new {             controller = "^H.*", action = "Index|About",             httpMethod = new HttpMethodConstraint("GET", "POST"),             customConstraint = new UserAgentConstraint("IE")         }, new[] { "URLsAndRoutes.Controllers" }); } 

路由文件请求

不是所有的请求都是针对controller的action的,我也需要提供对images,静态HTML文件,javascript库等的服务。为了说明,先创建一个 StaticContent.html文件。
代码如下:

View Code

<html> <head><title>Static HTML Content</title></head> <body>This is the static html file (~/Content/StaticContent.html)</body> </html> 

输入http://localhost:<port>/Content/StaticContent.html可以访问.默认情况下:路由系统会先检查URL是否匹配具体的文件,不是匹配路由。如果有匹配的,则不会进行路由匹配。如果我们要颠倒这个顺序,可以使用routes.RouteExistingFiles = true(这里有个约定:必须放在方法体的最上面)。接着就可以为具体的文件路径定义路由了,如下所示:

View Code

public static void RegisterRoutes(RouteCollection routes){ 

routes.RouteExistingFiles = true; 

routes.MapRoute("DiskFile", "Content/StaticContent.html", new {             controller = "Account", action = "LogOn",         }, new {             customConstraint = new UserAgentConstraint("IE")         }); 

routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new {             controller = "^H.*", action = "Index|About",             httpMethod = new HttpMethodConstraint("GET", "POST"),             customConstraint = new UserAgentConstraint("IE")         }, new[] { "URLsAndRoutes.Controllers" }); }

此时运行系统输入:http://localhost:<port>/Content/StaticContent.html,会显示登录界面。因为优先使用了路由而不会去匹配URL里面的路径。
启用这个属性要慎重,以为很可能导致异常。一般情况下不要启用。现在添加一个OtherStaticContent.html,定义如下路由:

View Code

public static void RegisterRoutes(RouteCollection routes) { 

routes.RouteExistingFiles = true; 

routes.MapRoute("DiskFile", "Content/StaticContent.html", new {             controller = "Account", action = "LogOn",         }, new {             customConstraint = new UserAgentConstraint("IE")         }); 

routes.MapRoute("MyNewRoute", "{controller}/{action}"); 

routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new {             controller = "^H.*", action = "Index|About",             httpMethod = new HttpMethodConstraint("GET", "POST"),             customConstraint = new UserAgentConstraint("IE")         }, new[] { "URLsAndRoutes.Controllers" }); } 

运行程序,请求http://localhost:<port>/Content/OtherStaticContent.html就会报404.这样做是为了说明:当我们开启了routes.RouteExistingFiles = true属性,优先匹配的是文件路径而不是URL,当文件路径不匹配时(显然现在请求的是OtherStaticContent.html,跟路由里定义的routes.MapRoute("DiskFile", "Content/StaticContent.html"...是不匹配的,所以报404错误)

绕过路由

要忽略对某些文件的路由可以使用routes.IgnoreRoute,如下所示:

View Code

public static void RegisterRoutes(RouteCollection routes) { 

routes.RouteExistingFiles = true; 

routes.MapRoute("DiskFile", "Content/StaticContent.html", new {             controller = "Account", action = "LogOn",         }, new {             customConstraint = new UserAgentConstraint("IE")         }); 

routes.IgnoreRoute("Content/{filename}.html"); 

routes.MapRoute("", "{controller}/{action}"); 

routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new {             controller = "^H.*", action = "Index|About",             httpMethod = new HttpMethodConstraint("GET", "POST"),             customConstraint = new UserAgentConstraint("IE")         }, new[] { "URLsAndRoutes.Controllers" }); } 

IgnoreRoute方法创建了一个通路,让处理路由的实例是StopRoutingHandler类的而不是MvcRouteHandler的。当一个URL模式经过IgnoreRoute并匹配上,那后面的路由就不会进行匹配了或者说是评估。

好了,今天的笔记到这里,内容比较多,希望对跟我一样的同学有点帮助。
晚安!

时间: 2024-10-25 05:33:16

《Pro ASP.NET MVC 3 Framework》学习笔记之十八【URL和Routing】的相关文章

ASP.NET MVC 3 Framework学习笔记之Model Templates

.使用模板化的视图Helpers(Using Templated View Helpers) 模版化视图helpers的创意就是它们更加灵活.我们不用自己去指定应该用什么HTML元素来呈现一个模型的属性,MVC自己会搞定,在我们更新了视图模型时,也不用手动的更新视图.下面是一个例子:  代码如下 复制代码 //在Models里面添加Persons.cs using System; using System.Collections.Generic; using System.Linq; using

《Pro ASP.NET MVC 3 Framework》学习笔记目录

<Pro ASP.NET MVC 3 Framework>简介: 作者: Adam Freeman 和 Steven Sanderson 出版社: Apress; New 平装: 820页 语种: 英语 ISBN: 1430234040 声明:笔记里面按我自己的理解翻译了大部分内容,写这个笔记的目的:为了方便自己查阅,也为园友提供学习的方便. 我无意侵犯作者的任何权利,仅仅为了自己学习.也希望路过的朋友不要用于任何商业目的. 第一部分 ASP.NET MVC3介绍   <Pro ASP.

《Pro ASP.NET MVC 3 Framework》学习笔记之一【MVC的历程,优点,HelloWorld】

序论:asp.net mvc出现已经有两三年的时间了(2009开始1.0版本),但是这么方面的中文学习资料仍然非常少,特别是asp.net mvc3,几乎就没有中文的学习书籍.在英文的书籍中有两本是非常经典的mvc3教程:<Professional ASP.NET MVC 3>--作者:Jon Galloway , Phil Haack, Brad Wilson , K. Scott Allen和<Pro ASP.NET MVC 3 Framework>--作者:Steven Sa

ASP.NET MVC Web API 学习笔记----HttpClient简介

  1. HttpClient简单介绍  依稀还记得那个时候用WebClient,HttpWebRequest来发送一个请求,现在ASP.NET MVC4中自带了一个类HttpClient,用于接收HttpResponseMessage和发送HttpRequestMesssage. 问题在于既然WebClient,HttpWebRequest可以完成相应的功能,为什么还要使用HttpClient类,.NET Framework中既然提出了这样一个类肯定是有其特别之处的,这里罗列几个不同之处: (

ASP.NET MVC Web API 学习笔记---联系人增删改查

本章节简单介绍一下使用ASP.NET MVC Web API 做增删改查.目前很多Http服务还是通过REST或者类似RESP的模型来进行数据操作的.下面我们通过创建一个简单的Web API来管理联系人          说明:为了方便数据不使用真正的数据库,而是通过内存数据模拟    1.       Web API中包含的方法 Action HTTP method Relative URI GetAllContact GET /api/contact GetContact GET /api/

《Pro ASP.NET MVC 3 Framework》学习笔记之九【Ninject的使用-下】

接着上次的Ninject的笔记,如果你是初次路过,可以先看看我前面的笔记. 一,创建依赖链(Chains of Dependency) 当我们向Ninject请求创建一个类型时,Ninject会去检查该类型和其他类型之间的耦合关系.如果有额外的依赖,Ninject也会解析它们并创建我们需要的所有类的实例.为了进一步说明,我们创建一个新的接口和一个实现该接口的类.请注意我们的例子是跟前面的笔记衔接的,所以如果你打算跟着一起操作的话,最好能够去看看前面的笔记. 创建一个IDiscountHelper

《Pro ASP.NET MVC 3 Framework》学习笔记之四【领域模型介绍】

主题:应用领域驱动开发(Applying Domain-Driven Development) Domain Model是MVC程序的"心脏",其他的一切,包括Controllers和Views仅仅是用来跟Domain Model交互的一种方式,ASP.NET MVC并没有限制使用在Domain Model上面的技术,我们可以自由的选择跟.net framework交互的技术,并且这样的选择是非常多的.不仅如此,ASP.NET MVC为我们提供了基础的架构和约定来帮助Domain Mo

《Pro ASP.NET MVC 3 Framework》学习笔记之五【依赖注入及ninject工具使用】

一,创建松耦合的组件 1."分解关注点"是MVC模式里面一个非常重要的特性.我们想要在应用程序里面创建的组件尽可能的独立,这样我们就能管理比较少的依赖关系.理想情况下,每个组件都是孤立的,不知道其他组件的存在,处理应用程序的其他领域仅仅通过抽象接口,这就是所谓的松耦合,它让我们的应用程序更加容易测试和修改.通过一个简单的例子可以帮助我们理解,假如我们想写一个发邮件的组件,暂且就把这个组件命名为MyEmailSender,接着我们实现一个接口,这个接口定义了所有需要发送邮件的功能,也暂且

《Pro ASP.NET MVC 3 Framework》学习笔记之三十五 【部署】

准备要部署的应用程序 在正式进入部署MVC程序到IIS之前,会介绍一些关于应用程序迁移到生产环境之前探测错误以及一旦进入生产环境最大化性能的技术.同时也会展示关于流线型部署过程的有用的功能.   检测视图错误 Razor视图会在服务器需要的时候编译而不是在VS里面生成项目时编译,正常情况下,探测视图编译错误的方式是系统的访问每一个action,从而让每一个view都能够呈现.这显然是非常乏味而且不会一直成功的技术,特别是在基于不同的model状态呈现不同的view的时候.我们可以启用一个特别的项