目錄
0. 前言
歡迎來到第六天的 MVC 系列學習中。希望你在閱讀此篇文章的時候,已經學習了前五天的內容,這也是第六天學習的前提條件夷磕。
1. Lab 27 — 添加批量上傳選項
在這個實驗中,我們將會創(chuàng)建一個選項,用于從 CSV 文件中上傳多個 Employees耗溜。
我們將會做兩件事。
學會如何運用文件上傳控件省容。
異步控制器抖拴。
第一步:創(chuàng)建 FileUploadViewModel
在 ViewModels 文件夾下創(chuàng)建一個類,命名為 FileUploadViewModel腥椒。
public class FileUploadViewModel: BaseViewModel
{
public HttpPostedFileBase fileUpload {get; set ;}
}
HttpPostedFileBase 將會通過客戶端提供上傳文件的訪問入口阿宅。
第二步:創(chuàng)建 BulkUploadController 和 Index 行為方法
創(chuàng)建一個新的控制器,命名為 BulkUploadController笼蛛,以及一個行為方法洒放,命名為 Index。
public class BulkUploadController : Controller
{
[HeaderFooterFilter]
[AdminFilter]
public ActionResult Index()
{
return View(new FileUploadViewModel());
}
}
正如你所看見的滨砍,Index 行為方法附上了 HeaderFooterFilter 和 AdminFilter 屬性往湿。HeaderFooterFilter 確保了正確了頁眉和頁腳數據傳輸到 ViewModel,AdminFilter 限制了 Non-Admin 用戶訪問行為方法惋戏。
第三步:創(chuàng)建上傳視圖
為上述行為方法創(chuàng)建一個視圖领追。
需要注意的是,視圖的名稱應該為 Index.cshtml日川,并且應該放置在「~/Views/BulkUpload」文件夾下蔓腐。
第四步:設計上傳視圖
在視圖中放置如下內容。
@using WebApplication1.ViewModels
@model FileUploadViewModel
@{
Layout = "~/Views/Shared/MyLayout.cshtml";
}
@section TitleSection{
Bulk Upload
}
@section ContentBody{
<div>
<a href="/Employee/Index">Back</a>
<form action="/BulkUpload/Upload" method="post" enctype="multipart/form-data">
Select File : <input type="file" name="fileUpload" value="" />
<input type="submit" name="name" value="Upload" />
</form>
</div>
}
正如你所看見的龄句,在 FileUploadViewModel 中回论,屬性的名稱和 input[type="file"] 的名稱是一樣的,都是「FileUpload」分歇。我們在 Model Binder 實驗中已經講述了名稱屬性的重要性傀蓉。
注意:在 Form 標簽中,有一個額外的指定加密屬性职抡,我們將會在實驗結尾處討論它葬燎。
第五步:創(chuàng)建業(yè)務層上傳方法
在 EmployeeBusinessLayer 中創(chuàng)建一個新的方法,命名為 UploadEmployees。
public void UploadEmployees(List<Employee> employees)
{
SalesERPDAL salesDal = new SalesERPDAL();
salesDal.Employees.AddRange(employees);
salesDal.SaveChanges();
}
第六步:創(chuàng)建上傳行為方法
在 BulkUploadController 中創(chuàng)建一個新的行為方法谱净,命名為 Upload窑邦。
[AdminFilter]
public ActionResult Upload(FileUploadViewModel model)
{
List<Employee> employees = GetEmployees(model);
EmployeeBusinessLayer bal = new EmployeeBusinessLayer();
bal.UploadEmployees(employees);
return RedirectToAction("Index","Employee");
}
private List<Employee> GetEmployees(FileUploadViewModel model)
{
List<Employee> employees = new List<Employee>();
StreamReader csvreader = new StreamReader(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 = new Employee();
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 附上鏈接冈钦。
<a href="/Employee/AddNew">Add New</a>
<a href="/BulkUpload/Index">BulkUpload</a>
第八步:執(zhí)行并測試
為測試創(chuàng)建一個簡單的文件
創(chuàng)建一個簡單的文件如下,然后將其保存在電腦中李请。
執(zhí)行并測試
按下 F5瞧筛,然后執(zhí)行應用。完成登錄操作导盅,然后通過點擊鏈接導航到 BulkUpload 選項较幌。
選擇一個文件,然后點擊上傳白翻。
注意:在上述的例子中乍炉,我們沒有在視圖中用到任何客戶端或者服務器端的認證。它也許會導致如下的錯誤嘁字。
「Validation failed for one or more entities. See 'EntityValidationErrors' property for more details.」
為了發(fā)現這個錯誤的確切原因恩急,只需要在異常發(fā)生的時候添加如下的表達式。
((System.Data.Entity.Validation.DbEntityValidationException)$exception).EntityValidationErrors纪蜒。
表達式「$exception」呈現了任何從當前上下文中拋出的錯誤衷恭,即使它沒有被捕獲或者支配到一個變量中。
Lab 27 的 Q&A
為什么我們沒有在這里用到認證纯续?
為選項增加客戶端和服務器端的認證將會留給讀者完成随珠,我在這里給出一些暗示。
運用 Data Annotations 來進行服務器端的認證猬错。
你可以運用 Data Annotations 或者實現 JQuery Unobtrusive Validation 來實現客戶端認證窗看。明顯的是,這一次你需要手動設置自定義數據屬性倦炒,因為我們沒有為文件輸入創(chuàng)建 HtmlHelper 方法显沈。
對于客戶端的認證,你可以寫一些自定義的 JavaScript逢唤,然后通過點擊安全觸發(fā)它拉讯。這并不是很難,因為文件輸入是一個輸入控件鳖藕,值可以通過在 JavaScript 中獲取并認證魔慷。
什么是HttpPostedFileBase?
HttpPostedFileBase 可以通過客戶端提供文件上傳的訪問接口著恩。Model Binder 將會在發(fā)送 Post 請求時更新所有 FileUploadViewModel 類的屬性值≡憾現在 FileUploadViewModel 里只有一個屬性值蜻展,Model Binder 將會通過客戶端來設置這個屬性值,實現文件上傳邀摆。
提供多個文件輸入控件是否可行纵顾?
答案是肯定的。我們可以通過兩種方式實現它隧熙。
創(chuàng)建多個文件輸入控件片挂。每一個控件都需要有唯一的名字幻林。在 FileUploadViewModel 類中為每個控件創(chuàng)建一個 HttpPostedFileBase 的類型屬性贞盯。每一個屬性的名稱應該與控件的名稱相匹配。剩下的工作會由 ModelBinder 來處理沪饺。
創(chuàng)建多個文件輸入控件躏敢。每一個控件都需要有唯一的名字。這次不是創(chuàng)建多個 HttpPostedFileBase 的屬性整葡,而是創(chuàng)建一個類型 List件余。
注意:上述的情形對于所有控件都可行。當你擁有多個相同名稱的控件時遭居,如果要更新的屬性值是一個簡單參數啼器,Model Binder 將會更新第一個控件的屬性值。如果更新的屬性值是一個 List俱萍,Model Binder 會將每一個屬性值設置到控件中端壳。
enctype="multipart/form-data"是用于做什么的?
這個對知道與否并不重要枪蘑,但是知道確實會好一點损谦。
這個屬性指定了編碼類型,在傳輸數據時使用岳颇。屬性的默認值是「application/x-www-form-urlencoded」照捡。
例如,我們的登錄表單將會隨著 Post 請求向服務器發(fā)送如下數據话侧。
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
當 enctype="multipart/form-data"屬性被添加到表單標簽時栗精,隨著 Post 請求會發(fā)送到服務器上。
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 被一條邊界線所分隔,并且每一個部分都包含一個值乙漓。
如果表單標簽中包含文件輸入控件時级历,編碼類型需要設定為「multipart/form-data」。
注意:每一次請求發(fā)生時叭披,邊界線會隨機生成寥殖。你可能會看到不同的邊界線玩讳。
為什么我們不總是將 EncTyp 設置為「multipart/form-data」?
當 EncTyp 被設置為「multipart/form-data」嚼贡,它將會做兩件事熏纯,Post 數據以及上傳文件。這就是為什么我們不總是將其設置為「multipart/form-data」粤策。
答案就是樟澜,這樣會增加請求的總體大小。請求的大小越大叮盘,意味著性能越差秩贰。因為最佳實踐應該是將其設置為默認的值,即「application/x-www-form-urlencoded」柔吼。
為什么我們需要創(chuàng)建 ViewModel毒费?
在我們的視圖中有一個控件。我們可以通過直接向 HttpPostedFileBase 類型增加一個參數來實現同樣的結果愈魏,這里我們需要在上傳方法中命名為 「fileUpload」觅玻,而不是創(chuàng)建一個單獨的 ViewModel。代碼如下所示培漏。
public ActionResult Upload(HttpPostedFileBase fileUpload)
{
}
創(chuàng)建 ViewModel 是最佳實踐溪厘。Controller 應該總是向視圖發(fā)送以 ViewModel 為格式的數據,并且來自視圖的數據應該以 ViewModel 發(fā)送給 Controller牌柄。
2. 上述解決方案的問題
你是否想知道畸悬,當你發(fā)送一個請求時,如何獲得響應的友鼻?
現在不要去說傻昙,是通過行為方法接到請求然后怎樣怎樣的。盡管這是正確的答案彩扔,我仍然期望一些不同的答案妆档。我的問題是在最開始的時候發(fā)生了什么。
一個簡單的編程規(guī)則虫碉,程序中所有都通過線程執(zhí)行贾惦,盡管是請求。
在 Web 服務器上的 ASP.NET敦捧,.NET Framework 維護著線程池须板。每一次請求發(fā)送到 Web 服務器上時,就會把一個線程池中一個空閑的線程分配給服務器兢卵,用于處理請求习瑰。這個線程被稱為 Worker 線程。
Worker 線程在請求正常處理的過程中處于阻塞狀態(tài)秽荤,并且不能處理其它請求甜奄。
現在來假設一種場景柠横,一個應用接收到了很多請求,并且每個請求都會花費許多時間來處理進程课兄。在這種情形下牍氛,沒有 Worker 線程可用于服務器請求,所以當新的請求想要獲取該線程進行處理狀態(tài)時烟阐,我們可能需要在這時候終止它搬俊。這個我們稱之為 Thread Starvation(線程饑餓)。
在我們的例子樣本文件中蜒茄,只存在了兩個雇員記錄唉擂,而在真實場景中,可能存在成千上萬的記錄扩淀,這意味著請求也許會花費大量時間來完成進程楔敌。這樣會導致線程饑餓。
解決方案
迄今為止我們所討論的請求都是同步請求類型驻谆。
如果客戶端發(fā)出的是異步請求,而不是同步請求庆聘,那么線程饑餓的問題就解決了胜臊。
在異步請求的情形下,請求將會從線程池分配中獲得通常的 Worker 線程伙判,用于服務請求象对。
Worker 線程將會初始化異步操作,然后返回線程池來服務其它請求宴抚。異步操作將會繼續(xù)被 CLR 線程處理勒魔。
現在的問題是,CLR 線程不能返回響應菇曲,所以一旦當完成異步操作后冠绢,它就會通知 ASP.NET。
Web 服務器將會再一次從線程池中得到 Worker 線程常潮,用于處理剩余的請求和響應弟胀。
在上述的完整的場景中,兩個 Worker 線程從線程池中獲取喊式。這兩個 Worker 線程也許是同一個孵户,也許不是。
在我們的例子中岔留,文件讀取是通過 I/O 操作的夏哭,這個操作不需要 Worker 線程來處理。所以最好是將同步請求轉換為異步請求献联。
異步請求會提升響應時間嗎竖配?
答案是否定的厕吉。響應時間是相同的。這里線程將會被釋放械念,用于服務其它請求头朱。
3. Lab 28 — 解決線程饑餓問題
在 ASP.NET MVC 中,我們可以通過轉換同步行為方法到異步行為方法龄减,來將同步請求轉換為異步請求项钮。
第一步:創(chuàng)建異步控制器
將 UploadController 的基類改為AsynController。
public class BulkUploadController : AsyncController
{
第二步:轉換同步行為方法到異步行為方法
通過關鍵字希停,「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 = new EmployeeBusinessLayer();
bal.UploadEmployees(employees);
return RedirectToAction("Index", "Employee");
}
正如你所看見的宠能,我們在行為方法的開始和結束的地方將線程 ID 存儲在變量中亚隙。
現在讓我理解下代碼。
當客戶端點擊上傳按鈕時违崇,一個新的請求將被發(fā)送到服務器阿弃。
Webserver 從線程池中獲取一個 Worker 線程,然后將其分配給請求用于服務羞延。
Worker 線程使得行為方法用于執(zhí)行渣淳。
Worker 方法通過 Task.Factory.StartNew 方法執(zhí)行異步操作。
正如你所看見的伴箩,行為方法通過關鍵字 Async被標記為異步的入愧,這將會確保一旦異步方法操作開始執(zhí)行,Worker 線程就會得到釋放嗤谚。這個時候邏輯的異步操作將會通過獨立的 CLR 線程繼續(xù)在后臺執(zhí)行棺蛛。
現在異步操作調用將被標記為 Await 關鍵字。這將會確保接下來的代碼行不會被執(zhí)行巩步,除非異步操作完成旁赊。
一旦異步操作完成了,接下來的行為方法中的代碼就需要被執(zhí)行渗钉。因此又要需要一個 Worker 線程彤恶。因此 Webserver 將會從線程池中取出一個空閑線程,然后將其分配給剩余的請求用于服務鳄橘,并返回響應声离。
第三步:執(zhí)行并測試
執(zhí)行應用。導航到 BulkUpload 選項瘫怜。
在你做任何操作之前术徊,先導航到代碼,然后在最后一行代碼中打個斷點鲸湃。
現在選擇一個簡單的文件赠涮,然后點擊 Upload子寓。
正如你所看見的,在方法的開始和結束時笋除,線程 ID 是不同的斜友。輸出的結果和之前的實驗結果一樣。
4. Lab 29 — 異常處理 — 呈現自定義錯誤頁面
如果一個項目沒有正確的異常處理垃它,就不能算是一個完整的項目鲜屏。
迄今為止,我們討論過 ASP.NET MVC 中的兩個過濾器国拇,即 Action 過濾器和 Authentication 過濾器÷迨罚現在是時候討論第三個過濾器了,即 Exception 過濾器酱吝。
什么是 Exception 過濾器也殖?
Exception 過濾器的使用方式同其它過濾器一樣。我們將以屬性的方式運用务热。
運用 Exception 過濾器的步驟忆嗜。
使它們可用
將它們作為行為方法或者控制器的屬性。我們也可以將它們應用到 Global 級別陕习。
它們是用來做什么的霎褐?
一旦在行為方法內部發(fā)生異常時,Exception 過濾器就將會控制執(zhí)行并開始自動執(zhí)行其內部的代碼该镣。
是否存在自動的 Exception 過濾器?
ASP.NET MVC 提供給我們一個已經編寫好的 Exception 過濾器响谓,稱作 HandleError损合。
正如我們之前所說的,當行為方法中娘纷,一旦異常發(fā)生嫁审,過濾器就將被執(zhí)行。這個過濾器將會在「~/Views/[current controller]」或者「~/Views/Shared」文件夾內發(fā)現一個名稱為「Error」的視圖赖晶,為這個視圖創(chuàng)建一個 ViewResult律适,然后返回響應。
讓我們看一個 Demo遏插,用于更好地理解捂贿。在項目的實驗最后,我們將會實現 BulkUpload 選項「斐埃現在存在著較高的輸入文件的錯誤可能性厂僧。
第一步:創(chuàng)建一個簡單的帶有錯誤的 Upload 文件
創(chuàng)建一個簡單的上傳文件,就像之前一樣了牛。但是這次颜屠,文件中包含一些非法值辰妙。
正如你所看見的,Salary 是非法的甫窟。
第二步:執(zhí)行并測試應用
按下 F5密浑,執(zhí)行應用。導航到 Bulk Upload 選項粗井,選擇上述的文件尔破,然后點擊 Upload。
第三步:使異常過濾器可用
自定義異常開啟后背传,異常過濾器也被開啟呆瞻。為了開啟自定義異常,打開 Web.config 文件径玖,然后導航到 System.Web 區(qū)域痴脾,在該區(qū)域下增加自定義錯誤,如下所示梳星。
<system.web>
<customErrors mode="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 過濾器已經被附上 Global 級別届榄。
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());//ExceptionFilter
filters.Add(new AuthorizeAttribute());
}
如果需要移除 Global 過濾器,將會被附上方法或者控制器級別倔喂。
[AdminFilter]
[HandleError]
public async Task<ActionResult> Upload(FileUploadViewModel model)
{
但是不建議這么做铝条,最好還是應用 Global 級別。
第六步:執(zhí)行并測試
像之前的方式一樣席噩,讓我們來看一下應用的測試結果班缰。
第七步:在視圖中展示錯誤信息
為了達到這個目的,我們需要將錯誤視圖轉換為 HandleErrorInfo 類的強類型視圖班挖,然后在視圖中展示錯誤信息鲁捏。
@model HandleErrorInfo
@{
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>
Error Message :@Model.Exception.Message<br />
Controller: @Model.ControllerName<br />
Action: @Model.ActionName
</body>
</html>
第八步:執(zhí)行并測試
這次測試結果,我們將會得到如下的錯誤視圖。
我們是否錯失了什么给梅?
Handle Error 屬性確保了無論何時行為方法發(fā)生異常時假丧,自定義視圖都會被呈現。但是僅限于控制器和行為方法动羽。它不會處理「Resource not found」錯誤包帚。
執(zhí)行應用,輸入一些古怪的 URL运吓。
第九步:創(chuàng)建 ErrorController
在 Controller 文件夾下創(chuàng)建一個名為 ErrorController 的控制器渴邦,然后創(chuàng)建一個行為方法,命名為 Index拘哨。
public class ErrorController : Controller
{
// GET: Error
public ActionResult Index()
{
Exception e=new Exception("Invalid Controller or/and Action Name");
HandleErrorInfo eInfo = new HandleErrorInfo(e, "Unknown", "Unknown");
return View("Error", eInfo);
}
}
HandleErrorInfo 控制器擁有三個參數谋梭,即異常對象,控制器名稱和行為方法名稱倦青。
第十步:在非法的 URL 中呈現自定義錯誤視圖
在 Web.config 中設定「Resource not found error」定義瓮床。
<system.web>
<customErrors mode="On">
<error statusCode="404" redirect="~/Error/Index"/>
</customErrors>
第十一步:使所有人可訪問 ErrorController
在 ErrorController 中應用 AllowAnonymous 屬性,Index 方法不應該被綁定到一個有權限的用戶产镐。因為用戶可能在登錄前就輸入了非法的 URL隘庄。
[AllowAnonymous]
public class ErrorController : Controller
{
第十二步:執(zhí)行并測試
執(zhí)行應用程序,然后在瀏覽器地址欄輸入一些非法的 URL癣亚。
Lab 29 的 Q&A
可以改變視圖的名稱嗎丑掺?
答案是肯定的,保持視圖名稱為「Error」不是總是必須的述雾。
在這種情形下街州,當附上 HandleError 過濾器時,我們需要指定視圖的名稱玻孟。
[HandleError(View="MyError")]
或者是
filters.Add(new HandleErrorAttribute()
{
View="MyError"
});
對于不同的異常菇肃,獲取不同的錯誤視圖,是否可行取募?
答案是肯定的,這是可行的蟆技。在這種情形下玩敏,我們需要應用 Handle Error 過濾器多次。
[HandleError(View="DivideError",ExceptionType=typeof(DivideByZeroException))]
[HandleError(View = "NotFiniteError", ExceptionType = typeof(NotFiniteNumberException))]
[HandleError]
或者是
filters.Add(new HandleErrorAttribute()
{
ExceptionType = typeof(DivideByZeroException),
View = "DivideError"
});
filters.Add(new HandleErrorAttribute()
{
ExceptionType = typeof(NotFiniteNumberException),
View = "NotFiniteError"
});
filters.Add(new HandleErrorAttribute());
在上述的例子中质礼,我們增加了三個 Handle Error 過濾器旺聚。前兩個為指定的異常,而后一個更加通用一些眶蕉,它將會為所有其它異常展示錯誤視圖砰粹。
5. 理解上述實驗的局限
上述實驗存在唯一的局限,便是我們沒有將異常日志輸出造挽。
6. Lab 30 — 異常處理 — 異常日志
第一步:創(chuàng)建 Logger 類
在項目的根目錄下創(chuàng)建一個新的文件夾碱璃,稱為 Logger弄痹。
在 Logger 文件夾下創(chuàng)建一個類,命名為 FileLogger嵌器。
namespace WebApplication1.Logger
{
public class FileLogger
{
public void LogException(Exception e)
{
File.WriteAllLines("C://Error//" + DateTime.Now.ToString("dd-MM-yyyy mm hh ss")+".txt",
new string[]
{
"Message:"+e.Message,
"Stacktrace:"+e.StackTrace
});
}
}
}
第二步:創(chuàng)建 EmployeeExceptionFilter 類
在 Filters 文件夾下創(chuàng)建一個新的類肛真,命名為 EmployeeExceptionFilter。
namespace WebApplication1.Filters
{
public class EmployeeExceptionFilter
{
}
}
第三步:擴展 Handle Error 用于實現日志記錄
讓 EmployeeExceptionFilter 類繼承 HandleErrorAttribute 類爽航,然后重寫 OnException 方法蚓让。
public class EmployeeExceptionFilter:HandleErrorAttribute
{
public override void OnException(ExceptionContext filterContext)
{
base.OnException(filterContext);
}
}
注意:確保在 HandleErrorAttribute 類中的頂部引用了 System.Web.MVC。
第四步:定義 OnException 方法
在 OnException 方法中包含異常日志記錄代碼讥珍,如下所示历极。
public override void OnException(ExceptionContext filterContext)
{
FileLogger logger = new FileLogger();
logger.LogException(filterContext.Exception);
base.OnException(filterContext);
}
第五步:改變默認的異常過濾器
打開 FilterConfig.cs 文件,移除 HandleErrorAttribute衷佃,然后附上我們上一步驟中所創(chuàng)建的趟卸。
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
//filters.Add(new HandleErrorAttribute());//ExceptionFilter
filters.Add(new EmployeeExceptionFilter());
filters.Add(new AuthorizeAttribute());
}
第六步:執(zhí)行并測試
首先在 C 盤下創(chuàng)建一個文件夾,命名為「Error」纲酗。這個文件夾會存放錯誤的日志文件衰腌。
注意:可以更改路徑為你所期望的路徑。
按下 F5觅赊,然后執(zhí)行應用右蕊。導航到 Bulk Upload 選項。選擇文件吮螺,然后點擊 Upload饶囚。
這次的輸出將會有所不同,我們將會得到一些錯誤視圖鸠补,就像之前一樣萝风。唯一的不同便是我們會在「C:\Errors」文件夾發(fā)現一些錯誤日志文件。
Lab 30 的 Q&A
異常發(fā)生時紫岩,錯誤視圖是如何作為響應返回的规惰?
在上述實驗中,我們重寫了 OnException 方法泉蝌,然后實現了異常日志的功能⌒颍現在的問題是,默認的錯誤處理過濾器是如何繼續(xù)工作的勋陪?答案是簡單地贪磺,查看 OnException 方法的最后一行代碼。
base.OnException(filterContext);
這意味著诅愚,基類 OnException 將會做剩余的工作寒锚,基類 OnException 將會返回錯誤視圖的 ViewResult。
在 OnException 中,我們可以返回其它結果嗎刹前?
答案是肯定的泳赋,查看如下代碼。
public override void OnException(ExceptionContext filterContext)
{
FileLogger logger = new FileLogger();
logger.LogException(filterContext.Exception);
//base.OnException(filterContext);
filterContext.ExceptionHandled = true;
filterContext.Result = new ContentResult()
{
Content="Sorry for the Error"
};
}
當我們想要返回自定義響應時腮郊,首先要做的事便是摹蘑,通知 MVC 引擎,告知其我們已經手動處理異常了轧飞,所以不需要做默認的行為衅鹿,即不需要呈現默認的錯誤屏幕。這一切可以通過如下代碼來實現过咬。
filterContext.ExceptionHandled = true
7. 路由
迄今為止我們討論過許多概念大渤,我們也回答了許多有關 MVC 的問題,但是除了一個基本和重要的概念掸绞。
「當用戶發(fā)出請求時泵三,確切發(fā)生了什么」?
一個很好的答案便是「行為方法的執(zhí)行」衔掸。但是確切的答案是控制器和犯法是如何被一個特定的 URL 請求識別的烫幕?
當我們開始「實現用戶友好的 URLs」的實驗時,我們首先需要回答上述的問題敞映。你也許會奇怪為什么這個主題會放置到最后较曼。我故意將其放置到最后,是因為我想讓更多的人在理解內部之前振愿,先了解 MVC捷犹。
理解 RouteTable
在 ASP.NET MVC 中,存在一個概念冕末,稱作 RouteTable萍歉。這里存儲了應用的 URL 路由。用簡單的話說档桃,它承載了一個應用的 URL 模式的集合枪孩。
默認情況下,一個路由將會作為項目模板的一部分被添加藻肄∠眨可以通過 Global.asax 文件查看它。在 Application_Start 中仅炊,你將會發(fā)現如下的代碼。
RouteConfig.RegisterRoutes(RouteTable.Routes);
你將會在 App_Start 文件夾下發(fā)現 RouteConfig.cs 文件澎蛛,它包含了如下代碼抚垄。
namespace WebApplication1
{
public class RouteConfig
{
public static void RegisterRoutes(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 方法已經通過 Route.MapRoutes 方法定義了一個默認的路由。
在 RegisterRoutes 方法中定義的路由將會在 ASP.NET MVC 請求周期中被用到呆馁,用于決定執(zhí)行確切的控制器和方法桐经。
如果需要,我們可以通過使用 Route.MapRoutes 函數浙滤,創(chuàng)建多個路由阴挣。內部定義路由意味著創(chuàng)建 Route 對象。
MapRoute 函數也可以把路由對象附上 RouteHandler纺腊,這樣將會是 MVCRouteHandler畔咧。
理解 ASP.NET MVC 請求周期
在我們開始之前,你需要清楚揖膜,我們將要 100% 地解釋請求周期誓沸。我們將要接觸到之前未講到的重要概念。
第一步:UrlRoutingModule
當終端用戶發(fā)出請求后壹粟,首先會通過 UrlRoutingModule 對象拜隧。UrlRoutingModule 是一個 HTTP 模塊。
第二步:路由
UrlRoutingModule 首先會從路由集合中匹配 Route 對象趁仙。對于匹配洪添,請求的 URL 將會與路由中定義的 URL 模式相對比。
下述的規(guī)則將會在匹配中被考慮到雀费。
- 請求 URL 中參數的數字以及在路由中定義的 URL 模式干奢。例如:
- URL 模式中定義的可選參數。例如:
- 在參數中定義的靜態(tài)參數坐儿。
第三步:創(chuàng)建 MVC Route Handler
一旦路由對象被選中律胀,UrlRoutingModule 將會從路由對象中獲得 MvcRouteHandler。
第四步:創(chuàng)建 RouteData 和 RequestContext
UrlRoutingModule 對象將會通過 Route 對象創(chuàng)建 RouteData貌矿,它將會用于創(chuàng)建 RequestContext炭菌。
RouteData 封裝了關于路由的信息,如控制器的名稱逛漫,行為方法的名稱黑低,路由參數的值。
Controller 名稱
為了從請求 URL 中獲得控制器的名稱酌毡,需要遵循如下的簡單規(guī)則克握。即“在 URL 模式中{Controller} 是識別控制器名稱的關鍵詞”。
例如:
當URL 模式是 {Controller}/{Action}/{Id}枷踏,而請求 URL 是「http://localhost:8870/BulkUpload/Upload/5」時菩暗,BulkUpload 是控制器的名稱。
當 URL 模式是 {Action}/{Controller}/{Id}旭蠕,而請求 URL 是 「http://localhost:8870/BulkUpload/Upload/5」時停团,Upload 是控制器的名稱旷坦。
行為方法名稱
為了獲得請求 URL 中的行為方法,需要遵循如下的簡單規(guī)則佑稠。即「在 URL 模式中 {Action} 是行為方法名稱的關鍵詞」秒梅。
例如:
當URL 模式是 {Controller}/{Action}/{Id},而請求 URL 是「http://localhost:8870/BulkUpload/Upload/5」時舌胶,Upload 是行為方法的名稱捆蜀。
當 URL 模式是 {Action}/{Controller}/{Id},而請求 URL 是 「http://localhost:8870/BulkUpload/Upload/5」時幔嫂,BulkUpload 是行為方法的名稱辆它。
路由參數
一個基本的 URL 模式包含如下四個要素。
{Controller}婉烟,用于識別控制器名稱娩井。
{Action},識別行為方法名稱似袁。
一些字符串洞辣,例如「MyCompany/{Controller}/{Action}」,在這個模式中昙衅,「MyCompany」是一個必須的字符串扬霜。
{Something},例如「{Controller}/{Action}/{Id}」而涉,在這個模式中「Id」是路由參數著瓶。在請求的 URL 中,路由參數可以被用于獲取 URL 的值啼县。
我們來看一下如下示例材原。
路由模式是 {Controller}/{Action}/{Id}。
請求 URL 是「http://localhost:8870/BulkUpload/Upload/5」季眷。
測試一:
public class BulkUploadController : Controller
{
public ActionResult Upload (string id)
{
//value of id will be 5 -> string 5
...
}
}
測試二:
public class BulkUploadController : Controller
{
public ActionResult Upload (int id)
{
//value of id will be 5 -> int 5
...
}
}
測試三:
public class BulkUploadController : Controller
{
public ActionResult Upload (string MyId)
{
//value of MyId will be null
...
}
}
第五步:創(chuàng)建 MVCHandler
MvcRouteHandler 將會創(chuàng)建 MVCHandler 的實例余蟹,傳輸 RequestContext 對象。
第六步:創(chuàng)建控制器實例
MVCHandler 將會通過 ControllerFactory(默認的是 DefaultControllerFactory) 創(chuàng)建控制器實例子刮。
第七步:執(zhí)行方法
MVCHandler 將會觸發(fā)控制器的執(zhí)行方法威酒。執(zhí)行方法在控制器基類中被定義。
第八步:觸發(fā)行為方法
每一個控制器都與一個 ControllerActionInvoker 對象相關聯挺峡。在執(zhí)行方法中葵孤,ControllerActionInvoker 觸發(fā)正確的行為方法。
第九步:執(zhí)行結果
行為方法接收到用戶的輸入橱赠,然后準備合適的響應數據尤仍,并通過返回一個類型來執(zhí)行結果。現在返回的結果可能是 ViewResult狭姨,可能是 RedirectToRoute 結果或者可能是其它吓著。
現在鲤嫡,我相信你已經對路由的概念有了很好的理解,所以讓我們通過路由來使得項目的 URLs 更友好吧绑莺。
8. Lab 31 — 實現用戶友好性的 URLs
第一步:重新定義 RegisterRoutes 方法
在 RegisterRoutes 方法中包含額外的路由。
public static void RegisterRoutes(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 }
);
}
正如你所看見的惕耕,我們現在已經不止定義一個路由了纺裁。
第二步:更改 URL 引用
從「~/Views/Employee」文件夾下打開 AddNewLink.cshtml 文件,然后更改 BulkUpload 鏈接如下司澎。
<a href="/Employee/BulkUpload">BulkUpload</a>
第三步:執(zhí)行并測試
執(zhí)行應用欺缘,將會看到神奇的地方。
正如你所看見的挤安,URL 不再是“Controller/Action”的形式谚殊。它看起來更加用戶友好,但是輸出是一樣的蛤铜。
我建議你定義更多的路由嫩絮,嘗試更多的 URLs。
Lab 31 的 Q&A
之前的 URL 還是否起作用围肥?
答案是肯定的剿干,之前的 URL 也會起作用。
現在 BulkUploadController 中的 Index 方法可以通過兩個 URLs 訪問穆刻。
默認路由中的「Id」是什么置尔?
我們之前提到過它。它被稱作路由參數氢伟。它可以通過 URL 來用于獲取值榜轿。它是一個可被替換的查詢字符串。
路由參數和查詢字符串的區(qū)別是什么朵锣?
查詢字符串有大小限制谬盐,然而我們可以定義路由參數的任意數字。
我們不能向查詢字符串值添加限制猪勇,但是我們可以向路由參數添加限制设褐。
可以設定路由參數的默認值,然而查詢字符串的默認值不可設定泣刹。
查詢字符串使得 URL 凌亂助析,但是路由參數保持 URL 整潔。
如何向路由參數應用限制椅您?
可以通過正則表達式來完成這件事外冀。例如,查看如下路由掀泳。
routes.MapRoute(
"MyRoute",
"Employee/{EmpId}",
new {controller=" Employee ", action="GetEmployeeById"},
new { EmpId = @"\d+" }
);
行為方法將如下所示雪隧。
public ActionResult GetEmployeeById(int EmpId)
{
...
}
現在如果用戶通過 URL「http://..../Employee/1」 或者 「http://..../Employee/111」來發(fā)出請求西轩,行為方法將會得到執(zhí)行,但是如果用戶通過 URL「http://..../Employee/Sukesh」 脑沿,他將會得到「Resource Not Found」的錯誤藕畔。
行為方法中的參數名稱和路由參數名稱需要保持一致嗎?
從根本上說庄拇,路由模式也許包含多個 RouteParameters注服。為了單獨地識別每一個路由參數,需要保持行為方法中的參數名稱和路由參數名稱一致措近。
定義自定義路由的次序重要嗎溶弟?
答案是肯定的,次序是重要的瞭郑。UrlRoutingModule 將會匹配第一個路由對象辜御。
在上述的實驗中,我們已經定義了兩個路由屈张。一個是自定義路由擒权,一個是默認路由。現在我們來討論一種情況袜茧,默認路由被首先定義菜拓,自定義路由被第二個定義。
在這種情況下笛厦,終端用戶發(fā)起一個請求 URL纳鼎,即「http://…/Employee/BulkUpload」。在匹配階段裳凸,UrlRoutingModules 將會發(fā)現請求的 URL 與默認的路由模式匹配贱鄙,它將會認為「Employee」是控制器的名稱,「BulkUpload」是行為方法的名稱姨谷。
因此次序在定義路由時是非常重要的逗宁。大多數通用的路由應該被放置到最后。
是否存在更簡單的方式來定義行為方法的 URL 模式梦湘?
我們可以運用基于路由的屬性來解決這個問題瞎颗。讓我們來試一下。
第一步:使基于路由的屬性可用
在 RegisterRoutes 方法中的 IgnoreRoute 語句后添加如下代碼捌议。
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();
routes.MapRoute(
...
第二步:為行為方法定義路由模式
在 EmployeeController 中的 Index 行為方法中附上 Route 屬性哼拔。
[Route("Employee/List")]
public ActionResult Index()
{
第三步:執(zhí)行并測試
執(zhí)行應用程序,然后完成登錄操作瓣颅。
正如你所看見的倦逐,我們擁有相同的輸出結果,但是不同的是擁有了更加用戶友好性的 URL宫补。
我們可以通過基于路由的屬性來定義路由參數嗎檬姥?
答案是肯定的曾我,可以查看如下語法。
[Route("Employee/List/{id}")]
publicActionResult Index (string id) { ... }
在這種情況下的限制呢健民?
這將會變得更加容易抒巢。
[Route("Employee/List/{id:int}")]
我們可以擁有如下限制。
{x:alpha} – 字符串認證
{x:bool} – 布爾認證
{x:datetime} – Date Time 認證
{x:decimal} – Decimal 認證
{x:double} – 64 位 Float 認證
{x:float} – 32 位 Float 認證
{x:guid} – GUID 認證
{x:length(6)} – 長度認證
{x:length(1,20)} – 最小和最大長度認證
{x:long} – 64 位 Int 認證
{x:max(10)} – 最大 Integer 長度認證
{x:maxlength(10)} – 最大長度認證
{x:min(10)} – 最小 Integer 長度認證
{x:minlength(10)} – 最小長度認證
{x:range(10,50)} – 整型 Range 認證
{x:regex(SomeRegularExpression)} – 正則表達式認證
在 RegisterRoutes 方法中 IgnoreRoutes 是用于做什么的秉犹?
當我們不想運用路由做指定擴展時虐秦,我們可以運用 IgnoreRoutes。作為 MVC 模板的一部分凤优,如下的代碼已經寫入 RegisterRoutes 方法中。
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
這意味著蜈彼,當終端用戶發(fā)出一個帶有「.axd」擴展的請求時筑辨,將不會執(zhí)行任何路由操作。請求將會直接定位到物理資源幸逆。我們也可以定義自己的 IgnoreRoute 語句棍辕。
9. 總結
在第 6 天的學習中,我們完成了簡單的 MVC 項目还绘。希望你能夠享受完成系列學習的樂趣楚昭。
稍等一下!第 7 天的學習呢拍顷?
在第 7 天中抚太,我們將會運用 MVC, JQuery 和 Ajax 來創(chuàng)建一個 Single Page 應用昔案。這將會更加有趣尿贫,并富有挑戰(zhàn)。
保持學習的熱情吧踏揣!
原文地址:Learn MVC Project in 7 days
OneAPM for .NET 能夠深入到所有 .NET 應用內部完成應用性能管理和監(jiān)控庆亡,包括代碼級別性能問題的可見性、性能瓶頸的快速識別與追溯捞稿、真實用戶體驗監(jiān)控又谋、服務器監(jiān)控和端到端的應用性能管理。想閱讀更多技術文章娱局,請訪問 OneAPM 官方博客彰亥。