005_控制器和动作
到達應用程序的每一個請求都是由控制器處理的。但要注意,不要把事務或數據存儲邏輯放到控制器中,也不要生成用戶界面。
在ASP.NET MVC框架中,控制器是含有請求處理邏輯的.NET類。其作用是封裝應用程序邏輯。也就是說,控制器要負責處理輸入請求、執行域模型上的操作,并選擇渲染給用戶的視圖。
控制器的介紹
為了能夠詳細的說明控制器和動作的功能,這里使用“空(Empty)”模板創建一個名為“ControllersAndActions”的新的MVC項目(記得選擇“創建單元測試項目(Create a unit test project)”)。
???????? 在MVC框架中,必須實現System.Web.Mvc命名空間的IController接口。這個接口很簡單,只有唯一的一個方法:Execute,其在請求以控制器類為目標時被調用。MVC框架通過讀取路由數據生成的controller屬性值,便會指定請求的目標是哪一個控制器。
???????? 由于IController接口是一個相當低級的接口,因此必須做大量的工作才能達到預期效果。如下面所示的一個相當簡單的用于演示的控制器類:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing;namespace ControllersAndActions.Controllers {public class BasicController : IController{public void Execute(RequestContext requestContext){string controller = (string)requestContext.RouteData.Values["controller"];string action = (string)requestContext.RouteData.Values["action"];requestContext.HttpContext.Response.Write(string.Format("Controller: {0},Action: {1}", controller, action));}} }???????? 上面代碼僅僅演示了通過與請求相關聯的RouteData對象讀取controller和action變量的值,并將其顯示出來。MVC框架并未指出控制器應該如何處理請求,也就是說可以采用任何方式來處理。需要注意的是,MVC框架并未在這個Basic控制器上強加視圖引擎。如何產生響應是控制器本身要做的事,MVC框架不會對生成響應所用的技術做任何假設。
???????? MVC框架可以無限定制和擴展。我們可以通過實現IController接口,來創建自己的控制器類,根據自己的需求來決定該如何處理請求。也可以通過System.Web.Mvc.Controller類來派生控制器。
???????? 該類提供了三個關鍵特性:
- 動作方法(Action Method):一個控制器的行為被分解為多個方法(而不是只有單一的Execute方法)。每個動作方法被暴露給不同的URL,并通過輸入請求提取的參數進行調用。
- 動作結果(Action Result):可以返回一個描述動作結果的對象(如,渲染一個視圖,或重定向到一個不同的URL或動作方法),然后通過該對象實現目的。這種指定結果和執行它們之間的分離簡化了單元測試。
- 過濾器(Filter):可以把可重用的行為(如認證)封裝成過濾器,然后通過在源代碼中放置一個[Attribute](注解屬性)的辦法,把這種行為標注到一個或多個控制器或動作方法上。
除非已經有了一個非常明確的需求,否則創建控制器最好的辦法是通過Controller類進行派生,這也是Visual Studio創建控制器的默認方式。如下面通過這種方式創建的一個簡單控制器:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc;namespace ControllersAndActions.Controllers {public class DerivedController : Controller{public ActionResult Index(){ViewBag.Message = "Hello from the DerivedController Index method";return View("MyView");}} }Controller基類會實現Execute方法并負責調用動作方法,動作方法名與路由數據中的action的值匹配。
Controller類也連接到Razor視圖系統。上面代碼中返回的View方法的結果,在其中傳遞了希望渲染給客戶端的視圖名。下面是該視圖的內容:
@{ViewBag.Title = "MyView"; }<h2>MyView</h2>Message:@ViewBag.Message?
接收輸入
控制器經常要訪問輸入數據,如查詢字符串值、表單值及路由系統根據輸入URL解析所得到的參數。
而控制器訪問輸入數據的主要途徑有以下三個:
- 通過一組上下文對象(context objects)進行提取;
- 作為參數(Parameters)被傳遞給動作方法而形成的數據;
- 明確地調用框架模型綁定(Model Binding)特性。
下面先重點針對上下文對象和動作方法參數的方式進行介紹,模型綁定的方式將在“模型綁定”的章節中介紹。
1.通過上下文對象獲取數據
當控制器是通過Controller基類派生而來的時候,便得到了一組便利屬性(Convenience Property),可以用來訪問與請求相關的信息。包括Request、Response、RouteData、HttpContexty以及Server。之所以將這些屬性叫做便利屬性,是因為它們每一個都從請求的ControllerContext實例(可以通過Controller.ControllerContext屬性對其進行訪問)接受了不同類型的數據。
下表是一些常見的上下位對象:
| 屬性 | 類型 | 描述 |
| Request.QueryString | NameValueCollection | 隨該請求發送的GET變量 |
| Request.Form | NameValueCollection | 隨該請求發送的POST變量 |
| Request.Cookies | HttpCookieCollection | 由瀏覽器隨該請求發送的Cookies |
| Request.HttpMethod | string | 用于該請求的HTTP方法(動詞,如GET或POST) |
| Request.Headers | NameValueCollection | 隨該請求發送的整個HTTP報頭 |
| Request.Url | Uri | 所請求的URL |
| Request.UserHostAddress | string | 形成該請求的用戶的IP地址 |
| RouteData.Route | RouteBase | 為該請求所選擇RouteTable.Routes條目 |
| RouteData.Values | RouteValueDictionary | 當前路由的參數(從URL或默認值提取) |
| HttpContext.Application | HttpApplicationStateBase | 應用程序狀態庫 |
| HttpContext.Cache | Cache | 應用程序緩存庫 |
| HttpContext.Items | IDictionary | 當前請求的狀態庫 |
| HttpContext.Session | HttpSessionStateBase | 訪問者的會話狀態庫 |
| User | IPrincipal | 已登錄用戶的認證信息 |
| TempData | TempDataDictionary | 為當前用戶存儲的臨時數據項 |
在一個動作方法中,可以用這些上下文(Context)對象的任意一個,來獲取與請求相關的信息,下面這段代碼給出了簡單獲取這些信息的方法:
public ActionResult RenameProduct(){// 訪問上下文對象的各個屬性string userName = User.Identity.Name;string serverName = Server.MachineName;string clientIP = Request.UserHostAddress;DateTime dateStamp = HttpContext.Timestamp;AuditRequest(userName, serverName, clientIP, dateStamp, "Renaming product");// 接收 Request.Form 所遞交的數據string oldProductName = Request.Form["OldName"];string newProductName = Request.Form["NewName"];bool result = AttemptProductRename(oldProductName, newProductName);ViewData["RenameResult"] = result;return View("ProductRenamed");}?
2.使用動作方法參數
使用動作方法參數的方式提取數據比通過上下文對象手工提取更加靈活,且可讀性更強。而且,這種方式還利于單元測試——不需要模仿控制器類的便利屬性。如:
public ActionResult ShowWeatherForecast(){string city = (string)RouteData.Values["city"];DateTime forDate = DateTime.Parse(Request.Form["forDate"]);// …在這里實現天氣預報…return View();}???????? 可以把它重寫成使用參數的形式,如:?
public ActionResult ShowWeatherForecast(string city, DateTime forDate){// …在這里實現天氣預報…return View();}需要注意的是在動作方法中不允許使用out或ref這樣的出參,一是這么做沒有任何意義,而且在ASP.NET MVC中,如果出現這種參數將會直接拋出異常。MVC框架會自動對動作方法的參數進行賦值,這是通過檢查上下文對象來完成的。對參數名的處理不區分大小寫,因此,像上面那樣的“city”的動作方法參數能夠被Request.Form["City"]的值所填充。
- 參數對象實例化
Controller基類使用叫作“值提供器(Value Provider)”和“模型綁定器(Model Binder)”的MVC框架組件來獲取動作方法的參數值。
值提供器:表現一組可用于控制器的數據項。有一組內建的值提供器,它們可以抓取Request.Form、Request.QueryString、Request.Files,以及RouteData.Values的數據項。然后這些值被傳遞給模型綁定器,模型綁定器會嘗試將這些數據映射成動作方法參數的數據類型。
默認的模型綁定器能創建并填充任何.NET類型的對象,包括集合和項目專用的自定義類型。
- 可選參數與強制參數
MVC框架如果找不到引用類型參數(如string或object)的值,動作方法仍然會被調用,但對該參數會使用一個null值。若找不到值類型參數(如int或double)的值,則會拋出一個異常,并且不會調用該動作方法。
值類型參數是強制的,為了使它們可選,可以為其指定一個默認值,或將該參數的類型改為可空(nullable)類型(如int?或DateTime?),這樣,MVC框架在無值可用時會傳遞null值。
引用類型參數是可選的,為了使其為必需的(如以保證傳遞一個非空值),可以把一些代碼添加到該動作方法的頂部,以拒絕null值。如,當值為null時拋出一個ArgumentNullException異常。
- 指定默認參數值
如果希望處理不含動作方法參數值的請求,但又不想在代碼中檢查null值或拋出異常,可以使用C#的可選參數特性來代替。如:
public ActionResult Search(string query = "all", int page = 1){// …處理請求…return View();}這時,如果MVC框架發現無可用的值,則將使用指定的默認值代替。
可選參數可以用于字面類型,字面類型(LiteralType)是不需要用new關鍵字定義的類型,包括string、int和double等。
?
注意:如果一個請求確實包含了一個參數的值,但該值無法轉換為正確的類型,那么框架會傳遞該參數類型的默認值,并在一個名為“ModelState(模型狀態)”的特殊上下文對象中將這個嘗試值注冊為一個驗證錯誤。除非檢查ModelState中的驗證錯誤,否則,當用戶在表單中輸入了不良數據的情況下,可能會得到奇怪的境況:該請求還是被處理了,就好像用戶沒有輸入任何數據,或輸入的是這個默認值一樣。
產生輸出
當控制器完成一個請求之后,通常要生成一個響應。通過實現IController接口創建“裸機控制器(Bare-metal Controller)”(單純繼承控制器接口的原始的控制器——需要手動實現接口的功能及各種必須功能等)時,需要負責處理請求的各個方面,包括生成對客戶端的響應。如:要想發送一個HTML響應,必須創建并裝配HTML數據,并用Response.Write方法把它發送至客戶端。類似地,若想將用戶瀏覽器重定向到另一個URL,則需要調用Response.Redirect方法,并直接傳遞所需的URL。如下演示了這種需求(加粗部分):
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing;namespace ControllersAndActions.Controllers {public class BasicController : IController{public void Execute(RequestContext requestContext){string controller = (string)requestContext.RouteData.Values["controller"];string action = (string)requestContext.RouteData.Values["action"];if (action.ToLower() == "redirect"){requestContext.HttpContext.Response.Redirect("/Derived/Index");}else{requestContext.HttpContext.Response.Write(string.Format("Controller: {0}, Action: {1}", controller, action));} }} }???????? 當控制器派生于Controller類時,可以使用同樣的辦法。在Execute方法中讀取requestContext.HttpContext.Reponse屬性時,返回的是HttpResponseBase類,這個類派生控制器中可直接通過Controller.Response屬性進行使用。如下所示(加粗部分):
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc;namespace ControllersAndActions.Controllers {public class DerivedController : Controller{public ActionResult Index(){ViewBag.Message = "Hello from the DerivedController Index method";return View("MyView");}public void ProduceOutput(){if (Server.MachineName == "TINY"){Response.Redirect("/Derived/Index");}else{Response.Write("Controller: Derived, Action: ProduceOutput");}} } }上面示例代碼中,ProduceOutput方法使用Server.Machine.Name屬性的值來決定發送給客戶端的響應(TINY是其中一個開放機器的名稱)。
這種方式是可行的,但仍存在以下幾個問題:
- 控制器類必須包含詳細的HTML或URL結構,這些使得控制器類更加難以閱讀和維護。
- 將響應直接生成為輸出的控制器難以進行單元測試。為了確定輸出表示的是什么,需要創建Response對象的模仿實現,然后才能處理從控制器接收到的輸出。如,這可能意味著要解析HTML關鍵字,這是費時而痛苦的。
- 這種處理每個響應微小細節的方式是乏味而易錯的。
還好,MVC框架提供了一個很好的特性來解決這種問題——動作結果(ActionResult),這將在后續內容中逐一介紹。
1.理解動作結果
MVC框架通過使用動作結果把指明意圖和執行意圖分離開。此處不直接使用Response對象,而是返回一個ActionResult類的對象,它描述控制器響應要完成的功能,如渲染一個視圖、重定向到另一個URL或動作方法等。
注:動作結果系統是一種命令模式(Command Pattern)。該模式描述你所處的場景,并發送一些對象,這些對象描述了要執行的操作。
當MVC框架從動作方法接收到一個ActionResult對象時,它調用有這個對象定義的ExecuteResult方法。然后在該動作結果的實現中處理Response對象,生成符合你意圖的輸出。(嚴格上說,MVC框架在接到動作結果對象時,是調用該對象類型對應的動作結果處理類(如:RedirectResult、ViewResult類等——這些類都繼成于ActionResult類(這是一個抽象類,作用就想其描述的那樣:封裝一個操作方法的結果并用于代表該操作方法執行框架級操作。)),然后執行該類的ExecuteResult方法——這是動作結果的一個實現方法,主要負責處理Response對象,最終生成所期望的輸出)。如下述清單所示(在項目中創建一個Infrastructure文件夾,然后在這里創建演示類:CustomRedirectResult類):
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc;namespace ControllersAndActions.Infrastructure {public class CustomRedirectResult : ActionResult{public string Url { get; set; }public override void ExecuteResult(ControllerContext context){string fullUrl = UrlHelper.GenerateContentUrl(Url,context.HttpContext);context.HttpContext.Response.Redirect(fullUrl);}} }下面是如何使用該類:
?
/// <summary>/// 演示對 CustomRedirectResult 對象調用的使用方法/// </summary>/// <returns></returns>public ActionResult ProduceOutput(){if (Server.MachineName == "TINY"){return new CustomRedirectResult { Url="/Basic/Index"};}else{Response.Write("Controller: Derived, Action: ProduceOutput");return null;}}上面介紹了如何自定義實現動作結果,并了解了其基本的工作模式。一般我們直接使用微軟提供的已經過完全測試的動作結果,而且,很多動作結果都有其便利的輔助器方法。如下表中展示了一些常用的內建ActionResult類型:
| 類型 | 描述 | 輔助器方法 |
| ViewResult | 返回指定的或默認的視圖模板 | View |
| PartialViewResult | 返回指定的默認的分部視圖模板 | PartialView |
| RedirectToRouteResult | 將HTTP301(或302)重定向發送給一個動作方法或特定的路由條目,根據路由配置生成一個URL | RedirectToAction RedirectToActionPermanent RedirectToRoute RedirectToRoutePermanent |
| RedirectResult | 將HTTP301或302重定向發送給一個特定的URL | Redirect RedirectPermanent |
| HttpUnauthorizedResult | 將響應的HTTP狀態碼設置為401(意為“未授權”),這會引發當前的認證機制(表單認證或Windows認證)要求訪問者進行登錄 | None |
| HttpNotFoundResult | 返回一個HTTP的“404——未找到”的錯誤 | HttpNotFound |
| HttpStatusCodeResult | 返回一個指定的HTTP碼 | None |
| EmptyResult | 什么也不做 | None |
?
2.通過渲染視圖返回HTML
動作方法最常用的一種響應形式是生成HTML,并將其發送給瀏覽器。如下面示例使用ViewResult指定了一個要被渲染的視圖(通過View輔助器方法創建了一個ViewResult實例對象):
public ViewResult Index(){return View("Homepage");}當MVC框架調用ViewResult對象的ExecuteResult方法時,將開始搜索已經指定的視圖。如果在項目中使用了區域,則框架將查找以下位置:
- /Areas/<AreaName(區域名)>/Views/<ControllerName(控制器名)>/<ViewName(視圖名)>.aspx
- /Areas/<AreaName>/Views/<ControllerName>/<ViewName>.ascx
- /Areas/<AreaName>/Views/Shared/<ViewName>.aspx
- /Areas/<AreaName>/Views/Shared/<ViewName>.ascx
- /Areas/<AreaName>/Views/<ControllerName>/<ViewName>.cshtml
- /Areas/<AreaName>/Views/<ControllerName>/<ViewName>.vbhtml
- /Areas/<AreaName>/Views/Shared/<ViewName>.cshtml
- /Areas/<AreaName>/Views/Shared/<ViewName>.vbhtml
從上可見,即時在創建項目時指定的是Razor,框架也會查找遺留視圖引擎創建的視圖(文件擴展名為.aspx和.ascx)。框架也查找了C#和VB的.NET Razor模板(.cshtml文件屬于C#模板,.vbhtml屬于VB模板,Razor語法在這些文件中是一樣的,而代碼使用的是不同的語言)。MVC框架會依次檢測這些文件是否存在,且,只要找到一個匹配的對象,便會用這個視圖來渲染該動作方法的結果。
如果上述區域目錄下未找到適當的文件,或未使用區域,則框架便會查找以下的位置:
- /Views/<ControllerName(控制器名)>/<ViewName(視圖名)>.aspx
- /Views/<ControllerName>/<ViewName>.ascx
- /Views/Shared/<ViewName>.aspx
- /Views/Shared/<ViewName>.ascx
- /Views/<ControllerName>/<ViewName>.cshtml
- /Views/<ControllerName>/<ViewName>.vbhtml
- /Views/Shared/<ViewName>.cshtml
- /Views/Shared/<ViewName>.vbhtml
同樣,只要MVC框架查找到合適的文件,便會停止搜索,且使用已經找到的這個視圖將響應渲染給客戶端。
在框架搜索相應位置的視圖文件時,對于控制器的部分,將會忽略Controller,如控制器:ExampleController將會以Example作為控制器名進行搜索。
單元測試:渲染一個視圖
為了測試動作方法渲染的視圖,可以檢測它返回的ViewResult對象。這當然不完全是一回事——畢竟,這并不是通過檢查最終生成的HTML來跟蹤這一過程——但也十分密切,只要能夠充分確信MVC框架的視圖系統會恰當工作。下面是我們的單元測試類:
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using ControllersAndActions.Controllers; using System.Web.Mvc;namespace ControllersAndActions.Tests {[TestClass]public class ActionTests{[TestMethod]public void ViewSelectionTest(){// 準備——創建控制器ExampleController target = new ExampleController();// 動作——調用動作方法ViewResult result = target.Index();// 斷言——檢測結果Assert.AreEqual("Homepage", result.ViewName);}} }當對選擇的默認視圖的動作方法進行測試時,有些區別,如動作方法:
public ViewResult Index(){return View();}此時,需要對視圖名采用空字符串(""),如:
Assert.AreEqual("", result.ViewName);MVC框架搜索視圖的目錄序列是“約定優于配置”這一規則的另一個例子。不需要用框架注冊視圖文件,只需要把它們放在一組已知的位置即可。如,假設動作方法未指定視圖,MVC框架則將會假設要渲染一個與動作方法同名的視圖。
View方法有多種重載版本,它們對應于在ViewResult對象上設置的不同屬性。如,通過明確地命名一個布局,可以重寫一個視圖所使用的(默認)布局,如:
public ViewResult Index(){return View("Index","_AlternateLayoutPage");}注意,上述指定的布局文件名不需要帶擴展名。
?
通過路徑指定視圖
命名約定辦法雖然簡單,但是很方便,且其確實能夠限制我們能夠渲染的視圖。如果要渲染的一個特定的視圖,可以通過提供一個明確的路徑并繞過搜索階段來完成。如:
public ViewResult Index(){return View("~/Views/Other/Index.cshtml");}注意,指定的路徑必須以“/”或“~/”開始,并包括文件擴展名。但是,讓我們要這么做的時候需要謹慎一些,因為這樣指定路徑的方式不利于應用程序的進一步擴展和維護,這是一種綁定或耦合。最好的做法是通過控制器的動作方法來重定向要渲染的視圖。
?
3.將數據從動作方法傳遞給視圖
在實際的項目中經常會需要將數據從一個動作方法傳給視圖。而MVC框架對此提供了多種不同的方法,下面將給出描述:
- 提供視圖模型對象
將一個對象作為View方法的參數發送給視圖,如下面示例:
public ViewResult Index(){DateTime date = DateTime.Now;return View(date);}上述示例傳遞了一個DateTime對象作為視圖模型。可以在視圖中用Razor的Model關鍵字來訪問這個對象,如:
@{// 演示如何獲取動作方法通過 View 方法的參數發送給視圖的視圖模型信息ViewBag.Title = "Index"; }<h2>Index</h2>The day is: @(((DateTime)Model).DayOfWeek)需要注意的是,非類型或弱類型視圖易產生雜亂的視圖,這可以通過強類型視圖來加以調整。
上面的視圖就是一個非類型視圖(或稱為弱類型視圖),該視圖不知道關于視圖模型對象的任何情況,而把它作為object的一個實例來看待。為了得到DayOfWeek屬性的值,需要將其轉換成DateTime的一個實例。然即使這樣,這種做法仍會產生雜亂的視圖。下面通過創建強類型視圖加以調整,如:
@*改用強類型視圖,以避免產生雜亂的視圖*@ @model DateTime @{// 演示如何獲取動作方法通過 View 方法的參數發送給視圖的視圖模型信息ViewBag.Title = "Index"; }<h2>Index</h2>@*弱類型視圖方式:The day is: @(((DateTime)Model).DayOfWeek)*@@*強類型視圖方式:*@ The day is: @Model.DayOfWeek注意:當指定模型類型時,要使用小寫的“m”(如:@model DateTime),而在讀取模型值時,要用大寫的“M”(如:@Model.DayOfWeek)。
?
視圖模型對象的單元測試
?
對于視圖模型對象的單元測試,我們可以通過ViewResult.ViewData.Model 屬性訪問從動作方法傳遞給視圖的視圖模型對象。下面是一個簡單的動作方法實例:
public ViewResult Index(){return View((object)"Hello,World");}該動作方法傳遞了一個字符串作為視圖模型對象。該字符串被轉換為object,以使編譯器不會認為我們要用的是指定視圖名稱的那個View重載版本。對應的具體測試方法如下:??
/// <summary>/// 通過 ViewResult.ViewData.Model 屬性訪問從動作方法傳遞給視圖的視圖模型對象。/// </summary> [TestMethod]public void ViewSelectionTest(){// 準備——創建控制器ExampleController target = new ExampleController();// 動作——調用動作方法ViewResult result = target.Index();// 斷言——檢查結果Assert.AreEqual("Hello,World", result.ViewData.Model);}
?
- 用ViewBag傳遞數據
ViewBag特性允許在一個動態對象上定義任意屬性,并在視圖中訪問它們。這個動態對象可以通過Controller.ViewBag屬性進行訪問,如:???
/// <summary>/// 使用視圖包特性:ViewBag/// </summary>/// <returns></returns>public ViewResult Index(){ViewBag.Message = "Hello";ViewBag.Date = DateTime.Now;return View();}就像上面演示的那個,在動態定義屬性:Massage和Date之前,它們是不存在的,不需要任何準備就可以創建它們。這是很方便的一個特性。要做視圖中讀取這些數據時,只有簡單地采用在動作方法中設置的同樣屬性即可。如:
@{ViewBag.Title = "Index"; }<h2>Index</h2>The day is: @ViewBag.Date.DayOfWeek <p>The message is: @ViewBag.Message </p>這樣做的好處是:ViewBag便于將多個對象發送給視圖。當用動態對象進行工作時,可以在視圖中鍵入屬性和方法調用的任意序列,如:
The day is: @ViewBag.Date.DayOfWeek.Blah.Blah.Blah(這里Blah是虛字,表示“一系列調用”)。
???????? 然而,這么做有一個不足,那就是:Visual Studio不能對包括ViewBag在內的任何動態對象提供智能感應支持,而在視圖被渲染之前,不支持諸如“對此無法展示”之類的錯誤提示。
???????? 所以,在平常的項目中經常需要使用ViewBag的靈活性和強類型視圖相結合的方式來滿足我們的需求,這兩種結合使用不會有任何問題。
ViewBag的單元測試
?
可以通過ViewResult.ViewBag屬性來讀取ViewBag的值,下面是測試方法:???????
/// <summary>/// 通過 ViewResult.ViewBag 來讀取 ViewBag,并對其進行測試/// </summary> [TestMethod]public void ViewSelectionTest(){// 準備——創建控制器ExampleController target = new ExampleController();// 動作——調用動作方法ViewResult result = target.Index();// 斷言——檢查結果Assert.AreEqual("Hello", result.ViewBag.Message);}4.執行重定向
不是所有的動作方法都是或不是任何時候都需要直接產生輸出,有時候我們需要把用戶的瀏覽器重定向到另一個RUL,而且,大多數情況,這個URL是應用程序中的另一個動作方法,它可以生成希望用戶看到的輸出。
POST/Redirect/GET模式
重定向最頻繁的使用是在處理HTTP POST請求的動作方法中。當想修改應用程序的程序狀態時,才會使用POST請求。如果在請求處理之后簡單地返回HTML,會陷入這樣的風險:用戶點擊瀏覽器的刷新按鈕,并再次遞交該表單,這會引發異常和不符合需求的結果。
為了解決這一問題,可以遵循一種“Post/Redirect/Get”的模式。在該模式中,先接受一個POST請求(POST)、對該請求進行處理,然后重定向(Redirect)瀏覽器,以便由瀏覽器形成另一個GET請求(GET)的URL。GET請求不會修改應用程序的狀態,因此,該請求的任何不經意的再次遞交都不會引起任何問題。
上面的描述也體現了動作方法處理POST請求的安全工作流程:Post/Redirect/Get。即,用一個POST動作方法接受用戶遞交的POST請求,在該方法中對請求進行處理,然后用重定向方法把用戶重定向到另一個GET方法。
?
在執行重定向時,給瀏覽器發送的是以下兩個HTTP代碼之一:
- 發送HTTP代碼302,這是一個臨時重定向。它是最常用的重定向類型,而且,當使用Post/Redirect/Get模式時,這就是要發送的代碼。
- 發送HTTP代碼301,它表示一個永久重定向。要小心使用它,因為它指示HTTP代碼接收器不要請求原先的URL,而使用包含在重定向代碼中的新URL。如果拿不準,則使用臨時重定向,即發送302代碼。
?重定向到字面URL
一般最常用最基本的方法是通過調用Redirect方法進行重定向,它返回RedirectResult類的一個實例,如:
/// <summary>/// 重定向到一個字面 URL/// </summary>/// <returns></returns>public RedirectResult Redirect(){return Redirect("/Example/Index");}???????? Redirect方法發送的是一個臨時重定向(HTTP代碼:302),可以使用RedirectPermanent方法發送的是一個永久重定向(HTTP代碼:301)。如:?
/// <summary>/// 重定向到一個字面 URL/// </summary>/// <returns></returns>public RedirectResult Redirect(){ // 永久重定向return RedirectPermanent("/Example/Index");}通過Redirect的重載方法,通過一個布爾型參數指定是否永久重定向。
?
單元測試:字面重定向
字面重定向易于測試,可用RedirectResult類的Url和Permanent屬性來讀取URL和永久或臨時重定向。如:??
[TestMethod]public void RedirectTest(){// 準備——創建控制器ExampleController target = new ExampleController();// 動作——調用動作方法RedirectResult result = target.Redirect();// 斷言——檢查結果 Assert.IsFalse(result.Permanent);Assert.AreEqual("/Example/Index", result.Url);}?
?重定向到路由系統的URL
用字面URL重定向的問題是,對路由方案的任何修改,都意味著你需要檢查代碼,并對這些URL進行更新。
對于這種問題,可以使用路由系統來解決,通過RedirectToRoute方法來生成有效的URL,如:?
/// <summary>/// 重定向到一個路由系統的 URL/// </summary>/// <returns></returns>public RedirectToRouteResult Redirect(){// 重定向到一個路由系統的 URLreturn RedirectToRoute(new{controller = "Example",action = "Index",ID = "MyID"});}???????? 該方法會發布一個臨時重定向。對于永久重定向可以使用RedirectToRoutePermanent方法。這兩個方法都以一個匿名類型作為參數,然后其屬性被傳遞給路由系統,以生成一個URL。
單元測試:路由重定向
?
測試代碼如下:
/// <summary>/// 測試: 路由重定向/// </summary> [TestMethod]public void RedirectValueTest(){// 準備——創建控制器ExampleController target = new ExampleController();// 動作——調用動作方法RedirectToRouteResult result = target.RedirectToRoute();// 斷言——檢查結果 Assert.IsFalse(result.Permanent);Assert.AreEqual("Example", result.RouteValues["controller"]);Assert.AreEqual("Index", result.RouteValues["action"]);Assert.AreEqual("MyID", result.RouteValues["ID"]);}?
?重定向到一個動作方法
可以通過RedirectToAction方法方便的重定向到一個動作方法。該方法是RedirectToRoute的一個封裝程序,讓用戶指定動作方法和控制器的值,而不需要創建一個匿名類型,如:
/// <summary>/// 用 RedirectToAction 方法重定向/// </summary>/// <returns></returns>public RedirectToRouteResult RedirectToAction(){return RedirectToAction("Index");}上述示例代碼中,只指定了一個動作方法,此時,系統默認該方法為當前控制器的動作方法。如果需要重定向到另一個控制器,則需要以參數的方式提供其名稱,如:
// 重定向到指定控制器(另一個控制器)的動作方法return RedirectToAction("Index", "Basic");還有一些其他的重載版本,可以用來為URL的生成提供額外的值,雖然其采用的匿名類型表示的方式破壞了一定的便利性,但仍具有很好的易讀性。
需要注意的是:為控制器和動作方法提供的值,在被傳遞給路由系統之前是不會被檢驗的。在開發的時候需要確保指定的目標是實際存在的。
???????? 同樣,該方法提供的是一個臨時重定向,其永久重定向的方法是:RedirectToActionPermanent。
?
知識點:保留重定向數據
?
重定向是跨請求的,ViewBag不能用于跨請求的情況下控制器與視圖之間的數據傳遞。重定向將導致瀏覽器遞交一個新的HTTP請求,這樣會失去對原先請求細節的訪問。此時,如果需要把當前請求的數據保傳遞給下一個請求,可以使用TempData(臨時數據)特性,
TempData類似于Session數據,只不過TempData的值在被讀取之后僅被標記為刪除狀態,當請求被處理完成后才真正刪除。下面是一個示例:
public RedirectToRouteResult RedirectToAction(){// 使用 TempData 保存重定向數據,以實現跨請求的重定向情況下控制器與視圖之間的數據傳遞TempData["Message"] = "Hello";TempData["Date"] = DateTime.Now;// 重定向到當前控制器的動作方法return RedirectToAction("Index");}???????? 上述示例方法中,在處理請求時,在TempData集合中設置了一些值,然后把用戶的瀏覽器重定向到同一個控制器中的Index動作方法。開發時,可以在母版動作方法中讀回的數據,然后把它們傳遞給視圖,如:
public ViewResult Index(){// 讀取重定向至該方法之前設置的臨時數據的值ViewBag.Message = TempData["Message"];ViewBag.Date = TempData["Date"];return View();}更直接的做法是直接在視圖中讀取,如:
The day is: @(((DateTime)TempData["Date"]).DayOfWeek) <p/> The message is:@TempData["Message"]如果在視圖中直接讀取了這些值,就不需要在動作方法中使用ViewBag特性獲取值了。然而,必須把TempData結果轉換成相應的類型。
利用Peek方法,可以得到TempData的值,而不把它標記為刪除,如:
// 使用 Peek 方法實現讀取 TempData 中的值但不將其標記為刪除的方式DateTime time = (DateTime)TempData.Peek("Date");???????? 利用Keep方法可以保留一個否則將被刪除的值,如:???????????
TempData.Keep("Date");但是,Keep方法是臨時保留,并非永久保留,如果該值被再次讀取,則它將被標記為刪除。如果想存儲一些數據,以使它們在請求被處理后不會被自動刪除,請使用session數據(注意,session數據會占用服務器資源,只有在會話過期后才會被刪除)。
5.返回錯誤及HTTP代碼
雖然一般情況下MVC框架會自動生成錯誤消息或HTTP結果,但是如果需要對發送給客戶端的響應有更直接的控制,則可能會需要使用一些內建的ActionResult類。下面我們就看看它們是如何使用的:
- 發送特定的HTTP結果嗎
使用HttpStatusCodeResult類將一個特定的HTTP狀態碼發送給瀏覽器,這個類沒有對應的控制器輔助器方法,所以在使用的時候必須對其進行實例化:
public HttpStatusCodeResult StatusCode(){return new HttpStatusCodeResult(404, "URL cannot be serviced");}- 發送404結果
對于返回404結果,還以更方便的方法:HttpNotFoundResult。這個類派生自HttpStatusCodeResult,而且可以使用控制器的輔助器方法HttpNotFound來創建:???????
/// <summary>/// 使用 HttpNotFound 方法返回 404 代碼/// </summary>/// <returns></returns>public HttpStatusCodeResult StatusCode404(){return HttpNotFound();}?
- 發送401結果
另一個特定的HTTP狀態碼的封裝類型是HttpUnauthorizedResult,它返回的是一個401代碼,表示一個未授權請求。如:
/// <summary>/// 返回 401 代碼/// </summary>/// <returns></returns>public HttpStatusCodeResult StatusCode401(){return new HttpUnauthorizedResult();}此時,一般的做法是將頁面返回至認證頁面。
單元測試:HTTP狀態碼
?
HttpStatusCodeResult類遵循了在其他結果類型所看到的模式,并通過一組屬性可使用它的狀態。此時,StatusCode屬性返回數值型的HTTP狀態碼,而StatusDescription屬性返回相應的描述字符串。下面是測試代碼,以StatusCode方法為例:??
/// <summary>/// 測試: HTTP 狀態碼/// </summary> [TestMethod]public void StatusCodeResultTest(){// 準備——創建控制器ExampleController target = new ExampleController();// 動作——調用動作方法HttpStatusCodeResult result = target.StatusCode();// 斷言——檢查結果Assert.AreEqual(404, result.StatusCode);}?
轉載于:https://www.cnblogs.com/KeSaga/p/5551900.html
總結
以上是生活随笔為你收集整理的005_控制器和动作的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android-ImageLoader的
- 下一篇: Prim算法 求出 最小生成树