MVC的標准流程是請求傳遞給控制器,由控制器action方法操作數據模型,最后交由視圖渲染輸出,這里忽略了兩個細節,就是MVC是如何創建相應控制器實例,又是如何調用控制器action方法的,這就必須講到控制器工廠和action調用器。

控制器工廠
Controller factory負責創建並初始化控制器,控制器工廠實現IControllerFactory接口:
namespace System.Web.Mvc { public interface IControllerFactory { IController CreateController(RequestContext requestContext, string controllerName); SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName); void ReleaseController(IController controller); } }
我們可以從IControllerFactory接口實現自定義的控制器工廠:
public class CustomControllerFactory: IControllerFactory { public IController CreateController(RequestContext requestContext, string controllerName) { Type targetType = null; switch (controllerName) { case "Product": targetType = typeof(ProductController); break; case "Customer": targetType = typeof(CustomerController); break; default: requestContext.RouteData.Values["controller"] = "Product"; targetType = typeof(ProductController); break; } return targetType == null ? null : (IController)DependencyResolver.Current.GetService(targetType); } public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName) { switch (controllerName) { case "Home": return SessionStateBehavior.ReadOnly; case "Product": return SessionStateBehavior.Required; default: return SessionStateBehavior.Default; } } public void ReleaseController(IController controller) { IDisposable disposable = controller as IDisposable; if (disposable != null) { disposable.Dispose(); } } }
這里最重要的方法就是CreateController(),由它根據請求的控制器的名稱直接創建所需控制器的實例,這里是直接硬編碼控制器名稱,當然實際的應用中眾多不應該是這樣操作。CreateController()必須返回一個實現IController的對象,上面例子中最后調用DependencyResolver.Current.GetService()來負責創建相應控制器的實例。
方法GetControllerSessionBehavior()是用於MVC確定是否需要維護會話信息,稍后詳述。方法ReleaseController()用於釋放需要的資源,這里只是單純調用控制器實例的Dispose()方法釋放資源。
要使用自定義的控制器工廠我們還必須在應用啟動時注冊它為當前控制器工廠:
public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); ControllerBuilder.Current.SetControllerFactory(new CustomControllerFactory()); ...
MVC內建控制器工廠DefaultControllerFactory
我們一般不需要自定義控制器工廠而使用MVC默認的DefaultControllerFactory,它根據路徑映射在應用程序中搜索符合這些要求的控制器類:
- 類必須是public
- 必須是實類,不是abstract
- 不能帶泛型參數
- 類名必須以Controller結尾
- 類必須實現IController接口
DefaultControllerFactory維護一個應用程序內符合要求的控制器列表,在請求到達時從列表中搜索相應的控制器。在路徑映射一文中講到搜索控制器時的命名空間優先級,除了在路徑映射中指定命名空間,還可以這樣添加默認優先搜索的命名空間:
ControllerBuilder.Current.DefaultNamespaces.Add("MyControllerNamespace"); ControllerBuilder.Current.DefaultNamespaces.Add("MyProject.*");
通過ControllerBuilder.Current.DefaultNamespaces.Add添加的命名空間比未在此添加的命名空間有更高的優先級,但是用此方法添加的所有命名空間是同等對待的,它們之間不分優先級。
DefaultControllerFactory使用DependencyResolver初始化控制器類,我們可以通過controller activator以DI(Dependency injection)的方式調整DefaultControllerFactory創建控制器類,controller activator用到IControllerActivator接口:
namespace System.Web.Mvc { using System.Web.Routing; public interface IControllerActivator { IController Create(RequestContext requestContext, Type controllerType); } }
創建一個controller activator的實現:
public class CustomControllerActivator : IControllerActivator { public IController Create(RequestContext requestContext, Type controllerType) { if (controllerType == typeof(ProductController)) { controllerType = typeof(CustomerController); } return (IController)DependencyResolver.Current.GetService(controllerType); } }
和自定義控制器工廠類似,我們需要注冊使用它:
ControllerBuilder.Current.SetControllerFactory(new DefaultControllerFactory(new CustomControllerActivator()));
以這種方式可以調整DefaultControllerFactory如何創建控制器類,比完全自定義類工廠更加簡單。實際上我們還可以直接從DefaultControllerFactory擴展自定義的控制器工廠,可以重載DefaultControllerFactory的方法CreateController、
GetControllerType、GetControllerInstance來滿足自己的需要。
自定義action invoker
如果我們的控制器類是直接從IController接口創建而來的,我們需要自己調用action方法,而如果控制器類是從Controller類擴展的,Controller則已經內建支持如何調用action方法。調用action方法用到接口IActionInvoker:
namespace System.Web.Mvc { public interface IActionInvoker { bool InvokeAction(ControllerContext controllerContext, string actionName); } }
從該接口我們可以創建自定義的action invoker:
public class CustomActionInvoker : IActionInvoker { public bool InvokeAction(ControllerContext controllerContext, string actionName) { if (actionName == "Index") { controllerContext.HttpContext. Response.Write("This is output from the Index action"); return true; } else { return false; } } }
這里只是簡單的匹配action的名稱,如果是index則直接輸出結果到響應。我們還必須將它和控制器聯系起來才能使用:
public class ActionInvokerController : Controller { public ActionInvokerController() { this.ActionInvoker = new CustomActionInvoker(); } }
這里在ActionInvokerController的構造函數中指定其ActionInvoker為自定義的ActionInvoker,由它負責處理action的調用。
內建Action invoker
和控制器工廠一樣,MVC提供內建的默認action invoker:ControllerActionInvoker。它負責在控制器內搜索匹配action方法,只有符合以下要求的控制器方法才被認為是action方法:
- 方法必須是public
- 方法必須不是static
- 方法必須不在System.Web.Mvc.Controller或它的任何子類中出現
- 方法名必須不是特殊名,比如不是構造函數、不是屬性、不是事件訪問方法,簡單的說就是不帶IsSpecialName(System.Reflection.MethodBase)標志。
雖然泛型函數比如MyMethod<T>()滿足上面的要求,但是MVC在調用這樣的方法時會報出異常。
默認情況下根據請求查找同名的action方法,但是我們可以這樣自定義action名稱:
[ActionName("Enumerate")] public ViewResult List() { return View("Result", new Result { ControllerName = "Customer", ActionName = "List" }); }
這里通過ActionName特性將Enumerate action映射到List方法,訪問原有的list會得到錯誤。通過這種方式可以實現不符合c#方法名稱的action,比如[ActionName("User-Registration")],還可以將同一個action名稱結合其他特性比如[HttpGet]、[HttpPost]將它們映射到不同的控制器方法上。
除了ActionName特性我們還可以使用NonAction特性指示一個控制器方法不能作為action調用:
[NonAction] public ActionResult MyAction() { return View(); }
這里MyAction雖然是合法的action方法,但是通過[NonAction]將其標識為非action,對它的請求將得到“Resource not found”錯誤。
Action方法選擇器
ActionName、NoActionAction、HttpPost、HttpGet這些特性都是從MethodSelectorAttribute擴展而來,它們統一稱為action方法選擇器,當然我們也可以創建自定義的action方法選擇器:
public class LocalAttribute : ActionMethodSelectorAttribute { public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) { return controllerContext.HttpContext.Request.IsLocal; } }
這里LocalAttribute確認所標識的action方法針對請求來自於本地是可以處理的:
public class HomeController : Controller { public ActionResult Index() { return View("Result", new Result { ControllerName = "Home", ActionName = "Index" }); } [Local] [ActionName("Index")] public ActionResult LocalIndex() { return View("Result", new Result { ControllerName = "Home", ActionName = "LocalIndex" }); } ...
比如這里的Home控制器的Index方法和通過ActionName("Index")指定的LocalIndex方法都對應到Index請求,如果LocalIndex不附加[Local]就會產生重名的action錯誤,通過[Local]就可以將來自於本地的請求映射到LocalIndex上,其他請求則調用Index方法。
未知Action的處理
如果沒有找到對應的Action方法,Controller類調用其HandleUnknownAction方法,默認該方法報404-Not found錯誤。我們可以重載Controller的HandleUnknownAction方法執行其他操作:
protected override void HandleUnknownAction(string actionName) { Response.Write(string.Format("You requested the {0} action",actionName)); }
如果沒有發現相應的action方法,這里直接輸出一條信息到響應。
Sessionless控制器
控制器默認支持會話,在多個客戶端請求間保存會話數據,如果客戶端同時發出多個請求,這些請求必須排隊依次處理,以先后順序修改會話數據,這會影響服務器的並發性能。對於那些不需要會話的場合,我們可以使用sessionless控制器。
IControllerFactory的GetControllerSessionBehavior方法返回一個SessionStateBehavior枚舉,它包含以下枚舉值:
- Default:使用配置文件中HttpContext節定義的默認ASP.NET會話狀態
- Requried:會話狀態可寫可讀
- ReadOnly:會話狀態只可寫
- Disabled:禁用會話狀態
如果我們是直接從IControllerFactory自定義控制器工廠,我們可以針對不同的請求設置不同的會話行為:
public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName) { switch (controllerName) { case "Home": return SessionStateBehavior.ReadOnly; case "Product": return SessionStateBehavior.Required; default: return SessionStateBehavior.Default; } }
如果使用的是默認DefaultControllerFactory控制器工廠,我們可以使用SessionState標識控制器會話行為:
[SessionState(SessionStateBehavior.Disabled)] public class FastController : Controller { public ActionResult Index() { return View("Result", new Result { ControllerName = "Fast ",ActionName = "Index" }); } }
這里將FastController標識未不啟用會話狀態,如果我們在視圖中試圖訪問會話數據比如@Session["Message"]將拋出異常。
異步控制器
ASP.NET維護一個.NET線程池用於處理客戶請求,這些工作線程在完成工作后返回到線程池等待為下一個請求服務。使用線程池可以節約為每個請求創建線程的開銷,通過固定線程數量也避免同時請求數超過服務器的負載能力的情況。一種極端的情況是如果我們請求的資源位於遠程服務器上,線程等待遠程資源被阻塞,造成服務器不能響應新的客戶端請求。我們用下面的例子模仿這種情況:
public class RemoteDataController : Controller { public ActionResult Data() { RemoteService service = new RemoteService(); string data = service.GetRemoteData(); return View((object)data); } }
RemoteData控制器方法Data請求的數據來自於RemoteService:
public class RemoteService { public string GetRemoteData() { Thread.Sleep(2000); return "Hello from the other side of the world"; } }
我們暫停RemoteService的線程2秒模仿請求遠程數據阻塞,如果同時多個請求都訪問/RemoteData/Data,比如造成服務器的響應延遲。
創建異步控制器可以直接從System.Web.Mvc.Async.IAsyncController實現,也可以從 System.Web.Mvc.AsyncController擴展,后者內部實現了IAsyncController接口。這里使用AsyncController為例:
public class RemoteDataController : AsyncController{ public async Task<ActionResult>Data() { string data = await Task<string>.Factory.StartNew(() => { return new RemoteService().GetRemoteData(); }); return View((object)data); } }
異步控制器action方法返回一個Task<ActionResult>對象,方法內部使用await等待訪問遠程數據完成,結果就是請求該action時工作線程不會被阻塞停止,而是返回到線程池相應其他其他請求,在獲取到遠程數據完成后再啟動線程繼續后續工作。
以上為對《Apress Pro ASP.NET MVC 4》第四版相關內容的總結,不詳之處參見原版 http://www.apress.com/9781430242369。
