《Pro ASP.NET MVC 3 Framework》學習筆記之二十六【Controller擴展】


本章內容分為兩個部分,第一部分:介紹關於controllers工作原理的高級功能,探究從請求到action方法執行的整個請求處理管道的組成部分並闡釋控制這個過程的不同的方式;第二部分:介紹兩種特殊的控制器,分別是:無會話(sessionless)控制器,異步(asynchronous)控制器.這些能夠增進服務器的處理能力。這部分會闡釋如何創建和使用它們,並且會說明在什么情況下使用它們。

請求處理管道的組成(Request Processing Pipeline Components)


我們的焦點放在第一部分的Controller Factory和Action Invoker上面,其實從命名上看已經能夠了解它們的用途了。具體如下:
控制器工廠(Controller Factory):創建一個用來服務請求的控制器的實例。
Action調用(Action Invoker):找到並調用在Controller類里面的Action方法。
MVC框架包含了對這些組件的默認實現,下面會進行介紹。

創建一個自定義的控制器工廠(Defining a Custom Controller Factory)

Controller工廠是定義在IControllerFactory接口里面的,如下:

View Code
using System.Web.Routing; 
using System.Web.SessionState;

namespace System.Web.Mvc
{
public interface IControllerFactory
{

IController CreateController(RequestContext requestContext, string controllerName);
SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext,
string controllerName);
void ReleaseController(IController controller);
}
}

這個接口里面最重要的方法是CreateController,當需要一個控制器來處理請求的時候,MVC框架會調用它來創建一個控制器的實例。

其實不推薦創建自定義的控制器,其中的一個原因是:尋找在Web應用程序里面的控制器類並實例化它們是非常復雜的。下面是例子是一個簡單的演示,該控制器工廠僅僅支持兩個控制器FirstController和SecondController.如下所示:

View Code
//這部分是CustomControllerFactory代碼
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.SessionState;
using ControllerExtensibility.Controllers;

namespace ControllerExtensibility.Infrastructure
{
public class CustomControllerFactory : IControllerFactory
{
public IController CreateController(RequestContext requestContext, string controllerName)
{
Type targetType = null;
switch (controllerName)
{
case "Home":
requestContext.RouteData.Values["controller"] = "First";
targetType = typeof(FirstController);
break;
case "First":
targetType = typeof(FirstController);
break;
case "Second":
targetType = typeof(SecondController);
break;
default:
break;
}
return targetType == null ? null : (IController)Activator.CreateInstance(targetType);
}

public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
{
return SessionStateBehavior.Default;
}

public void ReleaseController(IController controller)
{
IDisposable disposable = controller as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
}
}
}

//FirstController代碼
namespace ControllerExtensibility.Controllers
{
public class FirstController : Controller
{
//
// GET: /First/

public ActionResult Index()
{
ViewBag.Message = "Hello from the FirstController class";
return View();
}

}
}

//SecondController代碼
namespace ControllerExtensibility.Controllers
{
public class SecondController : Controller
{
//
// GET: /Second/

public ActionResult Index()
{
ViewBag.Message = "Hello from SecondController class";
return View();
}

}
}

//兩個Controller對應的View代碼如下:
@{
ViewBag.Title = "Index";
}
<h2>
This is the second controller view</h2>
Message:@ViewBag.Message

@{
ViewBag.Title = "Index";
}

<h2>This is the first controller view</h2>
Message:@ViewBag.Message

CreateController方法的目的是創建一個能夠處理請求的控制器實例,工廠如何處理的過程是完全開放的。到目前為止,我們從書里面的例子看到的約定是存在的,因為這種方式正是默認控制器的工作方式,在我們完成了自定義工廠以后會覆蓋默認的工廠,在我們上面的例子中,忽略了所有的約定,而是實現我們自己的邏輯。這是非常奇怪的事情,但是也能反映MVC框架提供的完全的靈活性。

如果我們收到一個請求,controller的值是First或者Second,我們創建FirstController類或者SecondController類的實例。創建實例使用的是System.Activator類,它通過targetType的類型創建一個對象的實例,然后轉換為IController類型。例如:(IController)Activator.CreateInstance(targetType); 當我們收到一個controller值是Home的請求,我們也將它映射到了FirstController類,當然這也非常奇怪的,卻能夠說明請求和controller之間的映射僅僅是控制器工廠(controller factory)的職責,但是我們不能說映射請求到視圖。

MVC框架是根據routing data里面controller的值來選擇視圖的。例如,如果我們想映射一個對Home控制器的請求到First控制器的實例,我們需要改變請求里面controller的值。像這樣:requestContext.RouteData.Values["controller"] = "First";

所以,控制器工廠不僅有匹配請求到控制器的職責,而且能夠改變請求並改變請求處理管道后續步驟的行為。這是一個強有力的而且是MVC框架關鍵性的組件。下面介紹在IControllerFactory接口里面的另外兩個方法:

a.GetControllerSessionBehavior:被MVC用來決定是否應該為控制器維護一個會話數據。
b.ReleaseController:當一個控制器對象不在被需要的時候調用該方法。在我們實現里面是檢查這個類是否實現IDisposable接口,如果實現了,就可以調用Dispose方法來釋放不在使用的資源。

注冊自定義的控制器工廠(Registering a Custom Controller Factory)

我們通過ControllerBuilder類告訴MVC框架使用自己的控制器工廠。如下所示:

View Code
protected void Application_Start() 
{
AreaRegistration.RegisterAllAreas();

ControllerBuilder.Current.SetControllerFactory(new CustomControllerFactory());

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

使用內置控制器工廠(Working with the Built-In Controller Factory)

對大多數應用程序,使用內置的控制器工廠類DefaultControllerFactory已經足夠了。當它接收到一個來自路由系統的請求時,這個工廠會在routing data里面尋找controller屬性的值,並試圖在web應用程序里面找到一個滿足下面條件的類:

a.這個類必須是Public   b.這個類是能夠實例化對象的(不是抽象的)  c.這個類不能帶泛型參數  d.類的名字必須以Controller結尾  e.必須實現IController接口

DefaultControllerFactory類維護着這些類的一個列表,以致於它不用在每一次請求到達時都去執行對每一個類的查找。如果找到了適合的類就會使用控制器激活器(controller activator)創建一個實例,這時控制器的工作就完成了。如果沒有匹配的控制器,那么這個請求就不能被進一步處理。

注意一下DefaultControllerFactory是如何遵循"約定勝於配置"模式的——你不需要在配置文件里面注冊控制器,因為這個工廠會為我們找到他們。我們只要創建一個滿足工廠尋找條件的類就行了。

如果我們想創建自定義控制器工廠的行為,可以配置默認工廠的設置或重寫一些方法。通過這種方式,能夠創建有用的符合"約定勝於配置"的行為並且不需要重復創建。下面會展示裁剪控制器創建的不同方式。

指定優先的命名空間(Prioritizing Namespaces)

在前面的章節里面我們知道了如何在創建路由的時候指定一個或多個優先的命名空間。這是為了解決具有相同名字並且不在同一命名空間下的控制器模棱兩可的問題。其實在前面章節用到指定優先的命名空間實際上是DefaultControllerFactory處理的。如果我們的應用程序有很多的路由,指定全局的優先命名空間是非常方便的,這樣可以應用所有的路由。如下所示:

View Code
protected void Application_Start() 
{
AreaRegistration.RegisterAllAreas(); ControllerBuilder.Current.DefaultNamespaces.Add("MyControllerNamespace");
ControllerBuilder.Current.DefaultNamespaces.Add("MyProject.*");

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

可見,我們是用靜態的ControllerBuilder.Current.DefaultNamespaces.Add方法添加優先的命名空間。我們添加的順序並不代表任何對命名空間的查找順序,所有的默認命名空間將被用來搜尋候選的Controller類,並且當我們直接執行在路由里面定義的相同的任務時,會因為重復引發異常。

Tips:全局指定的優先命名空間會被我們在具體定義路由時指定的優先命名空間所覆蓋。
如果控制器工廠不能在我們指定的命名空間里面找到適合的控制器,會接着去匹配其他的。上面指定時有一個使用"*",這個表示匹配以MyProject開頭的所有子命名空間,注意這里的"*"並不是正則表達式,而且這里也不能使用除"*"以外的正則符號。

定制DefaultControllerFactory控制器的創建方式

有很多種方式可以定制DefaultControllerFactory類創建控制器對象,這樣做的一個最常見的理由是添加對DI(依賴注入)的支持。可以采取很多種方式來這樣做,最合適的方式取決於在應用程序的其他的地方如何使用DI。可以使用依賴解析器創建控制器,這個在最開始的幾章里面有介紹過使用NinjectDependencyResolver。

還有一種方式:使用控制器激活器。如下所示:

View Code
using System.Web.Routing; 

namespace System.Web.Mvc
{
public interface IControllerActivator
{
IController Create(RequestContext requestContext, Type controllerType);
}
}

實例化上面的接口,如下:

View Code
namespace ControllerExtensibility.Infrastructure
{
public class CustomControllerActivator : IControllerActivator
{
public IController Create(System.Web.Routing.RequestContext requestContext, Type controllerType)
{
if (controllerType == typeof(FirstController))
{
controllerType = typeof(SecondController);
}
return DependencyResolver.Current.GetService(controllerType) as IController;
}
}
}

我們的實現是傳遞請求給了依賴解析器,當然除非它們是FirstController類型。在那種情下,我們請求一個SecondController類的實例。

IControllerActivator接口僅僅在我們也使用了依賴解析器時被使用,這是因為DefaultControllerFactory類通過調用IDependencyResolver.GetService方法來尋找一個控制器激活器。所以,我們需要使用依賴解析器直接注冊激活器,正如使用NinjectDependencyResolver類的AddBindings方法。注冊一個控制器激活器的示例如下:

View Code
private void AddBindings() 
{
// put bindings here
Bind<IControllerActivator>().To<CustomControllerActivator>();
}

依靠依賴解析器創建控制器是非常簡單的,然而如果我們想攔截和操控請求,那么使用控制器激活器就是一個非常有用並且適合的功能。

重寫DefaultControllerFactory方法(Overriding DefaultControllerFactory Methods)

可以通過重寫DefaultControllerFactory類的方法來定制控制器的創建,能夠重寫的方法有:
CreateController(返回IController類型):是IControllerFactory接口CreateController方法的實現,默認情況下,這個方法調用GetControllerType決定哪一種類型應該被實例化,然后通過傳遞GetControllerInstance方法的結果獲取一個控制器對象。
GetControllerType(返回Type類型):映射請求到控制器的類型
GetControllerInstance(返回IController類型):創建一個具體類型的實例

創建一個自定義的Action調用者(Creating a Custom Action Invoker)

一旦控制器工廠創建了一個類的實例,MVC框架需要一種能夠調用這個實例上Action的方式。如果我們的控制器是從Controller類派生的,那么調用Action的是action調用者的職責。如果是通過直接實現IController接口來創建控制器,那么我們就直接負責調用action了,下面是IActionInvoker接口的代碼:

View Code
namespace System.Web.Mvc 
{
public interface IActionInvoker
{
bool InvokeAction(ControllerContext controllerContext, string actionName);
}
}

接口僅有一個成員:InvokerAction。返回值是bool,true表示action已經找到並且被調用,false表示controller沒有匹配的action。注意這里的描述並沒有使用method方法,而是使用的action,因為action跟method之間的關聯是嚴格可選的。下面是一個自定義的Action調用者:

View Code
namespace ControllerExtensibility.Infrastructure
{
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調用者不關心controller類里面的方法,實際上,它只處理action本身。如果請求是針對Index Action的,那么調用者就會寫一條信息到Response。如果不是則返回false,這樣會造成404錯誤展示給用戶。關聯一個控制器的action調用者是通過Controller.ActionInvoker屬性獲得的。這意味着在同一個應用程序里面的不同控制器能夠使用不同的action調用者。下面展示了一個使用action調用者的例子:

View Code
using ControllerExtensibility.Infrastructure;

namespace ControllerExtensibility.Controllers
{
public class CustomActionInvokerController : Controller
{
public CustomActionInvokerController()
{
ActionInvoker = new CustomActionInvoker();
}
}
}

在上面這個controller里面沒有action方法,它依靠action調用者來處理請求。不建議實現自己的action調用者,因為內置的支持有非常有用的功能。

使用內置的Action調用者(Using the Built-In Action Invoker)

ControllerActionInvoker類是內置的action調用者,具有匹配請求到action的非常復雜精細的技術。不像我們在前面的實現,默認的action調用者在方法上操作,能夠作為action的方法必須滿足一下條件:(ps:到這里應該明白為什么前面會說action和method在MVC里表述是不同的吧,呵呵)

a.方法必須是Public   b.方法不能是static的   c.方法不能是System.Web.Mvc.Controller類或它的基類里面已經存在的  d.方法名不能特殊

前面兩個條件很容易理解,第三個條件是指像ToString()和GetHashCode()要排除,因為這些是實現了IController接口的,這是容易意識到的,因為我們不想暴露控制器的內部運行原理。最后一個條件的意思是構造器,屬性,事件訪問器要排除。實際上,沒有來自 System.Reflection.MethodBase類並具有IsSpecialName標志的類成員會被用來處理一個action。

注:具有泛型參數的方法滿足上面四個條件,但是如果我們視圖調用這樣的一個方法來處理請求,MVC框架會拋異常。
默認情況下,ControllerActionInvoker會尋找具有與被請求的action相同的名字。

例如,如果路由系統產生的action的值是Index,那么ControllerActionInvoker會尋找名為Index並滿足四個條件的方法。如果找到了這樣一個方法,就會調用它來處理請求。

使用自定義的Action命名(Using a Custom Action Name)

通常情況下,action方法的名字決定了它說表現的action,Index action方法為針對Index action的請求服務。我們能夠通過ActionName屬性重寫這種行為,如下所示:

View Code
using System.Web.Mvc;

namespace ActionInvokers.Controllers
{
public class HomeController : Controller
{
[ActionName("Index")]
public ActionResult MyAction()
{
return View();
}

}
}

上面的例子應用了ActionName這個特性將MyAction重寫為Index,這意味着MyAction方法不在用來針對MyAction的action進行服務,而是對Index的action進行服務。要重寫ActionName一般會是以下兩個原因:

a.接收一個不合法的C#方法名。如[ActionName("User-Registration")]
b.如果想擁有兩個不同的C#方法接收同一套參數並處理同樣的action名,但是為了響應不同的HTTP請求類型(HttpGet/HttpPost),我們能夠給定方法不同的名字來滿足編譯,但是使用[ActionName]映射兩個相同的action名。

一個比較奇怪的地方是,當我們在MyAction上右鍵創建視圖時,彈出的對話框依然默認顯示的是MyAction,如圖:

使用Action方法選擇(Using Action Method Selection)

一個控制器包含多個同名的action是非常常見的情形,因為不同的參數或者使用【ActionName】,所以導致多個方法表示同樣的action.這種情形下,MVC框架需要一些輔助選擇合適的action來處理請求,把這種機制成為:action方法選擇(action method selection).它允許我們定義一個action將要處理的多種請求。其實這個在最開始的那個SportsStore項目里面已經有了展示。對action方法設置【HttpPost】或【HttpGet】,讓同名的action處理不同的請求。如:

View Code
... 
[HttpPost]
public ViewResult Checkout(Cart cart, ShippingDetails shippingDetails) {
// 方法主體
}

public ViewResult Checkout() {
// 方法主體
}
...

action調用者使用一個action方法選擇器消除了在選擇一個action時模棱兩可的情況。還有一個【NonAction】特性表示該方法永遠不會被當中action使用,這個特性可以防止我們暴露controller類的工作原理。當然,這些方法應該是private的。當然如果方法是Public而又不想被當作action使用,就可以使用【NonAction】特性來實現。

創建自定義的Action方法選擇器(Creating a Custom Action Method Selector)

action方法選擇器是從ActionMethodSelectorAttribute類派生的,如下所示:

View Code
using System; 
using System.Reflection;

namespace System.Web.Mvc
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public abstract class ActionMethodSelectorAttribute : Attribute {
public abstract bool IsValidForRequest(ControllerContext controllerContext,
MethodInfo methodInfo);
}
}

ActionMethodSelectorAttribute是抽象的並定義了一個抽象方法:IsValidForRequest。參數ControllerContext讓我們獲取請求的信息,MethodInfo對象獲取應用了選擇器的方法的信息。如果方法能夠處理請求則返回true,否則返回false。下面是一個自定義的action方法選擇器:

View Code
using System.Web.Mvc;
using System.Reflection;

namespace ActionInvokers.Infrastructure
{
public class LocalAttribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
return controllerContext.HttpContext.Request.IsLocal;
}
}
}

這個選擇器的作用是當請求源是本地機器時返回true。可以這樣使用它,如下:

View Code
... 
[ActionName("Index")]
public ActionResult FirstMethod()
{
return View((object)"Message from FirstMethod");
}

[Local]
[ActionName("Index")]
public ActionResult SecondMethod()
{
return View((object)"Message from SecondMethod");
}
...

當controller收到一個針對Index action的請求,Local選擇器會進行評估,如果滿足條件(請求源是本地機器),那么第二個action方法會被調用,否則調用第一個。
不建議使用action選擇器作為一種安全檢查,除非我們想調試。

Action方法選擇器消除歧義的處理過程

Action調用者是從一個滿足action四個條件的controller里面的可能的方法列表開始處理過程,步驟如下:

a.調用者根據名字會丟棄不滿足的方法
b.調用者會丟棄任何具有action方法選擇器的特性(attribute)(該特性對當前請求返回false)的方法
c.如果剛好有一個action保留下來,那么這個方法會被使用。如果有多個保留下來,會拋出異常,因為這樣沒有消除歧義。
d.如果一個方法都沒有保留下來,那么調用者會繼續進行尋找不使用選擇器的方法。如果剛好有一個保留下來,那么直接使用,如果是多個,則拋異常。

處理未知的Action(Handling Unknown Actions)

如果action調用者不能找到一個action方法調用,它會返回false。發生這種情況時,Controller類會調用HandleUnknownAction方法,默認情況下,這個方法返回404-NOT Found到客戶端。這是一個controller能夠做的最有意義的事情了,當然我們能夠選擇重寫controller類里面的這個方法來做些比較特別的事情。下面是一個重寫的例子:

View Code
using System.Web.Mvc;

namespace ActionInvokers.Controllers
{
public class HomeController : Controller
{
[ActionName("Index")]
public ActionResult MyAction()
{
return View();
}

protected override void HandleUnknownAction(string actionName)
{
Response.Write(string.Format("You request the {0} action", actionName));
}
}
}

使用Action方法選擇器支持REST服務(Using Action Method Selectors to Support REST Services)

最近這些年,越來越多的開發者選擇使用REST方式代替SOAP實現Web服務。通過聯合一個URL和HTTP方法來指定一個REST操作,URL具有應用程序的具體含義。例如有這樣一個URL:/Staff/1,指向在staff數據庫的第一條記錄。不同的HTTP方法代表在這個記錄上執行的不同操作。Get請求檢索數據,Post請求修改數據,DELETE刪除數據等等。我們可以通過聯合ActionName特性和action方法選擇器提供REST支持。支持REST Web服務:

View Code
public class StaffController : Controller 
{
[HttpGet]
[ActionName("Staff")]
public ActionResult StaffGet(int id)
{
//
}

[HttpPost]
[ActionName("Staff")]
public ActionResult StaffModify(int id, StaffMember person)
{
//
}

[HttpDelete]
[ActionName("Staff")]
public ActionResult StaffDelete(int id)
{
//
}
}

假如我們添加一個路由實體到全局里面如下:

View Code
public static void RegisterRoutes(RouteCollection routes) 
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(null, "Staff/{id}",
new { controller = "Staff", action = "Staff" },
new { id = @"\d+" /* Require ID to be numeric */ });

routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}

現在每個StaffMember能表單的URL唯一編址,並且一個客戶端能夠給使用GET,POST,和DELETE HTTP方法對這些地址執行操作。這被成為RESTful API

重寫HTTP方法(Overriding HTTP Methods)

REST風格的API是偉大的,只要我們所有的客戶端能夠完全使用HTTP方法,服務端用.C#,JAVA,Ruby都可以。

在主流的瀏覽器使用Javascript AJAX調用也不會有什么困難。不幸的是,一些主流的Web技術僅僅支持POST和GET方法。這包括HTML表單和Flash應用程序。另外一些防火牆也只允許GET和POST兩種請求通過。為了彌補這個缺點,在我們包含了必須的HTTP方法地方有一個約定,即使請求不能使用某一個方法,它也會被包含在請求的鍵值對里面,key是X-HTTP-Method-Override,值是我們期望的方法。這個key能夠作為HTTP請求頭(HTTP Header),HTML表單隱藏域,Querystring的一部分被指定。

例如,一個POST請求包含了一個請求頭,這個請求頭指定了MVC程序即使收到一個DELETE請求也應該執行。在這背后的想法是大多數web技術允許設置任意的頭或表單輸入,即使它們不支持同一種HTTP方法。

在MVC HTML表單重寫HTTP方法(Overriding HTTP Methods in an MVC HTML Form)

MVC框架在視圖和控制器提供了對HTTP方法重寫的支持,視圖支持意味着我們能夠使用來自HTML表單的REST風格API;控制器支持意味着MVC框架會處理包含X-HTTP-Method-Override屬性的請求。下面是一個在視圖重寫HTTP方法的示例:

View Code
... 
@using (Html.BeginForm())
{
@Html.HttpMethodOverride(HttpVerbs.Delete)
<input type="submit" value="Delete" />
}
...

Html.HttpMethodOverride輔助方法讓我們可以指定想要用來處理請求的HTTP方法,上面的示例指定的是DELETE方法,這個方法會添加一個Input到表單,如:
<input name="X-HTTP-Method-Override" type="hidden" value="DELETE" />
MVC框架在處理表單時會尋找這個Input元素,並且經過Request.GetHttpMethodOverride方法獲取后這個重寫的HTTP方法是可用的。MVC框架僅僅對POST請求這次HTTP方法重寫。

使用專門的控制器改善性能(Improving Performance with Specialized Controllers)

MVC框架提供了兩種專門的控制器來改善MVC Web應用程序。像所有的優化一樣,在功能和性能之間進行折衷處理。

使用無會話的控制器(Using Sessionless Controllers)

默認情況下,控制器支持Session狀態,這樣能夠跨請求保存數據。創建和維護Session狀態就是必須涉及的處理過程,數據要被保存和檢索,Session本身也要管理,讓它們在合適的時間能夠過期。Session的數據會消耗服務器的內存或空間並且需要跨多個Web服務器同步數據使得我們在服務器集群上運行程序更加艱難。

為了簡化Session狀態,ASP.NET對給定的Session一次僅僅處理一個請求,如果客戶端制造多個重疊的請求,它們講話排隊還次序被服務器處理。這樣做的好處是我們不用擔心多個請求修改相同的數據,不好的地方是不能獲得一個較好的請求吞吐量。

不是所有的控制器都使用Session狀態功能,在不是用Session狀態的情況下,我們可以通過避免對Session狀態的維護來改善程序的性能。這時就可以使用無會話的控制器,除了兩個地方跟常規的控制器不同以外,其他都一樣。兩個不一樣的地方是:a.MVC框架不會加載和持久化Session狀態當他們被用來處理請求的時候  b.重疊的請求同時被處理

在自定義的IControllerFactory里面管理Session狀態(Managing Session State in a Custom IControllerFactory)

IControllerFactory接口里面包含了一個GetControllerSessionBehavior的方法,它從SessionStateBehavior返回一個枚舉值,枚舉里面包含了控制Session狀態的四個值:Default,Required,ReadOnly,Disabled。實現了IControllerFactory接口的控制器工廠可以通過GetControllerSessionBehavior方法里面的返回一個SessionStateBehavior枚舉值來設置Session狀態。示例如下:

View Code
public SessionStateBehavior GetControllerSessionBehavior( 
RequestContext requestContext, string controllerName)
{
switch (controllerName)
{
case "Home":
return SessionStateBehavior.ReadOnly;
case "Other":
return SessionStateBehavior.Required;
default:
return SessionStateBehavior.Default;
}
}

使用DefaultControllerFactory管理Session狀態(Managing Session State Using DefaultControllerFactory)
當我們使用內置的控制器工廠時,可以通過使用SessionState特性來控制Session狀態。如下所示:

View Code
using System.Web.Mvc;
using System.Web.SessionState;

namespace SpecialControllers.Controllers
{
[SessionState(SessionStateBehavior.Disabled)]
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
}
}

這個時候如果我們視圖設置一個Session的值像這樣:Session["Message"] = "Hello";在視圖里面添加Message: @Session["Message"] 則會拋出異常。如果設置為ReadOnly那么就只能讀取其他controller設置的Session的值。當然如果我們僅僅是為了向View傳遞數據,那完全可以使用ViewBag或者是ViewData。

使用異步控制器(Using Asynchronous Controllers)

ASP.NET平台的核心維護着一個處理客戶端請求的.NET線程池——工作者線程池(worker thread pool),里面的線程稱為工作者線程(worker threads).當接收一個請求時,就會從線程池里面取一個線程並分配處理這個請求的任務。當請求被處理完畢,線程又被放回到線程池里面,以致於我們可以接着使用處理其他的請求。
使用線程池有兩個重要的益處:

a.通過線程的重復使用,我們避免一些創建新的線程的額外的開銷。
b.擁有固定數量可用的線程能避免服務器同時處理超越其最大請求范圍的請求

當請求能夠在短時間內被處理時,線程池會發揮最大的效用。這也是符合大多數MVC程序的(大多數都能在很短的時間被處理),然而,如果有控制器依賴其他的服務器並且需要花費很長的時間才能完成,這樣可以能會出現這種情況:所有的線程都因為等待其他系統完成它們的工作而被占用。其實這些等待的時間本來可以用來做更多的事情。解決這個問題的方法就是使用異步控制器,這回改善應用程序的整體性能,但是對異步操作的執行是沒有什么益處的。

注:異步控制器僅僅對I/O,網絡綁定,CPU不密集的的action有用。我們正在視圖使用異步控制器解決的問題是池模型和正在被處理的請求類型不匹配。線程池企圖確保每一個請求獲得恰當的服務器資源片,但卻被我們弄成了一套線程都無事可做。如果我們使用另外的后台線程來處理CPU使用密集的action,那么又淡化了服務器資源能跨越多個同時請求的效果。

創建一個異步控制器(Creating an Asynchronous Controller)

下面是一個耗時請求的示例,如下:

View Code
using System.Web.Mvc;
using SpecialControllers.Models;

namespace SpecialControllers.Controllers
{
public class RemoteDataController : Controller
{
public ActionResult Data()
{
RemoteService service = new RemoteService();
string data = service.GetRemoteData();
return View((object)data);
}

}
}

//Model里面的代碼
using System.Threading;

namespace SpecialControllers.Models
{
public class RemoteService
{
public string GetRemoteData()
{
Thread.Sleep(10000);
return "Hello from the other side of the world";
}
}
}

//視圖代碼
@model string
@{
ViewBag.Title = "Data";
}
Data:@Model

看了這個例子反映的問題,接着我們通過創建異步控制器來解決它。

創建異步控制器有兩種方式:

a.實現 System.Web.Mvc.Async.IAsyncController interface接口,這里不打算使用這種方式,因為這需要非常多關於.NET並行編程的闡釋,這里我們只專注於MVC本身。

b.從System.Web.Mvc.AsyncController(已經實現上面的接口)派生。其實這里類似於前面定制控制器的兩種方式,一種是實現接口,另一種是從已經實現了接口的類派生我們自己的控制器類。

一個異步控制器的示例如下:

View Code
using SpecialControllers.Models;
using System.Threading.Tasks;

namespace SpecialControllers.Controllers
{
public class RemoteDataController : AsyncController
{
//public ActionResult Data()
//{
// RemoteService service = new RemoteService();
// string data = service.GetRemoteData();
// return View((object)data);
//}
public void DataAsync()
{
AsyncManager.OutstandingOperations.Increment();
Task.Factory.StartNew(() =>
{
//創建一個模型對象
RemoteService service = new RemoteService();
//調用I/O綁定,耗時的方法
string data = service.GetRemoteData();
AsyncManager.Parameters["data"] = data;
AsyncManager.OutstandingOperations.Decrement();
});
}

public ActionResult DataCompleted(string data)
{
return View((object)data);
}
}
}

創建異步和已完成方法(Creating the Async and Completed Methods)

異步action方法是成對出現的,第一個是<Action>Async(像上面的DataAsync方法)
在<Action>Async里,我們可以創建並開始異步操作。當一個針對異步控制器的請求到來時,action調用者就知道action方法具有兩部分並調用<Action>Async方法。在我們的例子中,當導航到/RemoteData/Data URL時,會調用DataAsync.異步方法一般返回類型是void,調用的<Action>Async方法會使用線程來執行。

方法的另一部分是:<Action>Completed.例子里面是DataCompleted.這個方法是在所有的異步操作完成后調用並且准備返回一個結果到客戶端。

啟動異步任務(Starting Asynchronous Tasks)

在異步方法里面創建我們所有的異步任務並告訴MVC框架我們啟動了多少操作,下面對DataAsync方法進行分析:
首先需要告訴MVC框架啟動了一個異步操作,這是通過AsyncManager類調用 OutstandingOperations.Increment方法完成。

其次我們要實際地創建並啟動異步操作。.NET里面有多種方式來實現異步活動,這里選擇的是.NET4引入的Task Parallel Library (TPL)。

下面我看下在一個異步action里面有多個異步任務的例子:

View Code
public void DataAsync() 
{
AsyncManager.OutstandingOperations.Increment(3);
Task.Factory.StartNew(() => {
// ...perform async operation here
AsyncManager.OutstandingOperations.Decrement();
});

Task.Factory.StartNew(() => {
// ...perform async operation here
AsyncManager.OutstandingOperations.Decrement();
});

Task.Factory.StartNew(() => {
// ...perform async operation here AsyncManager.OutstandingOperations.Decrement();
});
}

上面有3個異步任務,在最開始指定3個任務AsyncManager.OutstandingOperations.Increment(3); 然后在每一個任務的結尾處調用Decrement();

完善異步任務(Finishing Asynchronous Tasks)

MVC框架無從得知我們在<Action>Async方法里從事的內容,所以我們需要給它一些需要的信息,這也是為什么需要在異步方法的結尾調用AsyncManager。Increment()能夠告訴MVC框架我們啟動了多少任務,在每一個任務完成后,數量減一(Decrement).當未解決任務的數量為0時,MVC框架知道我們想要異步方法做的所有工作已經完成,並且准備返回結果給客戶端。這時會指派一個來自線程池的線程通過調用<Action>Completed方法完成對請求的處理。

從Async向Completed方法傳遞參數(Passing Parameters from the Async to the Completed Method)

MVC框架會實現通常的邏輯,為來自請求的異步方法創建參數,但是我們要負責為Completed方法創建參數。方式是使用Parameters鍵值對集合,示例里面的 AsyncManager.Parameters["data"] = data;給data參數賦值了,在public ActionResult DataCompleted(string data){}使用,我們要確保AsyncManager.Parameters里面的參數類型跟Completed方法里參數類型一致。如果不一致不會拋異常,而是參數的值會使用默認值(例如null)。

超時管理(Managing Timeouts)

默認情況下,MVC框架給定45秒來完成所有的異步操作,如果45秒內沒有完成所有操作,請求會被放棄,並拋出超時異常。超時異常處理的方式跟其他異常一樣,但如果想在特殊的情形下處理超時,可以重寫OnException方法。如下所示:

View Code
protected override void OnException(ExceptionContext filterContext) 
{
if (filterContext.Exception is System.TimeoutException)
{
filterContext.Result = RedirectToAction("TryAgainLater");
filterContext.ExceptionHandled = true;
}
}

上面例子在超時異常處理是重定向頁面。我們也可以通過AsyncTimeout特性來修改超時的時間區間。如
[AsyncTimeout(10000)] //10秒
public void DataAsync() {...}
也可以使用NoAsyncTimeout來完全忽略超時,這樣做是不好的。因為可能某個操作失敗了,也有可能用戶已經放棄等待了。

放棄異步操作(Aborting Asynchronous Operations)

可以在我們的任務里面通過調用AsyncManager.Finish方法來放棄異步操作。這也就是告訴MVC框架我們准備立刻調用<Action>Completed方法並且會忽略任何未解決的操作,任何已經使用AsyncManager.Parameters集合設置的參數值會傳遞給Completed方法,在Finish方法被調用的時候沒有被設置值的參數會使用其類型的默認值(object類型是null,數值類型是0等等)
注:調用AsyncManager.Finish方法並沒有終止任何異步操作,而且它們仍然在運行。它們會正常完成,但是創建的參數值會被忽略。

使用.NET異步編程模型(Using the .NET Asynchronous Programming Pattern)

.NET框架支持不同的異步編程方式,一種是異步編程模型APM(Asynchronous Programming Model),有很多有用的.NET類遵循APM,可以通過方法名Begin<Operation>和End<Operation>很容易發現。我們通過調用Begion方法來啟動異步操作,傳遞一個在操作完成時的回調,傳遞給回調的參數是一個對IAsyncResult接口的實現,這個會傳遞給End方法獲取操作結果。這種方式不是很直接,但是可以使用Lambda表達式來簡化代碼。下面的示例是關於APM方式的:

View Code
using System.Web.Mvc;
using SpecialControllers.Models;
using System.Threading.Tasks;
using System.Net;
using System.IO;

namespace SpecialControllers.Controllers
{
public class RemoteDataController : AsyncController
{
//public ActionResult Data()
//{
// RemoteService service = new RemoteService();
// string data = service.GetRemoteData();
// return View((object)data);
//}
//public void DataAsync()
//{
// AsyncManager.OutstandingOperations.Increment();
// Task.Factory.StartNew(() =>
// {
////創建一個模型對象
// RemoteService service = new RemoteService();
////調用I/O綁定,耗時的方法
// string data = service.GetRemoteData();
// AsyncManager.Parameters["data"] = data;
// AsyncManager.OutstandingOperations.Decrement();
// });
//}

//public ActionResult DataCompleted(string data)
//{
// return View((object)data);
//}

public void PageAsync()
{
AsyncManager.OutstandingOperations.Increment();
WebRequest req = WebRequest.Create("http://www.cnblogs.com");
req.BeginGetResponse((IAsyncResult ias) =>
{
WebResponse resp = req.EndGetResponse(ias);
string content = new StreamReader(resp.GetResponseStream()).ReadToEnd();
AsyncManager.Parameters["html"] = content;
AsyncManager.OutstandingOperations.Decrement();
}, null);
}

public ContentResult PageCompleted(string html)
{
return Content(html, "text/html");
}
}
}

//添加Page視圖
@{

ViewBag.Title = "Page";
}

<h2>Page</h2>
//接着運行程序在URL輸入/RemoteData/Page

決定何時使用異步控制器(Deciding When to Use Asynchronous Controllers)

異步控制器相對於通常的控制器要復雜的多,它需要許多額外的代碼並且很難閱讀和維護。異步控制器要保守使用而且僅僅在下面的情況下使用:

a.action是I/O綁定並且不是CPU綁定
b.經常因為線程池耗盡而出問題時
c.我們自己接受創建異步控制器時產生的額外的復雜度
d.我們已經嘗試過相對簡單的解決方案,例如緩存等

上面並不是讓我們懼怕並遠離異步控制器,但是它們的確也用在解決小范圍的問題,而這些問題是大多數應用程序不會遭受的問題,並且引入它們的復雜性很難合理化。

好了,本章的筆記已經完成。
祝大家愉快!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM