到達應用程式的每一個請求都是由控制器處理的。但要註意,不要把事務或數據存儲邏輯放到控制器中,也不要生成用戶界面。 在ASP.NET MVC框架中,控制器是含有請求處理邏輯的.NET類。其作用是封裝應用程式邏輯。也就是說,控制器要負責處理輸入請求、執行域模型上的操作,並選擇渲染給用戶的視圖。 控制器的 ...
到達應用程式的每一個請求都是由控制器處理的。但要註意,不要把事務或數據存儲邏輯放到控制器中,也不要生成用戶界面。
在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() { // 重定向到一個路由系統的 URL return RedirectToRoute(new { controller = "Example", action = "Index", ID = "MyID" }); }
該方法會發佈一個臨時重定向。對於永久重定向可以使用RedirectToRoutePermanent方法。這兩個方法都以一個匿名類型作為參數,然後其屬性被傳遞給路由系統,以生成一個URL。
單元測試:路由重定