整個項目范圍的依賴注入(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接口,如下:

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接口實現,如下所示:

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里面

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,代碼如下:

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方法。如下所示:

public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}");
}
這個方法更加精簡,主要是因為不需要創建MvcRouteHandler類的實例。MapRoute僅僅是MVC使用的,在ASP.NET WebFrom里面使用的是MapPageRoute(也是定義在RouteCollection里面的)
要對routes進行單元測試,需要mock三個類 HttpRequestBase, HttpContextBase, 和HttpResponseBase。為了方便測試,我們創建幾個通用的方法如下:

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);
}
下面是一個測試路由的例子:

[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時,默認值就被使用了。把路由做如下更改:

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時,我們定義的默認值就被使用了。
我們進一步提供默認值,修改如下:

public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute("MyRoute", "{controller}/{action}",
new { controller = "Home", action = "Index" });
}
我們提供了兩個默認值,這個時候運行程序就不會報錯了。因為此時"http://localhost:<port>/"里面既沒有controller這個segment,也沒有action這個segment。但是我們定義的它們的默認值會被使用。這個路由可以匹配的URL如下所示:
可以做如下測試:

[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架構:

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變量,如下所示:

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里面,如下所示:

public ViewResult CustomVariable()
{
ViewBag.CustomVariable = RouteData.Values["id"];
return View();
}
添加一個視圖CustomVariable.cshtml將id的值顯示出來,如下:

@{
ViewBag.Title = "CustomVariable";
}
<h2>Variable: @ViewBag.CustomVariable</h2>
下面是針對上面路由的單元測試,如下:

[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方法以至於它有一個匹配的參數,如下:

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本身就恰好包含了默認值。
對應測試的代碼如下:

[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的數量是沒有上限的。對應的單元測試如下:

[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類,代碼如下:

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。具體的使用在上面的定義的路由里面已經有了,定義一個路由如下:

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,如下所示:

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") }。
對應的單元測試如下:

[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接口。下面的示例實現了對游覽器的約束,當然實際情況不提倡限制瀏覽器的。這里僅僅為了說明功能。代碼如下:

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方法,用來實現路由系統的約束是否被滿足。如下:

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文件。
代碼如下:

<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(這里有個約定:必須放在方法體的最上面)。接着就可以為具體的文件路徑定義路由了,如下所示:

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,定義如下路由:

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,如下所示:

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並匹配上,那后面的路由就不會進行匹配了或者說是評估。
好了,今天的筆記到這里,內容比較多,希望對跟我一樣的同學有點幫助。
晚安!