ASPNET MVC4 都是Action的控制器(Actions-packed Controllers)
--我沒有自信是否能把本次的任務完成,但我會負責任的完成
本人能力有限,盡量將書中的知識濃縮去講,仔細學過后,然后你再學習其他語言的MVC框架也就大同小異了
本次覆蓋知識點:
- 1. 怎樣才算一個控制器(What makes a controller)
- 2. 控制器中的成員有什么 (What belongs in a controller)
- 3. 手動映射視圖模型 (Manually mapping view models )
- 4. 驗證該用戶的輸入 (Validating user input )
- 5. 使用默認的單元測試模版(Using the default unit test project )
在最近兩章里,我們看了一下創建一個Guestbook應用程序基礎知識,也看了幾種不同的把數據傳遞給視圖的可以實現的方式。在本章里,我們將會完成Guestbook例子,我們只添加一點點細節部分。我們將研究什么應該是controller中的一部分,什么不應該是,然后看一下怎樣手動構造一個視圖models,驗證用戶的輸入,寫幾個不會使用到視圖的控制器操作(action)。這個將會讓我們在controller中建造一個很常用action塊(block),也就是不返回view類型action代碼,我們以前學的都是返回一個view(),這里就不是的了。
我們也將向你簡單地介紹一下單元測試controller中的action,這樣可以確保這些action可以正確的工作。我們將會使用默認的單元測試項目,然后為GuestbookController創建單元測試,測試前面幾章的工作情況。
在我們學習這些新知識之前,我們先快速地概括一下controllers和actions
4.1 探討Controllers和actions
備注:controller action指controller中的action(也就是你們經常寫的方法而已)
controller actions指controller中全部actions
action名字加action表示 某某名字的action,例如有個叫Index的action,我就會這樣表示
Index action.知道了嗎?這方便我寫博客,也更方便理解
view model指頁面(view)中用到model,我上篇博客說的視圖模型就是view model
action method指 返回值是ActionResult那個方法,比如Index那個action方法
user interface,用戶接口,你可以意會理解成頁面,也就是要跟用戶交互的東西,比如我們給用提供一個頁面,讓用戶錄入數據,這就是一個interface,一個接口,你暫且在這里就是view,就是頁面
business logic,商業邏輯,類似於數據訪問層中業務實現的邏輯
domain,你可以理解數據訪問層
domain model,數據訪問層要使用到的model
以后我想直接寫英文了,好方便理解,在上一波博客我是吃過苦了,文章讀起來好拗口
在第一波里面我們看出來了Controller的重要性,它里面有很多方法(action),這些action都可以通過url實現觸發調用.而這些action就是把model中的數據傳到頁面上的中間者,也可以理解為搬運工吧,所以理解action是怎樣工作的是很重要的.在下一節,我們主要更詳細地了解下controller actions是怎樣工作的.
下面我們看下action的樣子(GuestbookController中的Index action)
它繼承了Controller,也含有默認的action,我們也知道所有的控制器必須繼承Controller,其實框架允許我們只要至少實現IController否則,它是不能處理Web請求的
下面我們看看框架是怎么知道哪個class是個被當做controller來處理,因為controller說白了也是一個類而已.下面讓我們看看
IController
4.1.1 IController和Controller 父類(base class)
IController定義了一個controller最根本的東東---Execute方法接受一個RequestContext類型的對象
下面是一個簡單的controller,我們寫一些html放到響應流里面(response stream)
我們手動實現一下:
這是一個實現了IController的控制器,我們實現了Execute方法,我們就能直接訪問HttpContext,Request還有Response對象了.這種方式很容易定義但是沒什么用.這樣做,我們就無法直接呈現view.而且我們這樣做---在controller中直接就寫HTML,把邏輯和呈現的內容混在一起了,很難受.
好吧,我們先不看框架那些有用的特征,比如說安全性(第8章會講),model binding(第10章),action results(第16章).
這樣做,我們也喪失了定義action方法的能力----------因為所有的請求都被Execute處理了
實際上,你需要去實現IController也不太可能那樣去做,因為它本身沒什么用(避開框架的主要部分,有一些理由能讓你覺的還是有用的).通常我們會繼承父類----------ControllerBase和Controller
ControllerBase
ControllerBase類除了包含了我們已經看過的一些基本特征以外,它本身就直接實現了IController.舉個例子:ControllerBase也包含ViewData屬性(把數據出給view的一個手段).但是ControllerBase仍然還是不怎么有用-----它仍然還是不能通過使用action來呈現view.所以Controller來了
Controller
Controller繼承ControllerBase,所以除了包含一些有意義的東西以外,它還包含了ControllerBase定義的東東(比如ViewData).它包含了ControllerActionInvoker(這是一個知道怎樣根據url找到對應的方法(method),並執行了那個方法,它定義了一些方法,比如View(可以通過controller action來呈現一個view))
這是一個當你開始創建好你的controller的時候會自動繼承的類,所以說直接繼承ControllerBase或者IController都不會有什么幫助.但是我們知道了在MVC 管道(pipeline)模型中扮演很重要的角色,知道它們的存在還是很有用的.
好了,現在我們已經知道了怎樣把一個類變成一個Controller.下面我們看看action
4.1.2 怎樣才可以成為一個action method
在第二章(我的第一波博客,我從第二章開始講的,第一章是介紹,以后我會按書里面的章節順序講,還希望諒解)我們 在controller中看到了一些public的action方法(事實上,決定一個方法能否成為一個action,是有條規律規則的,講起來還是蠻復雜的,我們將會在第16章開始講解).
平常呢,action method都是返回一個ActionResult的一個實例.舉個例子,一個action返回值可以是void類型的,也可以直接輸出HTML(很像我們剛剛的SimpleController)
我們也可以這樣寫,它們結果是一樣的,返回一個HTML片段(snippet)
這個沒有問題,因為ControllerActionInvoker它保證了action的返回值總是ActionResult.如果action返回的是一個ActionResult(例如ViewResult),ControllerActionInvoker就會被調用.但是如果action返回的一個不同的類型(就像這里,一個string),它的返回值是ContentObject對象(也是一個把要寫入的東西放入響應流里面的東東),直接使用ContentResult也是一樣的
這就是一個很簡單的action了,不需要view就可以直接在瀏覽器中呈現HTML標簽了。但是在真實世界的應用程序中通常不會這樣用的,還是最好通過view把要呈現的頁面和controller分開,這樣做當我們改變user interface的時候,就不用動controller中的代碼了
除了呈現標簽或者返回一個view,還有其他幾種類型,舉個例子,我們可以使用RedirectToRouteResult,讓用戶在瀏覽的時候跳到其他頁面(我們在第二章(我的第一波博客)中的RedirectToAction方法你已經使用過了,還記得嗎?),我們也可以返回JSON(在第七章的AJAX中我們將會學到)
我們可以使用NonActionAttribute讓一個controller中的public的action不是一個action。
NonActionAttribute是一個action方法選擇器(action method selector),它可以重寫 匹配一個方法名到一個action名的默認行為(behavior).NonActionAttribute是最簡單的一個選擇器,它可以使那些可以通過URL訪問的方法不可以通過這種手段來訪問,其實在第二章,我們也已經看到了一些選擇器,比如HttpPostAttribute(可以確保這個action可以響應HTTP post形式的請求,其他方式都是不可以訪問的)
提醒
NonActionAttribute很少使用的,在一個controller中一個public的方法你不想它成為action,你最好想一下controller中是不是它最好的地方,如果這個方法有用,你可能會寫成private訪問修飾符級別的。如果這個方法必須public,因為需要測試的原因,我覺得應該提取成一個單獨的類
現在讓我簡單地看一下什么構成了action,你將會看到一些不同的方式,把內容放到瀏覽器中顯示。除了view,你也可以直接發送content(內容),或者直接執行其他的action(重定向),這些技術在你的應用程序中都會很有用。
現在讓我們去看一下action中的邏輯
4.2 在action method中有什么
MVC中最大的兩點就是將各自的職責分離,user interface 和 邏輯(比如頁面提交數據怎么處理)分開,因此整個程序就更容易維護(maintain)了。如果你沒有讓你的controller更精巧(lightweight實際是輕量級的意思),把重點都放在controller里,你就不會體會到它的好處了。
Controller應該扮演中間者(coordinator)--它不應該包含business logic。反之亦然(vice versa),它可以把view上的表單,用戶的輸入的數據(user input),來自view的,封裝成一個對象,然后我們把這個對象在domain(實際business logic代碼寫
在的地方)層中使用,這樣我們數據的存儲都可以完成了。
讓我們看一下通過controller如何把一個任務完成--手動的映射view models,接受用戶的輸入的數據。首先,展現的是如何匹配view models,現在我們打開我們的guestbook例子,添加一個新的頁面, 用不同的方式顯示數據,存數據就先放着。接下來我們在這個頁面上,把數據添加到數據庫之前先添加一些驗證,因為我們的數據庫不需要存儲一些沒有用的(invalid)數據。在本節結束之后,你應該在知道怎樣構建一個view model了,怎樣完成基本的input驗證,應該有個大致的了解了
4.2.1 手動地去匹配view models(view models 指view使用到的model)
在第三章(上一波博客),我們已經有了強類型視圖的印象,還有一個view model--為了能夠把數據更好地顯示在屏幕上,創建的而一個單獨的model對象。
到目前為止,我們做的例子中,我們使用到了同樣的類(GuestbookEntry)作為我們的domain model和view model--它展現了我們數據庫中的信息,也是在user interface上要表示的字段,信息
在非常小的項目中使用,比如我們的guestbook,這還可以,但是隨着程序越來越復雜,用戶界面也復雜了,model中的數據已經不能直接匹配了,就有必要要分成兩部分(題外話:我現在自己公司做的項目就是兩部分,一個EF生成的實體,一個擴充的實體,都已Dto結尾,只是為了更方便的顯示數據),正是由於這個,我們應該有能力可以把domain model轉換成view model,讓數據更容易在user interface顯示
我們就寫個例子吧,向我們的Guestbook項目上添加一個頁面:做一個總結頁面,知道每一個人發了多少個comment,首先我們創建一個view model(view model專門讓頁面好展現數據,就看你怎么設計了)
在Models文件下建立CommentSummary.cs
我們現在需要創建一個controller action----返回一個CommentSummary集合,我們在GuestbookController中寫action
1: public ActionResult CommentSummary()
2: {
3: var entries = from entry in _db.Entries
4: group entry by entry.name into groupByName
5: orderby groupByName.Count() descending
6: select new CommentSummary
7: {
8: NumberOfComments = groupByName.Count(),
9: UserName = groupByName.Key
10: };
11: return View(entries.ToList());
12:
13: }
我們使用linq查詢,不懂的可以看一下我的 小孩系列LINQ教程
這個映射(mapping)邏輯相當簡單,把這些代碼寫在控制器你還是可以講得通的,但是如果這個mapping變得更加復雜(舉個例子,如果它取了很多來自不同數據源的信息,只是為了構造view model),我們應該把這些邏輯從controller action中移出,分離(就像以前的三層框架),讓我們的controller更加簡單,輕量級的
在view中,我們循環輸出這個列表,用table展現
右鍵該action名稱,保持默認,添加一個視圖
1: @model IEnumerable<GuestInfo.Models.CommentSummary>
2:
3: <table>
4: <tr>
5: <th>Number of comments</th>
6: <th>User name</th>
7: </tr>
8: @foreach(var summaryRow in Model) {
9: <tr>
10: <td>@summaryRow.NumberOfComments</td>
11: <td>@summaryRow.UserName</td>
12: </tr>
13: }
14: </table>
自動匹配View models
除了手動地把domain object和view models進行映射(mapping)以外,你也可以使用工具,比如開源的AutoMapper,像這個目的,更少的代碼就可以完成了,我們將在第11章中MVC項目中教你怎么使用AutoMapper
在這一節,我們已經很簡短地看了一點view model,但是在下一章我們將要更仔細地看,我們當然也會研究view models和input models的不同點
除了mapping操作,controller中還有個經常用到的任務,驗證用戶輸入
4.2.2 驗證用戶輸入(input validation)
回到第二章,我們在GuestbookController.cs中的Create action接受了用戶的input(輸入)。
這個action,接受了Create.cshtml post過來的input的數據(一個GuestbookEntry對象(已經被MVC模型綁定(model-binding)機制處理了)的表單),設置一下日期,然后我們Insert一條數據到數據庫,雖然已經完成了,但還沒有真的完成--我們還沒有任何的驗證。此時,用戶是可以不輸入他們的name或者comment就可以提交(submit)數據的。現在讓我們添加一些驗證。
首先讓我們在GuestbookEntry類中用required特性 注解一下 Name和Message,使用using System.ComponentModel.DataAnnotations;導入命名空間
1: using System;
2: using System.Collections.Generic;
3: using System.ComponentModel.DataAnnotations;
4: using System.Linq;
5: using System.Web;
6:
7: namespace GuestInfo.Models
8: {
9: public class GuestbookEntry
10: {
11: public int Id { get; set; }
12: [Required]
13: public string name { get; set; }
14: [Required]
15: public string Message { get; set; }
16: public DateTime DateAdded { get; set; }
17:
18: }
19: }
使用注解(Annotate),也是一種驗證對象屬性的方法,Required表示這個filed不能為空,還有一些其他注解,比如StringLengthAttribute:驗證字符串長度的。(我們將會在第6章更深入學習一下)
一旦注解了,當Create action被調用的時候,MVC將會自動地驗證這些屬性,看是否驗證通過,我們可以使用ModelState.IsValid屬性,然后就可以做決定了,怎么處理。修改一下Create
1:
2: [HttpPost]//限制只能是HTTP Post 能夠訪問這個方法
3: public ActionResult Create(GuestbookEntry entry)
4: {
5: if (ModelState.IsValid)
6: {
7: entry.DateAdded = DateTime.Now;
8: _db.Entries.Add(entry);
9: _db.SaveChanges();
10: return RedirectToAction("Index");
11: }
12: return View(entry);
13: }
注意
調用ModelState.IsValid實際上不會進行表單驗證,它只是檢查驗證是失敗還是成功。驗證發生在Controller action被調用之前
我們可以調用Html.ValidationSummary方法,當驗證失敗的時候,顯示錯誤信息
運行頁面,還沒輸入任何內容點擊提交:
使用這些助手,MVC將會自動檢測驗證錯誤的信息,還應用了一個css樣式,因為我們的程序是基於默認的MVC項目模版上寫的,這個無效的信息,將會顯示淺紅色背景色
這個錯誤的信息內容你可以這樣改,在Required加參數
同樣地如果你不想 硬編碼消息,想要依賴於通過資源文件,支持本地化,你可以指定ResourceName和Resource type
例如:
1: [Required(ErrorMessageResourceType=typeOf(MyResources),ErrorMessageResourceName="RequiredMessageError")]
2: public string Message { get; set; }
4.3 單元測試介紹
在這一節,我們將要簡短地看一下測試controllers。有很多測試的方法,這里我們主要講:unit testing
單元測試很小,腳本測試,使用的語言和你項目中的語言一樣。為了驗證某個方法對不對,能不能達到想要的效果就去單獨隔離式地測試某個組件的方法或函數。隨着程序的變大,單元測試也會變多,看到一個應用程序有成百甚至上千個單元測試都是很正常的,它們可以在任何時間執行,去驗證bug,讓bug不會再發生
為了讓單元測試運行地更快,我們不能調用進程外面的東西,這點很重要。當測試一個controller中的代碼,任何獨立的部分都是模擬的,所以主要的產品代碼還是controller它本身。針對這個有一種可能,控制器被設計成獨立的部分,很容易就被調用(比如說database或者web服務)
為了更高效地測試我們的GuestbookController,為了允許測試,我們要做一些修改,但是在我們做這些事情之前,讓我們看一下ASP.NET MVC中默認的單元測試模版
4.3.1 使用默認的單元測試模版
默認地,當你創建一個新的ASP.NET MVC項目的時候,VS會提供一個創建單元測試項目的選項(在第二章我們已經見過了)
如果你選擇一個創建單元測試項目,vs將會生成一個using Visual Studio Unit Testing FrameWork。這個單元測試項目包括了一些示例的測試,比如在HomeControllerTest類可以看見
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.Web.Mvc;
6: using Microsoft.VisualStudio.TestTools.UnitTesting;
7: using GuestBook;
8: using GuestBook.Controllers;
9:
10: namespace Guestbook.Tests.Controllers
11: {
12: [TestClass]
13: public class HomeControllerTest
14: {
15: [TestMethod]
16: public void Index()
17: {
18: // 排列
19: HomeController controller = new HomeController();
20:
21: // 操作
22: ViewResult result = controller.Index() as ViewResult;
23:
24: // 斷言
25: Assert.AreEqual("修改此模板以快速啟動你的 ASP.NET MVC 應用程序。", result.ViewBag.Message);
26: }
27:
28: [TestMethod]
29: public void About()
30: {
31: // 排列
32: HomeController controller = new HomeController();
33:
34: // 操作
35: ViewResult result = controller.About() as ViewResult;
36:
37: // 斷言
38: Assert.IsNotNull(result);
39: }
40:
41: [TestMethod]
42: public void Contact()
43: {
44: // 排列
45: HomeController controller = new HomeController();
46:
47: // 操作
48: ViewResult result = controller.Contact() as ViewResult;
49:
50: // 斷言
51: Assert.IsNotNull(result);
52: }
53: }
54: }
看代碼,跟以前其他 客戶端形式的單元測試還是很像的,很好就學會了,這里我就不多說了
4.3.2 測試GuestbookController
有一些事情,GuestbookController的實現直接初始化,使用了GuestContext對象,這個對象訪問數據庫。這就是說沒有數據庫的前提下,要想正確的獲得數據,這個測試不可能進行,這是一個集成測試(Integration Test),而不是單元測試了
盡管集成測試很重要,它能確保一個應用程序的組件是否正確的在配合,也就是說如果我們對在controller中測試這個邏輯感興趣,我們不得不先測試數據庫連接。在小數量的測試,這個可能,但是如果一個項目有成百上千個測試,執行時間明顯下降,因為每一個都要連接數據庫。這個問題的解決方案是減弱控制器中的GuestbookContext對象
不直接使用GuestbookContext,我們介紹一個repository,它提供了一個入口,在處理數據訪問操作,操作GuestbookEntry對象,我們為我們的repository寫個接口
這個接口定義了4個方法,匹配4個查詢,對應GuestbookController
我們定義了包含查詢邏輯的概念上的實現
1: using GuestInfo.Models;
2: using System;
3: using System.Collections.Generic;
4: using System.Linq;
5: using System.Text;
6: using System.Threading.Tasks;
7:
8: namespace Guestbook.Contract
9: {
10: public class GuestbookRepository : IGuestbookRepository
11: {
12: private GuestbookContext _db = new GuestbookContext();
13:
14: public IList<GuestbookEntry> GetMostRecentEntries()
15: {
16: return (from entry in _db.Entries
17: orderby entry.DateAdded descending
18: select entry).Take(20).ToList();
19: }
20:
21: public void AddEntry(GuestbookEntry entry)
22: {
23: entry.DateAdded = DateTime.Now;
24: _db.Entries.Add(entry);
25: _db.SaveChanges();
26:
27: }
28:
29: public GuestbookEntry FindById(int id)
30: {
31: var entry = _db.Entries.Find(id);
32: return entry;
33: }
34:
35: public IList<CommentSummary> GetCommentSummary()
36: {
37: var entries = from entry in _db.Entries
38: group entry by entry.Name into groupedByName
39: orderby groupedByName.Count() descending
40: select new CommentSummary
41: {
42: NumberOfComments = groupedByName.Count(),
43: UserName = groupedByName.Key
44: };
45: return entries.ToList();
46: }
47: }
48: }
接下來我們在GuestbookController中添加一些代碼,把接口引進來
修改代碼:
using Guestbook.Contract;
using GuestInfo.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace GuestInfo.Controllers
{
public class GuestbookController : Controller
{
private GuestbookContext _db = new GuestbookContext();
IGuestbookRepository _respository;
public GuestbookController() {
_respository = new GuestbookRepository();
}
public GuestbookController(IGuestbookRepository gu)
{
_respository = gu;
}
//
// GET: /Guestbook/
public ActionResult Index()
{
////Shift+Tab減少縮進,Tab增加縮進
//var mostRecentEntries = (from o in _db.Entries orderby o.DateAdded descending select o).Take(20);
//// ViewBag.Entries = mostRecentEntries.ToList();
//var model = mostRecentEntries.ToList();
var mostRecent = _respository.GetMostRecentEntries();
return View(mostRecent);
}
public ActionResult Create()
{
return View();
}
[HttpPost]//限制只能是HTTP Post 能夠訪問這個方法
public ActionResult Create(GuestbookEntry entry)
{
if (ModelState.IsValid)
{
//entry.DateAdded = DateTime.Now;
//_db.Entries.Add(entry);
//_db.SaveChanges();
_respository.AddEntry(entry);
return RedirectToAction("Index");
}
return View(entry);
}
public ActionResult Show(int id)
{
//var entry = _db.Entries.Find(id); //找到該Id的Entries
var entry = _respository.FindById(id);
bool hasPermission = User.Identity.Name == entry.name; //如果登陸人的姓名等於entry錄入人的姓名,就顯示Edit按鈕
ViewData["hasPermission"] = hasPermission;
ViewBag.hasPermission = hasPermission;
return View(entry);
}
public ActionResult CommentSummary()
{
//var entries = from entry in _db.Entries
// group entry by entry.name into groupByName
// orderby groupByName.Count() descending
// select new CommentSummary
// {
// NumberOfComments = groupByName.Count(),
// UserName = groupByName.Key
// };
var entries = _respository.GetCommentSummary();
return View(entries.ToList());
}
}
}
雖然我們已經把邏輯查詢部分移出了controller,但是仍然需要查詢和測試。它不再是單元測試的一部分,而是一個集成測試,鍛煉概念上的respository實例訪問數據庫。
依賴注入(Dependence Injection)
調把依賴性傳進對象的構造函中的技術被稱為依賴注入,我們已經手動地完成了依賴注入,通過在我們的類中添加很多構造函數。在第18章,我們將學習怎樣使用依賴注入容器去避免這個多構造函數的需求。更多關於依賴注入的信息 http://manning.com/seeman
此時我們已經有能力繞開數據庫測試我們的controller actions了,我們達到這個目的,我們要在我們的IGuestbookRepository接口的實現類做些假數據。我們將創建一個新類,實現這個接口,把所有的操作放進內存中,我們將使用mocking framework比如說moq,RHino Mocks(都可以通過Nuget安裝),他們能夠為我們自動創建含有假數據的接口的實現類
1: using Guestbook.Contract;
2: using GuestInfo.Models;
3: using System;
4: using System.Collections.Generic;
5: using System.Linq;
6: using System.Web;
7:
8: namespace GuestInfo.Interface
9: {
10: public class FakeGuestbookRepository : IGuestbookRepository
11: {
12: private List<GuestbookEntry> _entries
13: = new List<GuestbookEntry>();
14:
15: public IList<GuestbookEntry> GetMostRecentEntries()
16: {
17: return new List<GuestbookEntry>
18: {
19: new GuestbookEntry
20: {
21: DateAdded = new DateTime(2011, 6, 1),
22: Id = 1,
23: Message = "Test message",
24: name = "Jeremy"
25: }
26: };
27: }
28:
29: public void AddEntry(GuestbookEntry entry)
30: {
31: _entries.Add(entry);
32: }
33: public GuestbookEntry FindById(int id)
34: {
35: return _entries.SingleOrDefault(x => x.Id == id);
36: }
37:
38: public IList<CommentSummary> GetCommentSummary()
39: {
40: return new List<CommentSummary>
41: {
42: new CommentSummary
43: {
44: UserName = "Jeremy", NumberOfComments = 1
45: }
46: };
47: }
48: }
49: }
這個假實現暴露了相同的方法作為一個真實的版本,
除了那些簡單的內存的集合,GetCommentSummary和GetMostRecentEntries方法是不能夠響應獲得的,其他方法都可以獲得進行測試。新建一個單元測試的項目
這是兩個測試用例
1: [TestMethod]
2: public void Index_RendersView()
3: {
4: var controller = new GuestbookController(
5: new FakeGuestbookRepository());
6: var result = controller.Index() as ViewResult;
7: Assert.IsNotNull(result);
8: }
9:
10: [TestMethod]
11: public void Index_gets_most_recent_entries()
12: {
13: var controller = new GuestbookController(
14: new FakeGuestbookRepository());
15: var result = (ViewResult)controller.Index();
16: var guestbookEntries = (IList<GuestbookEntry>) result.Model;
17: Assert.AreEqual(1, guestbookEntries.Count);
18:
19: }