7 天玩轉 ASP.NET MVC — 第 6 天

目錄

0. 前言

歡迎來到第六天的 MVC 系列學習中。希望你在閱讀此篇文章的時候,已經學習了前五天的內容,這也是第六天學習的前提條件夷磕。

1. Lab 27 — 添加批量上傳選項

在這個實驗中,我們將會創(chuàng)建一個選項,用于從 CSV 文件中上傳多個 Employees耗溜。

我們將會做兩件事。

  1. 學會如何運用文件上傳控件省容。

  2. 異步控制器抖拴。

第一步:創(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>
&nbsp;
&nbsp;
<a href="/BulkUpload/Index">BulkUpload</a>

第八步:執(zhí)行并測試

為測試創(chuàng)建一個簡單的文件

創(chuàng)建一個簡單的文件如下,然后將其保存在電腦中李请。

7 天玩轉 ASP.NET MVC — 第 6 天
7 天玩轉 ASP.NET MVC — 第 6 天

執(zhí)行并測試

按下 F5瞧筛,然后執(zhí)行應用。完成登錄操作导盅,然后通過點擊鏈接導航到 BulkUpload 選項较幌。

7 天玩轉 ASP.NET MVC — 第 6 天
7 天玩轉 ASP.NET MVC — 第 6 天

選擇一個文件,然后點擊上傳白翻。

7 天玩轉 ASP.NET MVC — 第 6 天
7 天玩轉 ASP.NET MVC — 第 6 天

注意:在上述的例子中乍炉,我們沒有在視圖中用到任何客戶端或者服務器端的認證。它也許會導致如下的錯誤嘁字。

「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 將會通過客戶端來設置這個屬性值,實現文件上傳邀摆。

提供多個文件輸入控件是否可行纵顾?

答案是肯定的。我們可以通過兩種方式實現它隧熙。

  1. 創(chuàng)建多個文件輸入控件片挂。每一個控件都需要有唯一的名字幻林。在 FileUploadViewModel 類中為每個控件創(chuàng)建一個 HttpPostedFileBase 的類型屬性贞盯。每一個屬性的名稱應該與控件的名稱相匹配。剩下的工作會由 ModelBinder 來處理沪饺。

  2. 創(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 線程。

7 天玩轉 ASP.NET MVC — 第 6 天
7 天玩轉 ASP.NET MVC — 第 6 天

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子寓。

7 天玩轉 ASP.NET MVC — 第 6 天
7 天玩轉 ASP.NET MVC — 第 6 天

正如你所看見的,在方法的開始和結束時笋除,線程 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)建一個簡單的上傳文件,就像之前一樣了牛。但是這次颜屠,文件中包含一些非法值辰妙。

7 天玩轉 ASP.NET MVC — 第 6 天
7 天玩轉 ASP.NET MVC — 第 6 天

正如你所看見的,Salary 是非法的甫窟。

第二步:執(zhí)行并測試應用

按下 F5密浑,執(zhí)行應用。導航到 Bulk Upload 選項粗井,選擇上述的文件尔破,然后點擊 Upload。

7 天玩轉 ASP.NET MVC — 第 6 天
7 天玩轉 ASP.NET MVC — 第 6 天

第三步:使異常過濾器可用

自定義異常開啟后背传,異常過濾器也被開啟呆瞻。為了開啟自定義異常,打開 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í)行并測試

像之前的方式一樣席噩,讓我們來看一下應用的測試結果班缰。

7 天玩轉 ASP.NET MVC — 第 6 天
7 天玩轉 ASP.NET MVC — 第 6 天

第七步:在視圖中展示錯誤信息

為了達到這個目的,我們需要將錯誤視圖轉換為 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í)行并測試

這次測試結果,我們將會得到如下的錯誤視圖。

7 天玩轉 ASP.NET MVC — 第 6 天
7 天玩轉 ASP.NET MVC — 第 6 天

我們是否錯失了什么给梅?

Handle Error 屬性確保了無論何時行為方法發(fā)生異常時假丧,自定義視圖都會被呈現。但是僅限于控制器和行為方法动羽。它不會處理「Resource not found」錯誤包帚。

執(zhí)行應用,輸入一些古怪的 URL运吓。

7 天玩轉 ASP.NET MVC — 第 6 天
7 天玩轉 ASP.NET MVC — 第 6 天

第九步:創(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癣亚。

7 天玩轉 ASP.NET MVC — 第 6 天
7 天玩轉 ASP.NET MVC — 第 6 天

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饶囚。

7 天玩轉 ASP.NET MVC — 第 6 天
7 天玩轉 ASP.NET MVC — 第 6 天

這次的輸出將會有所不同,我們將會得到一些錯誤視圖鸠补,就像之前一樣萝风。唯一的不同便是我們會在「C:\Errors」文件夾發(fā)現一些錯誤日志文件。

7 天玩轉 ASP.NET MVC — 第 6 天
7 天玩轉 ASP.NET MVC — 第 6 天

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 模式干奢。例如:
7 天玩轉 ASP.NET MVC — 第 6 天
7 天玩轉 ASP.NET MVC — 第 6 天
  • URL 模式中定義的可選參數。例如:
7 天玩轉 ASP.NET MVC — 第 6 天
7 天玩轉 ASP.NET MVC — 第 6 天
  • 在參數中定義的靜態(tài)參數坐儿。
7 天玩轉 ASP.NET MVC — 第 6 天
7 天玩轉 ASP.NET MVC — 第 6 天

第三步:創(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 中的行為方法,需要遵循如下的簡單規(guī)則佑稠。即「在 URL 模式中 {Action} 是行為方法名稱的關鍵詞」秒梅。

例如:

路由參數

一個基本的 URL 模式包含如下四個要素。

  1. {Controller}婉烟,用于識別控制器名稱娩井。

  2. {Action},識別行為方法名稱似袁。

  3. 一些字符串洞辣,例如「MyCompany/{Controller}/{Action}」,在這個模式中昙衅,「MyCompany」是一個必須的字符串扬霜。

  4. {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 鏈接如下司澎。

&nbsp;
<a href="/Employee/BulkUpload">BulkUpload</a>

第三步:執(zhí)行并測試

執(zhí)行應用欺缘,將會看到神奇的地方。

7 天玩轉 ASP.NET MVC — 第 6 天
7 天玩轉 ASP.NET MVC — 第 6 天

正如你所看見的挤安,URL 不再是“Controller/Action”的形式谚殊。它看起來更加用戶友好,但是輸出是一樣的蛤铜。

我建議你定義更多的路由嫩絮,嘗試更多的 URLs。

Lab 31 的 Q&A

之前的 URL 還是否起作用围肥?

答案是肯定的剿干,之前的 URL 也會起作用。

現在 BulkUploadController 中的 Index 方法可以通過兩個 URLs 訪問穆刻。

  1. http://localhost:8870/Employee/BulkUpload

  2. http://localhost:8870/BulkUpload/Index

默認路由中的「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í)行應用程序,然后完成登錄操作瓣颅。

7 天玩轉 ASP.NET MVC — 第 6 天
7 天玩轉 ASP.NET MVC — 第 6 天

正如你所看見的倦逐,我們擁有相同的輸出結果,但是不同的是擁有了更加用戶友好性的 URL宫补。

我們可以通過基于路由的屬性來定義路由參數嗎檬姥?

答案是肯定的曾我,可以查看如下語法。

[Route("Employee/List/{id}")]
publicActionResult Index (string id) { ... }

在這種情況下的限制呢健民?

這將會變得更加容易抒巢。

[Route("Employee/List/{id:int}")]

我們可以擁有如下限制。

  1. {x:alpha} – 字符串認證

  2. {x:bool} – 布爾認證

  3. {x:datetime} – Date Time 認證

  4. {x:decimal} – Decimal 認證

  5. {x:double} – 64 位 Float 認證

  6. {x:float} – 32 位 Float 認證

  7. {x:guid} – GUID 認證

  8. {x:length(6)} – 長度認證

  9. {x:length(1,20)} – 最小和最大長度認證

  10. {x:long} – 64 位 Int 認證

  11. {x:max(10)} – 最大 Integer 長度認證

  12. {x:maxlength(10)} – 最大長度認證

  13. {x:min(10)} – 最小 Integer 長度認證

  14. {x:minlength(10)} – 最小長度認證

  15. {x:range(10,50)} – 整型 Range 認證

  16. {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 官方博客彰亥。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市铃辖,隨后出現的幾起案子剩愧,更是在濱河造成了極大的恐慌,老刑警劉巖娇斩,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件仁卷,死亡現場離奇詭異穴翩,居然都是意外死亡,警方通過查閱死者的電腦和手機锦积,發(fā)現死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門芒帕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人丰介,你說我怎么就攤上這事背蟆。” “怎么了哮幢?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵带膀,是天一觀的道長。 經常有香客問我橙垢,道長垛叨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任柜某,我火速辦了婚禮嗽元,結果婚禮上,老公的妹妹穿的比我還像新娘喂击。我一直安慰自己剂癌,他們只是感情好,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布翰绊。 她就那樣靜靜地躺著佩谷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪辞做。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天稚补,我揣著相機與錄音框喳,去河邊找鬼课幕。 笑死五垮,一個胖子當著我的面吹牛,可吹牛的內容都是我干的放仗。 我是一名探鬼主播润绎,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼莉撇!你這毒婦竟也來了?” 一聲冷哼從身側響起涂佃,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤汽抚,失蹤者是張志新(化名)和其女友劉穎伯病,沒想到半個月后狱从,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年驼卖,在試婚紗的時候發(fā)現自己被綠了鸿秆。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卿叽。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖考婴,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情缎罢,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布舰始,位于F島的核電站蔽午,受9級特大地震影響酬蹋,放射性物質發(fā)生泄漏范抓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一僧鲁、第九天 我趴在偏房一處隱蔽的房頂上張望寞秃。 院中可真熱鬧偶惠,春花似錦忽孽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽骂束。三九已至栖雾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間召廷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工先紫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留遮精,地道東北人败潦。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓檬洞,卻偏偏與公主長得像添怔,于是被迫代替她去往敵國和親幼驶。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內容

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理顶瞒,服務發(fā)現守问,斷路器袱贮,智...
    卡卡羅2017閱讀 134,601評論 18 139
  • 目錄 第 1 天 第 2 天 第 3 天 第 4 天 第 5 天 第 6 天 第 7 天 0. 前言 今天是開心的...
    OneAPM閱讀 1,652評論 0 17
  • 目錄 第 1 天 第 2 天 第 3 天 第 4 天 第 5 天 第 6 天 第 7 天 0. 前言 歡迎來到第四...
    OneAPM閱讀 905評論 0 6
  • 目錄 第 1 天 第 2 天 第 3 天 第 4 天 第 5 天 第 6 天 第 7 天 0. 前言 歡迎來到第五...
    OneAPM閱讀 865評論 0 5
  • 腰痛腿麻二十年,遷延不愈路難行仅偎。 輾轉求醫(yī)三五家,療效不佳念俱灰。 他人介紹慕名來箫措,手術治療果不凡镀岛。 術后三年再無...
    徐一村閱讀 407評論 0 5