7 天玩转 ASP.NET MVC — 第 6 天
目錄
第 1 天
第 2 天
第 3 天
第 4 天
第 5 天
第 6 天
第 7 天
0. 前言
歡迎來到第六天的 MVC 系列學(xué)習(xí)中。希望你在閱讀此篇文章的時候,已經(jīng)學(xué)習(xí)了前五天的內(nèi)容,這也是第六天學(xué)習(xí)的前提條件。
1. Lab 27 — 添加批量上傳選項
在這個實驗中,我們將會創(chuàng)建一個選項,用于從 CSV 文件中上傳多個 Employees。
我們將會做兩件事。
1. 學(xué)會如何運用文件上傳控件。
異步控制器。
第一步:創(chuàng)建 FileUploadViewModel
在 ViewModels 文件夾下創(chuàng)建一個類,命名為 FileUploadViewModel。
publicclassFileUploadViewModel:BaseViewModel
{
publicHttpPostedFileBase fileUpload {get;set;}
}
HttpPostedFileBase 將會通過客戶端提供上傳文件的訪問入口。
第二步:創(chuàng)建 BulkUploadController 和 Index 行為方法
創(chuàng)建一個新的控制器,命名為 BulkUploadController,以及一個行為方法,命名為 Index。
publicclassBulkUploadController:Controller
{
[HeaderFooterFilter]
[AdminFilter]
publicActionResultIndex()
{
returnView(newFileUploadViewModel());
}
}
正 如你所看見的,Index 行為方法附上了 HeaderFooterFilter 和 AdminFilter 屬性。HeaderFooterFilter 確保了正確了頁眉和頁腳數(shù)據(jù)傳輸?shù)?ViewModel,AdminFilter 限制了 Non-Admin 用戶訪問行為方法。
第三步:創(chuàng)建上傳視圖
為上述行為方法創(chuàng)建一個視圖。
需要注意的是,視圖的名稱應(yīng)該為 Index.cshtml,并且應(yīng)該放置在「~/Views/BulkUpload」文件夾下。
第四步:設(shè)計上傳視圖
在視圖中放置如下內(nèi)容。
@usingWebApplication1.ViewModels
@modelFileUploadViewModel
@{
Layout="~/Views/Shared/MyLayout.cshtml";
}
@sectionTitleSection{
BulkUpload
}
@sectionContentBody{
<div>
<a href="/Employee/Index">Back</a>
<form action="/BulkUpload/Upload" method="post" enctype="multipart/form-data">
SelectFile:<input type="file" name="fileUpload" value=""/>
<input type="submit" name="name" value="Upload"/>
</form>
</div>
}
正如你所看見的,在 FileUploadViewModel 中,屬性的名稱和 input[type="file"] 的名稱是一樣的,都是「FileUpload」。我們在 Model Binder 實驗中已經(jīng)講述了名稱屬性的重要性。
注意:在 Form 標(biāo)簽中,有一個額外的指定加密屬性,我們將會在實驗結(jié)尾處討論它。
第五步:創(chuàng)建業(yè)務(wù)層上傳方法
在 EmployeeBusinessLayer 中創(chuàng)建一個新的方法,命名為 UploadEmployees。
publicvoidUploadEmployees(List<Employee> employees)
{
SalesERPDAL salesDal =newSalesERPDAL();
salesDal.Employees.AddRange(employees);
salesDal.SaveChanges();
}
第六步:創(chuàng)建上傳行為方法
在 BulkUploadController 中創(chuàng)建一個新的行為方法,命名為 Upload。
[AdminFilter]
publicActionResultUpload(FileUploadViewModel model)
{
List<Employee> employees =GetEmployees(model);
EmployeeBusinessLayer bal =newEmployeeBusinessLayer();
bal.UploadEmployees(employees);
returnRedirectToAction("Index","Employee");
}
privateList<Employee>GetEmployees(FileUploadViewModel model)
{
List<Employee> employees =newList<Employee>();
StreamReader csvreader =newStreamReader(model.fileUpload.InputStream);
csvreader.ReadLine();// Assuming first line is header
while(!csvreader.EndOfStream)
{
var line = csvreader.ReadLine();
var values = line.Split(',');//Values are comma separated
Employee e =newEmployee();
e.FirstName= values[0];
e.LastName= values[1];
e.Salary=int.Parse(values[2]);
employees.Add(e);
}
return employees;
}
在 Upload 中附上 AdminFilter 是用于限制 Non-Admin 用戶訪問。
第七步:為 BulkUpload 創(chuàng)建鏈接
在「Views/Employee」文件夾下打開 AddNewLink.cshtml 文件,為 BulkUpload 附上鏈接。
<ahref="/Employee/AddNew">Add New</a>
<ahref="/BulkUpload/Index">BulkUpload</a>
第八步:執(zhí)行并測試
為測試創(chuàng)建一個簡單的文件
創(chuàng)建一個簡單的文件如下,然后將其保存在電腦中。
執(zhí)行并測試
按下 F5,然后執(zhí)行應(yīng)用。完成登錄操作,然后通過點擊鏈接導(dǎo)航到 BulkUpload 選項。
選擇一個文件,然后點擊上傳。
注意:在上述的例子中,我們沒有在視圖中用到任何客戶端或者服務(wù)器端的認(rèn)證。它也許會導(dǎo)致如下的錯誤。
「Validation failed for one or more entities. See 'EntityValidationErrors' property for more details.」
為了發(fā)現(xiàn)這個錯誤的確切原因,只需要在異常發(fā)生的時候添加如下的表達式。
((System.Data.Entity.Validation.DbEntityValidationException)$exception).EntityValidationErrors。表達式「$exception」呈現(xiàn)了任何從當(dāng)前上下文中拋出的錯誤,即使它沒有被捕獲或者支配到一個變量中。
Lab 27 的 Q&A
為什么我們沒有在這里用到認(rèn)證?
為選項增加客戶端和服務(wù)器端的認(rèn)證將會留給讀者完成,我在這里給出一些暗示。
運用 Data Annotations 來進行服務(wù)器端的認(rèn)證。
你可以運用 Data Annotations 或者實現(xiàn) JQuery Unobtrusive Validation 來實現(xiàn)客戶端認(rèn)證。明顯的是,這一次你需要手動設(shè)置自定義數(shù)據(jù)屬性,因為我們沒有為文件輸入創(chuàng)建 HtmlHelper 方法。
對于客戶端的認(rèn)證,你可以寫一些自定義的 JavaScript,然后通過點擊安全觸發(fā)它。這并不是很難,因為文件輸入是一個輸入控件,值可以通過在 JavaScript 中獲取并認(rèn)證。
什么是HttpPostedFileBase?
HttpPostedFileBase 可以通過客戶端提供文件上傳的訪問接口。Model Binder 將會在發(fā)送 Post 請求時更新所有 FileUploadViewModel 類的屬性值?,F(xiàn)在 FileUploadViewModel 里只有一個屬性值,Model Binder 將會通過客戶端來設(shè)置這個屬性值,實現(xiàn)文件上傳。
提供多個文件輸入控件是否可行?
答案是肯定的。我們可以通過兩種方式實現(xiàn)它。
創(chuàng) 建多個文件輸入控件。每一個控件都需要有唯一的名字。在 FileUploadViewModel 類中為每個控件創(chuàng)建一個 HttpPostedFileBase 的類型屬性。每一個屬性的名稱應(yīng)該與控件的名稱相匹配。剩下的工作會由 ModelBinder 來處理。
創(chuàng)建多個文件輸入控件。每一個控件都需要有唯一的名字。這次不是創(chuàng)建多個 HttpPostedFileBase 的屬性,而是創(chuàng)建一個類型 List。
注意:上述的情形對于所有控件都可行。當(dāng)你擁有多個相同名稱的控件時,如果要更新的屬性值是一個簡單參數(shù),Model Binder 將會更新第一個控件的屬性值。如果更新的屬性值是一個 List,Model Binder 會將每一個屬性值設(shè)置到控件中。
enctype="multipart/form-data"是用于做什么的?
這個對知道與否并不重要,但是知道確實會好一點。
這個屬性指定了編碼類型,在傳輸數(shù)據(jù)時使用。屬性的默認(rèn)值是「application/x-www-form-urlencoded」。
例如,我們的登錄表單將會隨著 Post 請求向服務(wù)器發(fā)送如下數(shù)據(jù)。
POST /Authentication/DoLogin HTTP/1.1
Host: localhost:8870
Connection: keep-alive
Content-Length:44
Content-Type: application/x-www-form-urlencoded
...
...
UserName=Admin&Passsword=Admin&BtnSubmi=Login
當(dāng) enctype="multipart/form-data"屬性被添加到表單標(biāo)簽時,隨著 Post 請求會發(fā)送到服務(wù)器上。
POST /Authentication/DoLogin HTTP/1.1
Host: localhost:8870
Connection: keep-alive
Content-Length:452
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarywHxplIF8cR8KNjeJ
...
...
------WebKitFormBoundary7hciuLuSNglCR8WC
Content-Disposition: form-data; name="UserName"
Admin
------WebKitFormBoundary7hciuLuSNglCR8WC
Content-Disposition: form-data; name="Password"
Admin
------WebKitFormBoundary7hciuLuSNglCR8WC
Content-Disposition: form-data; name="BtnSubmi"
Login
------WebKitFormBoundary7hciuLuSNglCR8WC—
正如你所看見的,表單以多個部分被發(fā)送。每一個部分都通過 Content-Type 被一條邊界線所分隔,并且每一個部分都包含一個值。
如果表單標(biāo)簽中包含文件輸入控件時,編碼類型需要設(shè)定為「multipart/form-data」。
注意:每一次請求發(fā)生時,邊界線會隨機生成。你可能會看到不同的邊界線。
為什么我們不總是將 EncTyp 設(shè)置為「multipart/form-data」?
當(dāng) EncTyp 被設(shè)置為「multipart/form-data」,它將會做兩件事,Post 數(shù)據(jù)以及上傳文件。這就是為什么我們不總是將其設(shè)置為「multipart/form-data」。
答案就是,這樣會增加請求的總體大小。請求的大小越大,意味著性能越差。因為最佳實踐應(yīng)該是將其設(shè)置為默認(rèn)的值,即「application/x-www-form-urlencoded」。
為什么我們需要創(chuàng)建 ViewModel?
在我們的視圖中有一個控件。我們可以通過直接向 HttpPostedFileBase 類型增加一個參數(shù)來實現(xiàn)同樣的結(jié)果,這里我們需要在上傳方法中命名為 「fileUpload」,而不是創(chuàng)建一個單獨的 ViewModel。代碼如下所示。
publicActionResultUpload(HttpPostedFileBase fileUpload)
{
}
創(chuàng)建 ViewModel 是最佳實踐。Controller 應(yīng)該總是向視圖發(fā)送以 ViewModel 為格式的數(shù)據(jù),并且來自視圖的數(shù)據(jù)應(yīng)該以 ViewModel 發(fā)送給 Controller。
2. 上述解決方案的問題
你是否想知道,當(dāng)你發(fā)送一個請求時,如何獲得響應(yīng)的?
現(xiàn)在不要去說,是通過行為方法接到請求然后怎樣怎樣的。盡管這是正確的答案,我仍然期望一些不同的答案。我的問題是在最開始的時候發(fā)生了什么。
一個簡單的編程規(guī)則,程序中所有都通過線程執(zhí)行,盡管是請求。
在 Web 服務(wù)器上的 ASP.NET,.NET Framework 維護著線程池。每一次請求發(fā)送到 Web 服務(wù)器上時,就會把一個線程池中一個空閑的線程分配給服務(wù)器,用于處理請求。這個線程被稱為 Worker 線程。
Worker 線程在請求正常處理的過程中處于阻塞狀態(tài),并且不能處理其它請求。
現(xiàn) 在來假設(shè)一種場景,一個應(yīng)用接收到了很多請求,并且每個請求都會花費許多時間來處理進程。在這種情形下,沒有 Worker 線程可用于服務(wù)器請求,所以當(dāng)新的請求想要獲取該線程進行處理狀態(tài)時,我們可能需要在這時候終止它。這個我們稱之為 Thread Starvation(線程饑餓)。
在我們的例子樣本文件中,只存在了兩個雇員記錄,而在真實場景中,可能存在成千上萬的記錄,這意味著請求也許會花費大量時間來完成進程。這樣會導(dǎo)致線程饑餓。
解決方案
迄今為止我們所討論的請求都是同步請求類型。
如果客戶端發(fā)出的是異步請求,而不是同步請求,那么線程饑餓的問題就解決了。
在異步請求的情形下,請求將會從線程池分配中獲得通常的 Worker 線程,用于服務(wù)請求。
Worker 線程將會初始化異步操作,然后返回線程池來服務(wù)其它請求。異步操作將會繼續(xù)被 CLR 線程處理。
現(xiàn)在的問題是,CLR 線程不能返回響應(yīng),所以一旦當(dāng)完成異步操作后,它就會通知 ASP.NET。
Web 服務(wù)器將會再一次從線程池中得到 Worker 線程,用于處理剩余的請求和響應(yīng)。
在上述的完整的場景中,兩個 Worker 線程從線程池中獲取。這兩個 Worker 線程也許是同一個,也許不是。
在我們的例子中,文件讀取是通過 I/O 操作的,這個操作不需要 Worker 線程來處理。所以最好是將同步請求轉(zhuǎn)換為異步請求。
異步請求會提升響應(yīng)時間嗎?
答案是否定的。響應(yīng)時間是相同的。這里線程將會被釋放,用于服務(wù)其它請求。
3. Lab 28 — 解決線程饑餓問題
在 ASP.NET MVC 中,我們可以通過轉(zhuǎn)換同步行為方法到異步行為方法,來將同步請求轉(zhuǎn)換為異步請求。
第一步:創(chuàng)建異步控制器
將 UploadController 的基類改為AsynController。
publicclassBulkUploadController:AsyncController
{
第二步:轉(zhuǎn)換同步行為方法到異步行為方法
通過關(guān)鍵字,「async」和「await」,可以很容易做這件事。
[AdminFilter]
public async Task<ActionResult>Upload(FileUploadViewModel model)
{
int t1 =Thread.CurrentThread.ManagedThreadId;
List<Employee> employees = await Task.Factory.StartNew<List<Employee>>
(()=>GetEmployees(model));
int t2 =Thread.CurrentThread.ManagedThreadId;
EmployeeBusinessLayer bal =newEmployeeBusinessLayer();
bal.UploadEmployees(employees);
returnRedirectToAction("Index","Employee");
}
正如你所看見的,我們在行為方法的開始和結(jié)束的地方將線程 ID 存儲在變量中。
現(xiàn)在讓我理解下代碼。
當(dāng)客戶端點擊上傳按鈕時,一個新的請求將被發(fā)送到服務(wù)器。
Webserver 從線程池中獲取一個 Worker 線程,然后將其分配給請求用于服務(wù)。
Worker 線程使得行為方法用于執(zhí)行。
Worker 方法通過 Task.Factory.StartNew 方法執(zhí)行異步操作。
正如你所看見的,行為方法通過關(guān)鍵字 Async被標(biāo)記為異步的,這將會確保一旦異步方法操作開始執(zhí)行,Worker 線程就會得到釋放。這個時候邏輯的異步操作將會通過獨立的 CLR 線程繼續(xù)在后臺執(zhí)行。
現(xiàn)在異步操作調(diào)用將被標(biāo)記為 Await 關(guān)鍵字。這將會確保接下來的代碼行不會被執(zhí)行,除非異步操作完成。
一旦異步操作完成了,接下來的行為方法中的代碼就需要被執(zhí)行。因此又要需要一個 Worker 線程。因此 Webserver 將會從線程池中取出一個空閑線程,然后將其分配給剩余的請求用于服務(wù),并返回響應(yīng)。
第三步:執(zhí)行并測試
執(zhí)行應(yīng)用。導(dǎo)航到 BulkUpload 選項。
在你做任何操作之前,先導(dǎo)航到代碼,然后在最后一行代碼中打個斷點。
現(xiàn)在選擇一個簡單的文件,然后點擊 Upload。
正如你所看見的,在方法的開始和結(jié)束時,線程 ID 是不同的。輸出的結(jié)果和之前的實驗結(jié)果一樣。
4. Lab 29 — 異常處理 — 呈現(xiàn)自定義錯誤頁面
如果一個項目沒有正確的異常處理,就不能算是一個完整的項目。
迄今為止,我們討論過 ASP.NET MVC 中的兩個過濾器,即 Action 過濾器和 Authentication 過濾器?,F(xiàn)在是時候討論第三個過濾器了,即 Exception 過濾器。
什么是 Exception 過濾器?
Exception 過濾器的使用方式同其它過濾器一樣。我們將以屬性的方式運用。
運用 Exception 過濾器的步驟。
使它們可用
將它們作為行為方法或者控制器的屬性。我們也可以將它們應(yīng)用到 Global 級別。
它們是用來做什么的?
一旦在行為方法內(nèi)部發(fā)生異常時,Exception 過濾器就將會控制執(zhí)行并開始自動執(zhí)行其內(nèi)部的代碼。
是否存在自動的 Exception 過濾器?
ASP.NET MVC 提供給我們一個已經(jīng)編寫好的 Exception 過濾器,稱作 HandleError。
正 如我們之前所說的,當(dāng)行為方法中,一旦異常發(fā)生,過濾器就將被執(zhí)行。這個過濾器將會在「~/Views/[current controller]」或者「~/Views/Shared」文件夾內(nèi)發(fā)現(xiàn)一個名稱為「Error」的視圖,為這個視圖創(chuàng)建一個 ViewResult,然后返回響應(yīng)。
讓我們看一個 Demo,用于更好地理解。在項目的實驗最后,我們將會實現(xiàn) BulkUpload 選項?,F(xiàn)在存在著較高的輸入文件的錯誤可能性。
第一步:創(chuàng)建一個簡單的帶有錯誤的 Upload 文件
創(chuàng)建一個簡單的上傳文件,就像之前一樣。但是這次,文件中包含一些非法值。
正如你所看見的,Salary 是非法的。
第二步:執(zhí)行并測試應(yīng)用
按下 F5,執(zhí)行應(yīng)用。導(dǎo)航到 Bulk Upload 選項,選擇上述的文件,然后點擊 Upload。
第三步:使異常過濾器可用
自定義異常開啟后,異常過濾器也被開啟。為了開啟自定義異常,打開 Web.config 文件,然后導(dǎo)航到 System.Web 區(qū)域,在該區(qū)域下增加自定義錯誤,如下所示。
<system.web>
<customErrorsmode="On"></customErrors>
第四步:創(chuàng)建錯誤視圖
在「~Views/Shared」文件夾下,可以看到一個文件,即「Error.cshtml」。這個文件作為 MVC 樣本文件的一部分在開始的時候被創(chuàng)建。如果沒有被創(chuàng)建,就手動創(chuàng)建。
@{
Layout=null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width"/>
<title>Error</title>
</head>
<body>
<hgroup>
<h1>Error.</h1>
<h2>An error occurred while processing your request.</h2>
</hgroup>
</body>
</html>
第五步:附上 Exception 過濾器
正如我們之前所討論的,一旦我們使異常過濾器可用,我們將會把它綁定到一個行為方法或者控制器中。
好的消息是我們無需手動附上過濾器。
在 App_Start 文件夾下打開 FilterConfig.cs 文件。在 RegisterGlobalFilter 方法下,你可以看到 HandleError 過濾器已經(jīng)被附上 Global 級別。
publicstaticvoidRegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(newHandleErrorAttribute());//ExceptionFilter
filters.Add(newAuthorizeAttribute());
}
如果需要移除 Global 過濾器,將會被附上方法或者控制器級別。
[AdminFilter]
[HandleError]
public async Task<ActionResult>Upload(FileUploadViewModel model)
{
但是不建議這么做,最好還是應(yīng)用 Global 級別。
第六步:執(zhí)行并測試
像之前的方式一樣,讓我們來看一下應(yīng)用的測試結(jié)果。
第七步:在視圖中展示錯誤信息
為了達到這個目的,我們需要將錯誤視圖轉(zhuǎn)換為 HandleErrorInfo 類的強類型視圖,然后在視圖中展示錯誤信息。
@modelHandleErrorInfo
@{
Layout=null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width"/>
<title>Error</title>
</head>
<body>
<hgroup>
<h1>Error.</h1>
<h2>An error occurred while processing your request.</h2>
</hgroup>
ErrorMessage:@Model.Exception.Message<br />
Controller:@Model.ControllerName<br />
Action:@Model.ActionName
</body>
</html>
第八步:執(zhí)行并測試
這次測試結(jié)果,我們將會得到如下的錯誤視圖。
我們是否錯失了什么?
Handle Error 屬性確保了無論何時行為方法發(fā)生異常時,自定義視圖都會被呈現(xiàn)。但是僅限于控制器和行為方法。它不會處理「Resource not found」錯誤。
執(zhí)行應(yīng)用,輸入一些古怪的 URL。
第九步:創(chuàng)建 ErrorController
在 Controller 文件夾下創(chuàng)建一個名為 ErrorController 的控制器,然后創(chuàng)建一個行為方法,命名為 Index。
publicclassErrorController:Controller
{
// GET: Error
publicActionResultIndex()
{
Exception e=newException("Invalid Controller or/and Action Name");
HandleErrorInfo eInfo =newHandleErrorInfo(e,"Unknown","Unknown");
returnView("Error", eInfo);
}
}
HandleErrorInfo 控制器擁有三個參數(shù),即異常對象,控制器名稱和行為方法名稱。
第十步:在非法的 URL 中呈現(xiàn)自定義錯誤視圖
在 Web.config 中設(shè)定「Resource not found error」定義。
<system.web>
<customErrorsmode="On">
<errorstatusCode="404"redirect="~/Error/Index"/>
</customErrors>
第十一步:使所有人可訪問 ErrorController
在 ErrorController 中應(yīng)用 AllowAnonymous 屬性,Index 方法不應(yīng)該被綁定到一個有權(quán)限的用戶。因為用戶可能在登錄前就輸入了非法的 URL。
[AllowAnonymous]
publicclassErrorController:Controller
{
第十二步:執(zhí)行并測試
執(zhí)行應(yīng)用程序,然后在瀏覽器地址欄輸入一些非法的 URL。
Lab 29 的 Q&A
可以改變視圖的名稱嗎?
答案是肯定的,保持視圖名稱為「Error」不是總是必須的。
在這種情形下,當(dāng)附上 HandleError 過濾器時,我們需要指定視圖的名稱。
[HandleError(View="MyError")]或者是
filters.Add(newHandleErrorAttribute()
{
View="MyError"
});
對于不同的異常,獲取不同的錯誤視圖,是否可行?
答案是肯定的,這是可行的。在這種情形下,我們需要應(yīng)用 Handle Error 過濾器多次。
[HandleError(View="DivideError",ExceptionType=typeof(DivideByZeroException))]
[HandleError(View="NotFiniteError",ExceptionType=typeof(NotFiniteNumberException))]
[HandleError]
或者是
filters.Add(newHandleErrorAttribute()
{
ExceptionType=typeof(DivideByZeroException),
View="DivideError"
});
filters.Add(newHandleErrorAttribute()
{
ExceptionType=typeof(NotFiniteNumberException),
View="NotFiniteError"
});
filters.Add(newHandleErrorAttribute());
在上述的例子中,我們增加了三個 Handle Error 過濾器。前兩個為指定的異常,而后一個更加通用一些,它將會為所有其它異常展示錯誤視圖。
5. 理解上述實驗的局限
上述實驗存在唯一的局限,便是我們沒有將異常日志輸出。
6. Lab 30 — 異常處理 — 異常日志
第一步:創(chuàng)建 Logger 類
在項目的根目錄下創(chuàng)建一個新的文件夾,稱為 Logger。
在 Logger 文件夾下創(chuàng)建一個類,命名為 FileLogger。
namespaceWebApplication1.Logger
{
publicclassFileLogger
{
publicvoidLogException(Exception e)
{
File.WriteAllLines("C://Error//"+DateTime.Now.ToString("dd-MM-yyyy mm hh ss")+".txt",
newstring[]
{
"Message:"+e.Message,
"Stacktrace:"+e.StackTrace
});
}
}
}
第二步:創(chuàng)建 EmployeeExceptionFilter 類
在 Filters 文件夾下創(chuàng)建一個新的類,命名為 EmployeeExceptionFilter。
namespaceWebApplication1.Filters
{
publicclassEmployeeExceptionFilter
{
}
}
第三步:擴展 Handle Error 用于實現(xiàn)日志記錄
讓 EmployeeExceptionFilter 類繼承 HandleErrorAttribute 類,然后重寫 OnException 方法。
publicclassEmployeeExceptionFilter:HandleErrorAttribute
{
publicoverridevoidOnException(ExceptionContext filterContext)
{
base.OnException(filterContext);
}
}
注意:確保在 HandleErrorAttribute 類中的頂部引用了 System.Web.MVC。
第四步:定義 OnException 方法
在 OnException 方法中包含異常日志記錄代碼,如下所示。
publicoverridevoidOnException(ExceptionContext filterContext)
{
FileLogger logger =newFileLogger();
logger.LogException(filterContext.Exception);
base.OnException(filterContext);
}
第五步:改變默認(rèn)的異常過濾器
打開 FilterConfig.cs 文件,移除 HandleErrorAttribute,然后附上我們上一步驟中所創(chuàng)建的。
publicstaticvoidRegisterGlobalFilters(GlobalFilterCollection filters)
{
//filters.Add(new HandleErrorAttribute());//ExceptionFilter
filters.Add(newEmployeeExceptionFilter());
filters.Add(newAuthorizeAttribute());
}
第六步:執(zhí)行并測試
首先在 C 盤下創(chuàng)建一個文件夾,命名為「Error」。這個文件夾會存放錯誤的日志文件。
注意:可以更改路徑為你所期望的路徑。
按下 F5,然后執(zhí)行應(yīng)用。導(dǎo)航到 Bulk Upload 選項。選擇文件,然后點擊 Upload。
這次的輸出將會有所不同,我們將會得到一些錯誤視圖,就像之前一樣。唯一的不同便是我們會在「C:\Errors」文件夾發(fā)現(xiàn)一些錯誤日志文件。
Lab 30 的 Q&A
異常發(fā)生時,錯誤視圖是如何作為響應(yīng)返回的?
在上述實驗中,我們重寫了 OnException 方法,然后實現(xiàn)了異常日志的功能。現(xiàn)在的問題是,默認(rèn)的錯誤處理過濾器是如何繼續(xù)工作的?答案是簡單地,查看 OnException 方法的最后一行代碼。
base.OnException(filterContext);
這意味著,基類 OnException 將會做剩余的工作,基類 OnException 將會返回錯誤視圖的 ViewResult。
在 OnException 中,我們可以返回其它結(jié)果嗎?
答案是肯定的,查看如下代碼。
publicoverridevoidOnException(ExceptionContext filterContext)
{
FileLogger logger =newFileLogger();
logger.LogException(filterContext.Exception);
//base.OnException(filterContext);
filterContext.ExceptionHandled=true;
filterContext.Result=newContentResult()
{
Content="Sorry for the Error"
};
}
當(dāng)我們想要返回自定義響應(yīng)時,首先要做的事便是,通知 MVC 引擎,告知其我們已經(jīng)手動處理異常了,所以不需要做默認(rèn)的行為,即不需要呈現(xiàn)默認(rèn)的錯誤屏幕。這一切可以通過如下代碼來實現(xiàn)。
filterContext.ExceptionHandled=true
7. 路由
迄今為止我們討論過許多概念,我們也回答了許多有關(guān) MVC 的問題,但是除了一個基本和重要的概念。
「當(dāng)用戶發(fā)出請求時,確切發(fā)生了什么」?
一個很好的答案便是「行為方法的執(zhí)行」。但是確切的答案是控制器和犯法是如何被一個特定的 URL 請求識別的?
當(dāng)我們開始「實現(xiàn)用戶友好的 URLs」的實驗時,我們首先需要回答上述的問題。你也許會奇怪為什么這個主題會放置到最后。我故意將其放置到最后,是因為我想讓更多的人在理解內(nèi)部之前,先了解 MVC。
理解 RouteTable
在 ASP.NET MVC 中,存在一個概念,稱作 RouteTable。這里存儲了應(yīng)用的 URL 路由。用簡單的話說,它承載了一個應(yīng)用的 URL 模式的集合。
默認(rèn)情況下,一個路由將會作為項目模板的一部分被添加??梢酝ㄟ^ Global.asax 文件查看它。在 Application_Start 中,你將會發(fā)現(xiàn)如下的代碼。
RouteConfig.RegisterRoutes(RouteTable.Routes);
你將會在 App_Start 文件夾下發(fā)現(xiàn) RouteConfig.cs 文件,它包含了如下代碼。
namespaceWebApplication1
{
publicclassRouteConfig
{
publicstaticvoidRegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name:"Default",
url:"{controller}/{action}/{id}",
defaults:new{ controller ="Home", action ="Index", id =UrlParameter.Optional}
);
}
}
}
正如你所看見的,RegisterRoutes 方法已經(jīng)通過 Route.MapRoutes 方法定義了一個默認(rèn)的路由。
在 RegisterRoutes 方法中定義的路由將會在 ASP.NET MVC 請求周期中被用到,用于決定執(zhí)行確切的控制器和方法。
如果需要,我們可以通過使用 Route.MapRoutes 函數(shù),創(chuàng)建多個路由。內(nèi)部定義路由意味著創(chuàng)建 Route 對象。
MapRoute 函數(shù)也可以把路由對象附上 RouteHandler,這樣將會是 MVCRouteHandler。
理解 ASP.NET MVC 請求周期
在我們開始之前,你需要清楚,我們將要 100% 地解釋請求周期。我們將要接觸到之前未講到的重要概念。
第一步:UrlRoutingModule
當(dāng)終端用戶發(fā)出請求后,首先會通過 UrlRoutingModule 對象。UrlRoutingModule 是一個 HTTP 模塊。
第二步:路由
UrlRoutingModule 首先會從路由集合中匹配 Route 對象。對于匹配,請求的 URL 將會與路由中定義的 URL 模式相對比。
下述的規(guī)則將會在匹配中被考慮到。
請求 URL 中參數(shù)的數(shù)字以及在路由中定義的 URL 模式。例如:
URL 模式中定義的可選參數(shù)。例如:
在參數(shù)中定義的靜態(tài)參數(shù)。
第三步:創(chuàng)建 MVC Route Handler
一旦路由對象被選中,UrlRoutingModule 將會從路由對象中獲得 MvcRouteHandler。
第四步:創(chuàng)建 RouteData 和 RequestContext
UrlRoutingModule 對象將會通過 Route 對象創(chuàng)建 RouteData,它將會用于創(chuàng)建 RequestContext。
RouteData 封裝了關(guān)于路由的信息,如控制器的名稱,行為方法的名稱,路由參數(shù)的值。
Controller 名稱
為了從請求 URL 中獲得控制器的名稱,需要遵循如下的簡單規(guī)則。即“在 URL 模式中{Controller} 是識別控制器名稱的關(guān)鍵詞”。
例如:
當(dāng)URL 模式是 {Controller}/{Action}/{Id},而請求 URL 是「http://localhost:8870/BulkUpload/Upload/5」時,BulkUpload 是控制器的名稱。
當(dāng) URL 模式是 {Action}/{Controller}/{Id},而請求 URL 是 「http://localhost:8870/BulkUpload/Upload/5」時,Upload 是控制器的名稱。
行為方法名稱
為了獲得請求 URL 中的行為方法,需要遵循如下的簡單規(guī)則。即「在 URL 模式中 {Action} 是行為方法名稱的關(guān)鍵詞」。
例如:
當(dāng)URL 模式是 {Controller}/{Action}/{Id},而請求 URL 是「http://localhost:8870/BulkUpload/Upload/5」時,Upload 是行為方法的名稱。
當(dāng) URL 模式是 {Action}/{Controller}/{Id},而請求 URL 是 「http://localhost:8870/BulkUpload/Upload/5」時,BulkUpload 是行為方法的名稱。
路由參數(shù)
一個基本的 URL 模式包含如下四個要素。
{Controller},用于識別控制器名稱。
{Action},識別行為方法名稱。
一些字符串,例如「MyCompany/{Controller}/{Action}」,在這個模式中,「MyCompany」是一個必須的字符串。
{Something},例如「{Controller}/{Action}/{Id}」,在這個模式中「Id」是路由參數(shù)。在請求的 URL 中,路由參數(shù)可以被用于獲取 URL 的值。
我們來看一下如下示例。
路由模式是 {Controller}/{Action}/{Id}。
請求 URL 是「http://localhost:8870/BulkUpload/Upload/5」。
測試一:
publicclassBulkUploadController:Controller
{
publicActionResultUpload(string id)
{
//value of id will be 5 -> string 5
...
}
}
測試二:
publicclassBulkUploadController:Controller
{
publicActionResultUpload(int id)
{
//value of id will be 5 -> int 5
...
}
}
測試三:
publicclassBulkUploadController:Controller
{
publicActionResultUpload(stringMyId)
{
//value of MyId will be null
...
}
}
第五步:創(chuàng)建 MVCHandler
MvcRouteHandler 將會創(chuàng)建 MVCHandler 的實例,傳輸 RequestContext 對象。
第六步:創(chuàng)建控制器實例
MVCHandler 將會通過 ControllerFactory(默認(rèn)的是 DefaultControllerFactory) 創(chuàng)建控制器實例。
第七步:執(zhí)行方法
MVCHandler 將會觸發(fā)控制器的執(zhí)行方法。執(zhí)行方法在控制器基類中被定義。
第八步:觸發(fā)行為方法
每一個控制器都與一個 ControllerActionInvoker 對象相關(guān)聯(lián)。在執(zhí)行方法中,ControllerActionInvoker 觸發(fā)正確的行為方法。
第九步:執(zhí)行結(jié)果
行為方法接收到用戶的輸入,然后準(zhǔn)備合適的響應(yīng)數(shù)據(jù),并通過返回一個類型來執(zhí)行結(jié)果?,F(xiàn)在返回的結(jié)果可能是 ViewResult,可能是 RedirectToRoute 結(jié)果或者可能是其它。
現(xiàn)在,我相信你已經(jīng)對路由的概念有了很好的理解,所以讓我們通過路由來使得項目的 URLs 更友好吧。
8. Lab 31 — 實現(xiàn)用戶友好性的 URLs
第一步:重新定義 RegisterRoutes 方法
在 RegisterRoutes 方法中包含額外的路由。
publicstaticvoidRegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name:"Upload",
url:"Employee/BulkUpload",
defaults:new{ controller ="BulkUpload", action ="Index"}
);
routes.MapRoute(
name:"Default",
url:"{controller}/{action}/{id}",
defaults:new{ controller ="Home", action ="Index", id =UrlParameter.Optional}
);
}
正如你所看見的,我們現(xiàn)在已經(jīng)不止定義一個路由了。
第二步:更改 URL 引用
從「~/Views/Employee」文件夾下打開 AddNewLink.cshtml 文件,然后更改 BulkUpload 鏈接如下。
<a href="/Employee/BulkUpload">BulkUpload</a>
第三步:執(zhí)行并測試
執(zhí)行應(yīng)用,將會看到神奇的地方。
正如你所看見的,URL 不再是“Controller/Action”的形式。它看起來更加用戶友好,但是輸出是一樣的。
我建議你定義更多的路由,嘗試更多的 URLs。
Lab 31 的 Q&A
之前的 URL 還是否起作用?
答案是肯定的,之前的 URL 也會起作用。
現(xiàn)在 BulkUploadController 中的 Index 方法可以通過兩個 URLs 訪問。
http://localhost:8870/Employee/BulkUpload
http://localhost:8870/BulkUpload/Index
默認(rèn)路由中的「Id」是什么?
我們之前提到過它。它被稱作路由參數(shù)。它可以通過 URL 來用于獲取值。它是一個可被替換的查詢字符串。
路由參數(shù)和查詢字符串的區(qū)別是什么?
查詢字符串有大小限制,然而我們可以定義路由參數(shù)的任意數(shù)字。
我們不能向查詢字符串值添加限制,但是我們可以向路由參數(shù)添加限制。
可以設(shè)定路由參數(shù)的默認(rèn)值,然而查詢字符串的默認(rèn)值不可設(shè)定。
查詢字符串使得 URL 凌亂,但是路由參數(shù)保持 URL 整潔。
如何向路由參數(shù)應(yīng)用限制?
可以通過正則表達式來完成這件事。例如,查看如下路由。
routes.MapRoute(
"MyRoute",
"Employee/{EmpId}",
new{controller=" Employee ", action="GetEmployeeById"},
new{EmpId=@"\d+"}
);
行為方法將如下所示。
publicActionResultGetEmployeeById(intEmpId)
{
...
}
現(xiàn)在如果用戶通過 URL「http://..../Employee/1」 或者 「http://..../Employee/111」來發(fā)出請求,行為方法將會得到執(zhí)行,但是如果用戶通過 URL「http://..../Employee/Sukesh」 ,他將會得到「Resource Not Found」的錯誤。
行為方法中的參數(shù)名稱和路由參數(shù)名稱需要保持一致嗎?
從根本上說,路由模式也許包含多個 RouteParameters。為了單獨地識別每一個路由參數(shù),需要保持行為方法中的參數(shù)名稱和路由參數(shù)名稱一致。
定義自定義路由的次序重要嗎?
答案是肯定的,次序是重要的。UrlRoutingModule 將會匹配第一個路由對象。
在上述的實驗中,我們已經(jīng)定義了兩個路由。一個是自定義路由,一個是默認(rèn)路由。現(xiàn)在我們來討論一種情況,默認(rèn)路由被首先定義,自定義路由被第二個定義。
在 這種情況下,終端用戶發(fā)起一個請求 URL,即「http://…/Employee/BulkUpload」。在匹配階段,UrlRoutingModules 將會發(fā)現(xiàn)請求的 URL 與默認(rèn)的路由模式匹配,它將會認(rèn)為「Employee」是控制器的名稱,「BulkUpload」是行為方法的名稱。
因此次序在定義路由時是非常重要的。大多數(shù)通用的路由應(yīng)該被放置到最后。
是否存在更簡單的方式來定義行為方法的 URL 模式?
我們可以運用基于路由的屬性來解決這個問題。讓我們來試一下。
第一步:使基于路由的屬性可用
在 RegisterRoutes 方法中的 IgnoreRoute 語句后添加如下代碼。
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();
routes.MapRoute(
...
第二步:為行為方法定義路由模式
在 EmployeeController 中的 Index 行為方法中附上 Route 屬性。
[Route("Employee/List")]
publicActionResultIndex()
{
第三步:執(zhí)行并測試
執(zhí)行應(yīng)用程序,然后完成登錄操作。
正如你所看見的,我們擁有相同的輸出結(jié)果,但是不同的是擁有了更加用戶友好性的 URL。
我們可以通過基于路由的屬性來定義路由參數(shù)嗎?
答案是肯定的,可以查看如下語法。
[Route("Employee/List/{id}")]
publicActionResult Index(string id){...}
在這種情況下的限制呢?
這將會變得更加容易。
[Route("Employee/List/{id:int}")]
我們可以擁有如下限制。
{x:alpha} – 字符串認(rèn)證
{x:bool} – 布爾認(rèn)證
{x:datetime} – Date Time 認(rèn)證
{x:decimal} – Decimal 認(rèn)證
{x:double} – 64 位 Float 認(rèn)證
{x:float} – 32 位 Float 認(rèn)證
{x:guid} – GUID 認(rèn)證
{x:length(6)} – 長度認(rèn)證
{x:length(1,20)} – 最小和最大長度認(rèn)證
{x:long} – 64 位 Int 認(rèn)證
{x:max(10)} – 最大 Integer 長度認(rèn)證
{x:maxlength(10)} – 最大長度認(rèn)證
{x:min(10)} – 最小 Integer 長度認(rèn)證
{x:minlength(10)} – 最小長度認(rèn)證
{x:range(10,50)} – 整型 Range 認(rèn)證
{x:regex(SomeRegularExpression)} – 正則表達式認(rèn)證
在 RegisterRoutes 方法中 IgnoreRoutes 是用于做什么的?
當(dāng)我們不想運用路由做指定擴展時,我們可以運用 IgnoreRoutes。作為 MVC 模板的一部分,如下的代碼已經(jīng)寫入 RegisterRoutes 方法中。
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");這意味著,當(dāng)終端用戶發(fā)出一個帶有「.axd」擴展的請求時,將不會執(zhí)行任何路由操作。請求將會直接定位到物理資源。我們也可以定義自己的 IgnoreRoute 語句。
9. 總結(jié)
在第 6 天的學(xué)習(xí)中,我們完成了簡單的 MVC 項目。希望你能夠享受完成系列學(xué)習(xí)的樂趣。
稍等一下!第 7 天的學(xué)習(xí)呢?
在第 7 天中,我們將會運用 MVC, JQuery 和 Ajax 來創(chuàng)建一個 Single Page 應(yīng)用。這將會更加有趣,并富有挑戰(zhàn)。
保持學(xué)習(xí)的熱情吧!
原文地址:Learn MVC Project in 7 days
OneAPM for .NET 能夠深入到所有 .NET 應(yīng)用內(nèi)部完成應(yīng)用性能管理和監(jiān)控,包括代碼級別性能問題的可見性、性能瓶頸的快速識別與追溯、真實用戶體驗監(jiān)控、服務(wù)器監(jiān)控和端到端的應(yīng)用性能管理。想技術(shù)文章,請訪問 OneAPM 官方博客。
轉(zhuǎn)載于:https://blog.51cto.com/oneapm/1687328
總結(jié)
以上是生活随笔為你收集整理的7 天玩转 ASP.NET MVC — 第 6 天的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: My SQL InnoDB 1217
- 下一篇: 设计模式(2)策略模式 (模式讲解+应用