7 天玩转 ASP.NET MVC ― 第 6 天

目录

  • 第 1 天

  • 第 2 天

  • 第 3 天

  • 第 4 天

  • 第 5 天

  • 第 6 天

  • 第 7 天

0. 前言

欢迎来到第六天的 MVC 系列学习中。希望你在阅读此篇文章的时候,已经学习了前五天的内容,这也是第六天学习的前提条件。

1. Lab 27 ― 添加批量上传选项

在这个实验中,我们将会创建一个选项,用于从 CSV 文件中上传多个 Employees。

我们将会做两件事。
1. 学会如何运用文件上传控件。

  1. 异步控制器。

第一步:创建 FileUploadViewModel

在 ViewModels 文件夹下创建一个类,命名为 FileUploadViewModel。

  1. publicclassFileUploadViewModel:BaseViewModel

  2. {

  3. publicHttpPostedFileBase fileUpload {get;set;}

  4. }

HttpPostedFileBase 将会通过客户端提供上传文件的访问入口。

第二步:创建 BulkUploadController 和 Index 行为方法

创建一个新的控制器,命名为 BulkUploadController,以及一个行为方法,命名为 Index。

  1. publicclassBulkUploadController:Controller

  2. {

  3. [HeaderFooterFilter]

  4. [AdminFilter]

  5. publicActionResultIndex()

  6. {

  7. returnView(newFileUploadViewModel());

  8. }

  9. }

正 如你所看见的,Index 行为方法附上了 HeaderFooterFilter 和 AdminFilter 属性。HeaderFooterFilter 确保了正确了页眉和页脚数据传输到 ViewModel,AdminFilter 限制了 Non-Admin 用户访问行为方法。

第三步:创建上传视图

为上述行为方法创建一个视图。

需要注意的是,视图的名称应该为 Index.cshtml,并且应该放置在「~/Views/BulkUpload」文件夹下。

第四步:设计上传视图

在视图中放置如下内容。

  1. @usingWebApplication1.ViewModels

  2. @modelFileUploadViewModel

  3. @{

  4. Layout="~/Views/Shared/MyLayout.cshtml";

  5. }


  6. @sectionTitleSection{

  7. BulkUpload

  8. }

  9. @sectionContentBody{

  10. <div>

  11. <a href="/Employee/Index">Back</a>

  12. <form action="/BulkUpload/Upload" method="post" enctype="multipart/form-data">

  13. SelectFile:<input type="file" name="fileUpload" value=""/>

  14. <input type="submit" name="name" value="Upload"/>

  15. </form>

  16. </div>

  17. }

正如你所看见的,在 FileUploadViewModel 中,属性的名称和 input[type="file"] 的名称是一样的,都是「FileUpload」。我们在 Model Binder 实验中已经讲述了名称属性的重要性。

注意:在 Form 标签中,有一个额外的指定加密属性,我们将会在实验结尾处讨论它。

第五步:创建业务层上传方法

在 EmployeeBusinessLayer 中创建一个新的方法,命名为 UploadEmployees。

  1. publicvoidUploadEmployees(List<Employee> employees)

  2. {

  3. SalesERPDAL salesDal =newSalesERPDAL();

  4. salesDal.Employees.AddRange(employees);

  5. salesDal.SaveChanges();

  6. }

第六步:创建上传行为方法

在 BulkUploadController 中创建一个新的行为方法,命名为 Upload。

  1. [AdminFilter]

  2. publicActionResultUpload(FileUploadViewModel model)

  3. {

  4. List<Employee> employees =GetEmployees(model);

  5. EmployeeBusinessLayer bal =newEmployeeBusinessLayer();

  6. bal.UploadEmployees(employees);

  7. returnRedirectToAction("Index","Employee");

  8. }


  9. privateList<Employee>GetEmployees(FileUploadViewModel model)

  10. {

  11. List<Employee> employees =newList<Employee>();

  12. StreamReader csvreader =newStreamReader(model.fileUpload.InputStream);

  13. csvreader.ReadLine();// Assuming first line is header

  14. while(!csvreader.EndOfStream)

  15. {

  16. var line = csvreader.ReadLine();

  17. var values = line.Split(',');//Values are comma separated

  18. Employee e =newEmployee();

  19. e.FirstName= values[0];

  20. e.LastName= values[1];

  21. e.Salary=int.Parse(values[2]);

  22. employees.Add(e);

  23. }

  24. return employees;

  25. }

在 Upload 中附上 AdminFilter 是用于限制 Non-Admin 用户访问。

第七步:为 BulkUpload 创建链接

在「Views/Employee」文件夹下打开 AddNewLink.cshtml 文件,为 BulkUpload 附上链接。

  1. <ahref="/Employee/AddNew">Add New</a>

  2. &nbsp;

  3. &nbsp;

  4. <ahref="/BulkUpload/Index">BulkUpload</a>

第八步:执行并测试

为测试创建一个简单的文件

创建一个简单的文件如下,然后将其保存在电脑中。

7 天玩转 ASP.NET MVC ― 第 6 天

执行并测试

按下 F5,然后执行应用。完成登录操作,然后通过点击链接导航到 BulkUpload 选项。

7 天玩转 ASP.NET MVC ― 第 6 天

选择一个文件,然后点击上传。

7 天玩转 ASP.NET MVC ― 第 6 天

注意:在上述的例子中,我们没有在视图中用到任何客户端或者服务器端的认证。它也许会导致如下的错误。

「Validation failed for one or more entities. See 'EntityValidationErrors' property for more details.」

为了发现这个错误的确切原因,只需要在异常发生的时候添加如下的表达式。

((System.Data.Entity.Validation.DbEntityValidationException)$exception).EntityValidationErrors。

表达式「$exception」呈现了任何从当前上下文中抛出的错误,即使它没有被捕获或者支配到一个变量中。

Lab 27 的 Q&A

为什么我们没有在这里用到认证?

为选项增加客户端和服务器端的认证将会留给读者完成,我在这里给出一些暗示。

  • 运用 Data Annotations 来进行服务器端的认证。

  • 你可以运用 Data Annotations 或者实现 JQuery Unobtrusive Validation 来实现客户端认证。明显的是,这一次你需要手动设置自定义数据属性,因为我们没有为文件输入创建 HtmlHelper 方法。

  • 对于客户端的认证,你可以写一些自定义的 JavaScript,然后通过点击安全触发它。这并不是很难,因为文件输入是一个输入控件,值可以通过在 JavaScript 中获取并认证。

什么是HttpPostedFileBase?

HttpPostedFileBase 可以通过客户端提供文件上传的访问接口。Model Binder 将会在发送 Post 请求时更新所有 FileUploadViewModel 类的属性值。现在 FileUploadViewModel 里只有一个属性值,Model Binder 将会通过客户端来设置这个属性值,实现文件上传。

提供多个文件输入控件是否可行?

答案是肯定的。我们可以通过两种方式实现它。

  1. 创 建多个文件输入控件。每一个控件都需要有唯一的名字。在 FileUploadViewModel 类中为每个控件创建一个 HttpPostedFileBase 的类型属性。每一个属性的名称应该与控件的名称相匹配。剩下的工作会由 ModelBinder 来处理。

  2. 创建多个文件输入控件。每一个控件都需要有唯一的名字。这次不是创建多个 HttpPostedFileBase 的属性,而是创建一个类型 List。
    注意:上述的情形对于所有控件都可行。当你拥有多个相同名称的控件时,如果要更新的属性值是一个简单参数,Model Binder 将会更新第一个控件的属性值。如果更新的属性值是一个 List,Model Binder 会将每一个属性值设置到控件中。

enctype="multipart/form-data"是用于做什么的?

这个对知道与否并不重要,但是知道确实会好一点。

这个属性指定了编码类型,在传输数据时使用。属性的默认值是「application/x-www-form-urlencoded」。

例如,我们的登录表单将会随着 Post 请求向服务器发送如下数据。

  1. POST /Authentication/DoLogin HTTP/1.1

  2. Host: localhost:8870

  3. Connection: keep-alive

  4. Content-Length:44

  5. Content-Type: application/x-www-form-urlencoded

  6. ...

  7. ...

  8. UserName=Admin&Passsword=Admin&BtnSubmi=Login

当 enctype="multipart/form-data"属性被添加到表单标签时,随着 Post 请求会发送到服务器上。

  1. POST /Authentication/DoLogin HTTP/1.1

  2. Host: localhost:8870

  3. Connection: keep-alive

  4. Content-Length:452

  5. Content-Type: multipart/form-data; boundary=----WebKitFormBoundarywHxplIF8cR8KNjeJ

  6. ...

  7. ...

  8. ------WebKitFormBoundary7hciuLuSNglCR8WC

  9. Content-Disposition: form-data; name="UserName"


  10. Admin

  11. ------WebKitFormBoundary7hciuLuSNglCR8WC

  12. Content-Disposition: form-data; name="Password"


  13. Admin

  14. ------WebKitFormBoundary7hciuLuSNglCR8WC

  15. Content-Disposition: form-data; name="BtnSubmi"


  16. Login

  17. ------WebKitFormBoundary7hciuLuSNglCR8WC

正如你所看见的,表单以多个部分被发送。每一个部分都通过 Content-Type 被一条边界线所分隔,并且每一个部分都包含一个值。

如果表单标签中包含文件输入控件时,编码类型需要设定为「multipart/form-data」。

注意:每一次请求发生时,边界线会随机生成。你可能会看到不同的边界线。

为什么我们不总是将 EncTyp 设置为「multipart/form-data」?

当 EncTyp 被设置为「multipart/form-data」,它将会做两件事,Post 数据以及上传文件。这就是为什么我们不总是将其设置为「multipart/form-data」。

答案就是,这样会增加请求的总体大小。请求的大小越大,意味着性能越差。因为最佳实践应该是将其设置为默认的值,即「application/x-www-form-urlencoded」。

为什么我们需要创建 ViewModel?

在我们的视图中有一个控件。我们可以通过直接向 HttpPostedFileBase 类型增加一个参数来实现同样的结果,这里我们需要在上传方法中命名为 「fileUpload」,而不是创建一个单独的 ViewModel。代码如下所示。

  1. publicActionResultUpload(HttpPostedFileBase fileUpload)

  2. {

  3. }

创建 ViewModel 是最佳实践。Controller 应该总是向视图发送以 ViewModel 为格式的数据,并且来自视图的数据应该以 ViewModel 发送给 Controller。

2. 上述解决方案的问题

你是否想知道,当你发送一个请求时,如何获得响应的?

现在不要去说,是通过行为方法接到请求然后怎样怎样的。尽管这是正确的答案,我仍然期望一些不同的答案。我的问题是在最开始的时候发生了什么。

一个简单的编程规则,程序中所有都通过线程执行,尽管是请求。

在 Web 服务器上的 ASP.NET,.NET Framework 维护着线程池。每一次请求发送到 Web 服务器上时,就会把一个线程池中一个空闲的线程分配给服务器,用于处理请求。这个线程被称为 Worker 线程。

7 天玩转 ASP.NET MVC ― 第 6 天

Worker 线程在请求正常处理的过程中处于阻塞状态,并且不能处理其它请求。

现 在来假设一种场景,一个应用接收到了很多请求,并且每个请求都会花费许多时间来处理进程。在这种情形下,没有 Worker 线程可用于服务器请求,所以当新的请求想要获取该线程进行处理状态时,我们可能需要在这时候终止它。这个我们称之为 Thread Starvation(线程饥饿)。

在我们的例子样本文件中,只存在了两个雇员记录,而在真实场景中,可能存在成千上万的记录,这意味着请求也许会花费大量时间来完成进程。这样会导致线程饥饿。

解决方案

迄今为止我们所讨论的请求都是同步请求类型。

如果客户端发出的是异步请求,而不是同步请求,那么线程饥饿的问题就解决了。

  • 在异步请求的情形下,请求将会从线程池分配中获得通常的 Worker 线程,用于服务请求。

  • Worker 线程将会初始化异步操作,然后返回线程池来服务其它请求。异步操作将会继续被 CLR 线程处理。

  • 现在的问题是,CLR 线程不能返回响应,所以一旦当完成异步操作后,它就会通知 ASP.NET。

  • Web 服务器将会再一次从线程池中得到 Worker 线程,用于处理剩余的请求和响应。

在上述的完整的场景中,两个 Worker 线程从线程池中获取。这两个 Worker 线程也许是同一个,也许不是。

在我们的例子中,文件读取是通过 I/O 操作的,这个操作不需要 Worker 线程来处理。所以最好是将同步请求转换为异步请求。

异步请求会提升响应时间吗?

答案是否定的。响应时间是相同的。这里线程将会被释放,用于服务其它请求。

3. Lab 28 ― 解决线程饥饿问题

在 ASP.NET MVC 中,我们可以通过转换同步行为方法到异步行为方法,来将同步请求转换为异步请求。

第一步:创建异步控制器

将 UploadController 的基类改为AsynController。

  1. publicclassBulkUploadController:AsyncController

  2. {

第二步:转换同步行为方法到异步行为方法

通过关键字,「async」和「await」,可以很容易做这件事。

  1. [AdminFilter]

  2. public async Task<ActionResult>Upload(FileUploadViewModel model)

  3. {

  4. int t1 =Thread.CurrentThread.ManagedThreadId;

  5. List<Employee> employees = await Task.Factory.StartNew<List<Employee>>

  6. (()=>GetEmployees(model));

  7. int t2 =Thread.CurrentThread.ManagedThreadId;

  8. EmployeeBusinessLayer bal =newEmployeeBusinessLayer();

  9. bal.UploadEmployees(employees);

  10. returnRedirectToAction("Index","Employee");

  11. }

正如你所看见的,我们在行为方法的开始和结束的地方将线程 ID 存储在变量中。

现在让我理解下代码。

  • 当客户端点击上传按钮时,一个新的请求将被发送到服务器。

  • Webserver 从线程池中获取一个 Worker 线程,然后将其分配给请求用于服务。

  • Worker 线程使得行为方法用于执行。

  • Worker 方法通过 Task.Factory.StartNew 方法执行异步操作。

  • 正如你所看见的,行为方法通过关键字 Async被标记为异步的,这将会确保一旦异步方法操作开始执行,Worker 线程就会得到释放。这个时候逻辑的异步操作将会通过独立的 CLR 线程继续在后台执行。

  • 现在异步操作调用将被标记为 Await 关键字。这将会确保接下来的代码行不会被执行,除非异步操作完成。

  • 一旦异步操作完成了,接下来的行为方法中的代码就需要被执行。因此又要需要一个 Worker 线程。因此 Webserver 将会从线程池中取出一个空闲线程,然后将其分配给剩余的请求用于服务,并返回响应。

第三步:执行并测试

执行应用。导航到 BulkUpload 选项。

在你做任何操作之前,先导航到代码,然后在最后一行代码中打个断点。

现在选择一个简单的文件,然后点击 Upload。

7 天玩转 ASP.NET MVC ― 第 6 天

正如你所看见的,在方法的开始和结束时,线程 ID 是不同的。输出的结果和之前的实验结果一样。

4. Lab 29 ― 异常处理 ― 呈现自定义错误页面

如果一个项目没有正确的异常处理,就不能算是一个完整的项目。

迄今为止,我们讨论过 ASP.NET MVC 中的两个过滤器,即 Action 过滤器和 Authentication 过滤器。现在是时候讨论第三个过滤器了,即 Exception 过滤器。

什么是 Exception 过滤器?

Exception 过滤器的使用方式同其它过滤器一样。我们将以属性的方式运用。

运用 Exception 过滤器的步骤。

  • 使它们可用

  • 将它们作为行为方法或者控制器的属性。我们也可以将它们应用到 Global 级别。

它们是用来做什么的?

一旦在行为方法内部发生异常时,Exception 过滤器就将会控制执行并开始自动执行其内部的代码。

是否存在自动的 Exception 过滤器?

ASP.NET MVC 提供给我们一个已经编写好的 Exception 过滤器,称作 HandleError。

正 如我们之前所说的,当行为方法中,一旦异常发生,过滤器就将被执行。这个过滤器将会在「~/Views/[current controller]」或者「~/Views/Shared」文件夹内发现一个名称为「Error」的视图,为这个视图创建一个 ViewResult,然后返回响应。

让我们看一个 Demo,用于更好地理解。在项目的实验最后,我们将会实现 BulkUpload 选项。现在存在着较高的输入文件的错误可能性。

第一步:创建一个简单的带有错误的 Upload 文件

创建一个简单的上传文件,就像之前一样。但是这次,文件中包含一些非法值。

7 天玩转 ASP.NET MVC ― 第 6 天

正如你所看见的,Salary 是非法的。

第二步:执行并测试应用

按下 F5,执行应用。导航到 Bulk Upload 选项,选择上述的文件,然后点击 Upload。

7 天玩转 ASP.NET MVC ― 第 6 天

第三步:使异常过滤器可用

自定义异常开启后,异常过滤器也被开启。为了开启自定义异常,打开 Web.config 文件,然后导航到 System.Web 区域,在该区域下增加自定义错误,如下所示。

  1. <system.web>

  2. <customErrorsmode="On"></customErrors>

第四步:创建错误视图

在「~Views/Shared」文件夹下,可以看到一个文件,即「Error.cshtml」。这个文件作为 MVC 样本文件的一部分在开始的时候被创建。如果没有被创建,就手动创建。

  1. @{

  2. Layout=null;

  3. }


  4. <!DOCTYPE html>

  5. <html>

  6. <head>

  7. <meta name="viewport" content="width=device-width"/>

  8. <title>Error</title>

  9. </head>

  10. <body>

  11. <hgroup>

  12. <h1>Error.</h1>

  13. <h2>An error occurred while processing your request.</h2>

  14. </hgroup>

  15. </body>

  16. </html>

第五步:附上 Exception 过滤器

正如我们之前所讨论的,一旦我们使异常过滤器可用,我们将会把它绑定到一个行为方法或者控制器中。

好的消息是我们无需手动附上过滤器。

在 App_Start 文件夹下打开 FilterConfig.cs 文件。在 RegisterGlobalFilter 方法下,你可以看到 HandleError 过滤器已经被附上 Global 级别。

  1. publicstaticvoidRegisterGlobalFilters(GlobalFilterCollection filters)

  2. {

  3. filters.Add(newHandleErrorAttribute());//ExceptionFilter

  4. filters.Add(newAuthorizeAttribute());

  5. }

如果需要移除 Global 过滤器,将会被附上方法或者控制器级别。

  1. [AdminFilter]

  2. [HandleError]

  3. public async Task<ActionResult>Upload(FileUploadViewModel model)

  4. {

但是不建议这么做,最好还是应用 Global 级别。

第六步:执行并测试

像之前的方式一样,让我们来看一下应用的测试结果。

7 天玩转 ASP.NET MVC ― 第 6 天

第七步:在视图中展示错误信息

为了达到这个目的,我们需要将错误视图转换为 HandleErrorInfo 类的强类型视图,然后在视图中展示错误信息。

  1. @modelHandleErrorInfo

  2. @{

  3. Layout=null;

  4. }


  5. <!DOCTYPE html>

  6. <html>

  7. <head>

  8. <meta name="viewport" content="width=device-width"/>

  9. <title>Error</title>

  10. </head>

  11. <body>

  12. <hgroup>

  13. <h1>Error.</h1>

  14. <h2>An error occurred while processing your request.</h2>

  15. </hgroup>

  16. ErrorMessage:@Model.Exception.Message<br />

  17. Controller:@Model.ControllerName<br />

  18. Action:@Model.ActionName

  19. </body>

  20. </html>

第八步:执行并测试

这次测试结果,我们将会得到如下的错误视图。

7 天玩转 ASP.NET MVC ― 第 6 天

我们是否错失了什么?

Handle Error 属性确保了无论何时行为方法发生异常时,自定义视图都会被呈现。但是仅限于控制器和行为方法。它不会处理「Resource not found」错误。

执行应用,输入一些古怪的 URL。

7 天玩转 ASP.NET MVC ― 第 6 天

第九步:创建 ErrorController

在 Controller 文件夹下创建一个名为 ErrorController 的控制器,然后创建一个行为方法,命名为 Index。

  1. publicclassErrorController:Controller

  2. {

  3. // GET: Error

  4. publicActionResultIndex()

  5. {

  6. Exception e=newException("Invalid Controller or/and Action Name");

  7. HandleErrorInfo eInfo =newHandleErrorInfo(e,"Unknown","Unknown");

  8. returnView("Error", eInfo);

  9. }

  10. }

HandleErrorInfo 控制器拥有三个参数,即异常对象,控制器名称和行为方法名称。

第十步:在非法的 URL 中呈现自定义错误视图

在 Web.config 中设定「Resource not found error」定义。

  1. <system.web>

  2. <customErrorsmode="On">

  3. <errorstatusCode="404"redirect="~/Error/Index"/>

  4. </customErrors>

第十一步:使所有人可访问 ErrorController

在 ErrorController 中应用 AllowAnonymous 属性,Index 方法不应该被绑定到一个有权限的用户。因为用户可能在登录前就输入了非法的 URL。

  1. [AllowAnonymous]

  2. publicclassErrorController:Controller

  3. {

第十二步:执行并测试

执行应用程序,然后在浏览器地址栏输入一些非法的 URL。

7 天玩转 ASP.NET MVC ― 第 6 天

Lab 29 的 Q&A

可以改变视图的名称吗?

答案是肯定的,保持视图名称为「Error」不是总是必须的。

在这种情形下,当附上 HandleError 过滤器时,我们需要指定视图的名称。

[HandleError(View="MyError")]

或者是

  1. filters.Add(newHandleErrorAttribute()

  2. {

  3. View="MyError"

  4. });

对于不同的异常,获取不同的错误视图,是否可行?

答案是肯定的,这是可行的。在这种情形下,我们需要应用 Handle Error 过滤器多次。

  1. [HandleError(View="DivideError",ExceptionType=typeof(DivideByZeroException))]

  2. [HandleError(View="NotFiniteError",ExceptionType=typeof(NotFiniteNumberException))]

  3. [HandleError]

或者是

  1. filters.Add(newHandleErrorAttribute()

  2. {

  3. ExceptionType=typeof(DivideByZeroException),

  4. View="DivideError"

  5. });

  6. filters.Add(newHandleErrorAttribute()

  7. {

  8. ExceptionType=typeof(NotFiniteNumberException),

  9. View="NotFiniteError"

  10. });

  11. filters.Add(newHandleErrorAttribute());

在上述的例子中,我们增加了三个 Handle Error 过滤器。前两个为指定的异常,而后一个更加通用一些,它将会为所有其它异常展示错误视图。

5. 理解上述实验的局限

上述实验存在唯一的局限,便是我们没有将异常日志输出。

6. Lab 30 ― 异常处理 ― 异常日志

第一步:创建 Logger 类

在项目的根目录下创建一个新的文件夹,称为 Logger。

在 Logger 文件夹下创建一个类,命名为 FileLogger。

  1. namespaceWebApplication1.Logger

  2. {

  3. publicclassFileLogger

  4. {

  5. publicvoidLogException(Exception e)

  6. {

  7. File.WriteAllLines("C://Error//"+DateTime.Now.ToString("dd-MM-yyyy mm hh ss")+".txt",

  8. newstring[]

  9. {

  10. "Message:"+e.Message,

  11. "Stacktrace:"+e.StackTrace

  12. });

  13. }

  14. }

  15. }

第二步:创建 EmployeeExceptionFilter 类

在 Filters 文件夹下创建一个新的类,命名为 EmployeeExceptionFilter。

  1. namespaceWebApplication1.Filters

  2. {

  3. publicclassEmployeeExceptionFilter

  4. {

  5. }

  6. }

第三步:扩展 Handle Error 用于实现日志记录

让 EmployeeExceptionFilter 类继承 HandleErrorAttribute 类,然后重写 OnException 方法。

  1. publicclassEmployeeExceptionFilter:HandleErrorAttribute

  2. {

  3. publicoverridevoidOnException(ExceptionContext filterContext)

  4. {

  5. base.OnException(filterContext);

  6. }

  7. }

注意:确保在 HandleErrorAttribute 类中的顶部引用了 System.Web.MVC。

第四步:定义 OnException 方法

在 OnException 方法中包含异常日志记录代码,如下所示。

  1. publicoverridevoidOnException(ExceptionContext filterContext)

  2. {

  3. FileLogger logger =newFileLogger();

  4. logger.LogException(filterContext.Exception);

  5. base.OnException(filterContext);

  6. }

第五步:改变默认的异常过滤器

打开 FilterConfig.cs 文件,移除 HandleErrorAttribute,然后附上我们上一步骤中所创建的。

  1. publicstaticvoidRegisterGlobalFilters(GlobalFilterCollection filters)

  2. {

  3. //filters.Add(new HandleErrorAttribute());//ExceptionFilter

  4. filters.Add(newEmployeeExceptionFilter());

  5. filters.Add(newAuthorizeAttribute());

  6. }

第六步:执行并测试

首先在 C 盘下创建一个文件夹,命名为「Error」。这个文件夹会存放错误的日志文件。

注意:可以更改路径为你所期望的路径。

按下 F5,然后执行应用。导航到 Bulk Upload 选项。选择文件,然后点击 Upload。

7 天玩转 ASP.NET MVC ― 第 6 天

这次的输出将会有所不同,我们将会得到一些错误视图,就像之前一样。唯一的不同便是我们会在「C:\Errors」文件夹发现一些错误日志文件。

7 天玩转 ASP.NET MVC ― 第 6 天

Lab 30 的 Q&A

异常发生时,错误视图是如何作为响应返回的?

在上述实验中,我们重写了 OnException 方法,然后实现了异常日志的功能。现在的问题是,默认的错误处理过滤器是如何继续工作的?答案是简单地,查看 OnException 方法的最后一行代码。

  1. base.OnException(filterContext);

这意味着,基类 OnException 将会做剩余的工作,基类 OnException 将会返回错误视图的 ViewResult。

在 OnException 中,我们可以返回其它结果吗?

答案是肯定的,查看如下代码。

  1. publicoverridevoidOnException(ExceptionContext filterContext)

  2. {

  3. FileLogger logger =newFileLogger();

  4. logger.LogException(filterContext.Exception);

  5. //base.OnException(filterContext);

  6. filterContext.ExceptionHandled=true;

  7. filterContext.Result=newContentResult()

  8. {

  9. Content="Sorry for the Error"

  10. };

  11. }

当我们想要返回自定义响应时,首先要做的事便是,通知 MVC 引擎,告知其我们已经手动处理异常了,所以不需要做默认的行为,即不需要呈现默认的错误屏幕。这一切可以通过如下代码来实现。

  1. filterContext.ExceptionHandled=true

7. 路由

迄今为止我们讨论过许多概念,我们也回答了许多有关 MVC 的问题,但是除了一个基本和重要的概念。

「当用户发出请求时,确切发生了什么」?

一个很好的答案便是「行为方法的执行」。但是确切的答案是控制器和犯法是如何被一个特定的 URL 请求识别的?

当我们开始「实现用户友好的 URLs」的实验时,我们首先需要回答上述的问题。你也许会奇怪为什么这个主题会放置到最后。我故意将其放置到最后,是因为我想让更多的人在理解内部之前,先了解 MVC。

理解 RouteTable

在 ASP.NET MVC 中,存在一个概念,称作 RouteTable。这里存储了应用的 URL 路由。用简单的话说,它承载了一个应用的 URL 模式的集合。

默认情况下,一个路由将会作为项目模板的一部分被添加。可以通过 Global.asax 文件查看它。在 Application_Start 中,你将会发现如下的代码。

  1. RouteConfig.RegisterRoutes(RouteTable.Routes);

你将会在 App_Start 文件夹下发现 RouteConfig.cs 文件,它包含了如下代码。

  1. namespaceWebApplication1

  2. {

  3. publicclassRouteConfig

  4. {

  5. publicstaticvoidRegisterRoutes(RouteCollection routes)

  6. {

  7. routes.IgnoreRoute("{resource}.axd/{*pathInfo}");


  8. routes.MapRoute(

  9. name:"Default",

  10. url:"{controller}/{action}/{id}",

  11. defaults:new{ controller ="Home", action ="Index", id =UrlParameter.Optional}

  12. );

  13. }

  14. }

  15. }

正如你所看见的,RegisterRoutes 方法已经通过 Route.MapRoutes 方法定义了一个默认的路由。

在 RegisterRoutes 方法中定义的路由将会在 ASP.NET MVC 请求周期中被用到,用于决定执行确切的控制器和方法。

如果需要,我们可以通过使用 Route.MapRoutes 函数,创建多个路由。内部定义路由意味着创建 Route 对象。

MapRoute 函数也可以把路由对象附上 RouteHandler,这样将会是 MVCRouteHandler。

理解 ASP.NET MVC 请求周期

在我们开始之前,你需要清楚,我们将要 100% 地解释请求周期。我们将要接触到之前未讲到的重要概念。

第一步:UrlRoutingModule

当终端用户发出请求后,首先会通过 UrlRoutingModule 对象。UrlRoutingModule 是一个 HTTP 模块。

第二步:路由

UrlRoutingModule 首先会从路由集合中匹配 Route 对象。对于匹配,请求的 URL 将会与路由中定义的 URL 模式相对比。

下述的规则将会在匹配中被考虑到。

  • 请求 URL 中参数的数字以及在路由中定义的 URL 模式。例如:

7 天玩转 ASP.NET MVC ― 第 6 天

  • URL 模式中定义的可选参数。例如:

7 天玩转 ASP.NET MVC ― 第 6 天

  • 在参数中定义的静态参数。

7 天玩转 ASP.NET MVC ― 第 6 天

第三步:创建 MVC Route Handler

一旦路由对象被选中,UrlRoutingModule 将会从路由对象中获得 MvcRouteHandler。

第四步:创建 RouteData 和 RequestContext

UrlRoutingModule 对象将会通过 Route 对象创建 RouteData,它将会用于创建 RequestContext。

RouteData 封装了关于路由的信息,如控制器的名称,行为方法的名称,路由参数的值。

Controller 名称

为了从请求 URL 中获得控制器的名称,需要遵循如下的简单规则。即“在 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 中的行为方法,需要遵循如下的简单规则。即「在 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 模式包含如下四个要素。

  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」。

测试一:

  1. publicclassBulkUploadController:Controller

  2. {

  3. publicActionResultUpload(string id)

  4. {

  5. //value of id will be 5 -> string 5

  6. ...

  7. }

  8. }

测试二:

  1. publicclassBulkUploadController:Controller

  2. {

  3. publicActionResultUpload(int id)

  4. {

  5. //value of id will be 5 -> int 5

  6. ...

  7. }

  8. }

测试三:

  1. publicclassBulkUploadController:Controller

  2. {

  3. publicActionResultUpload(stringMyId)

  4. {

  5. //value of MyId will be null

  6. ...

  7. }

  8. }

第五步:创建 MVCHandler

MvcRouteHandler 将会创建 MVCHandler 的实例,传输 RequestContext 对象。

第六步:创建控制器实例

MVCHandler 将会通过 ControllerFactory(默认的是 DefaultControllerFactory) 创建控制器实例。

第七步:执行方法

MVCHandler 将会触发控制器的执行方法。执行方法在控制器基类中被定义。

第八步:触发行为方法

每一个控制器都与一个 ControllerActionInvoker 对象相关联。在执行方法中,ControllerActionInvoker 触发正确的行为方法。

第九步:执行结果

行为方法接收到用户的输入,然后准备合适的响应数据,并通过返回一个类型来执行结果。现在返回的结果可能是 ViewResult,可能是 RedirectToRoute 结果或者可能是其它。

现在,我相信你已经对路由的概念有了很好的理解,所以让我们通过路由来使得项目的 URLs 更友好吧。

8. Lab 31 ― 实现用户友好性的 URLs

第一步:重新定义 RegisterRoutes 方法

在 RegisterRoutes 方法中包含额外的路由。

  1. publicstaticvoidRegisterRoutes(RouteCollection routes)

  2. {

  3. routes.IgnoreRoute("{resource}.axd/{*pathInfo}");


  4. routes.MapRoute(

  5. name:"Upload",

  6. url:"Employee/BulkUpload",

  7. defaults:new{ controller ="BulkUpload", action ="Index"}

  8. );


  9. routes.MapRoute(

  10. name:"Default",

  11. url:"{controller}/{action}/{id}",

  12. defaults:new{ controller ="Home", action ="Index", id =UrlParameter.Optional}

  13. );

  14. }

正如你所看见的,我们现在已经不止定义一个路由了。

第二步:更改 URL 引用

从「~/Views/Employee」文件夹下打开 AddNewLink.cshtml 文件,然后更改 BulkUpload 链接如下。

  1. &nbsp;

  2. <a href="/Employee/BulkUpload">BulkUpload</a>

第三步:执行并测试

执行应用,将会看到神奇的地方。

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 来用于获取值。它是一个可被替换的查询字符串。

路由参数和查询字符串的区别是什么?

  • 查询字符串有大小限制,然而我们可以定义路由参数的任意数字。

  • 我们不能向查询字符串值添加限制,但是我们可以向路由参数添加限制。

  • 可以设定路由参数的默认值,然而查询字符串的默认值不可设定。

  • 查询字符串使得 URL 凌乱,但是路由参数保持 URL 整洁。

如何向路由参数应用限制?

可以通过正则表达式来完成这件事。例如,查看如下路由。

  1. routes.MapRoute(

  2. "MyRoute",

  3. "Employee/{EmpId}",

  4. new{controller=" Employee ", action="GetEmployeeById"},

  5. new{EmpId=@"\d+"}

  6. );

行为方法将如下所示。

  1. publicActionResultGetEmployeeById(intEmpId)

  2. {

  3. ...

  4. }

现在如果用户通过 URL「http://..../Employee/1」 或者 「http://..../Employee/111」来发出请求,行为方法将会得到执行,但是如果用户通过 URL「http://..../Employee/Sukesh」 ,他将会得到「Resource Not Found」的错误。

行为方法中的参数名称和路由参数名称需要保持一致吗?

从根本上说,路由模式也许包含多个 RouteParameters。为了单独地识别每一个路由参数,需要保持行为方法中的参数名称和路由参数名称一致。

定义自定义路由的次序重要吗?

答案是肯定的,次序是重要的。UrlRoutingModule 将会匹配第一个路由对象。

在上述的实验中,我们已经定义了两个路由。一个是自定义路由,一个是默认路由。现在我们来讨论一种情况,默认路由被首先定义,自定义路由被第二个定义。

在 这种情况下,终端用户发起一个请求 URL,即「http://…/Employee/BulkUpload」。在匹配阶段,UrlRoutingModules 将会发现请求的 URL 与默认的路由模式匹配,它将会认为「Employee」是控制器的名称,「BulkUpload」是行为方法的名称。

因此次序在定义路由时是非常重要的。大多数通用的路由应该被放置到最后。

是否存在更简单的方式来定义行为方法的 URL 模式?

我们可以运用基于路由的属性来解决这个问题。让我们来试一下。

第一步:使基于路由的属性可用

在 RegisterRoutes 方法中的 IgnoreRoute 语句后添加如下代码。

  1. routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

  2. routes.MapMvcAttributeRoutes();

  3. routes.MapRoute(

  4. ...

第二步:为行为方法定义路由模式

在 EmployeeController 中的 Index 行为方法中附上 Route 属性。

  1. [Route("Employee/List")]

  2. publicActionResultIndex()

  3. {

第三步:执行并测试

执行应用程序,然后完成登录操作。

7 天玩转 ASP.NET MVC ― 第 6 天

正如你所看见的,我们拥有相同的输出结果,但是不同的是拥有了更加用户友好性的 URL。

我们可以通过基于路由的属性来定义路由参数吗?

答案是肯定的,可以查看如下语法。

  1. [Route("Employee/List/{id}")]

  2. publicActionResult Index(string id){...}

在这种情况下的限制呢?

这将会变得更加容易。

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

我们可以拥有如下限制。

  1. {x:alpha} �C 字符串认证

  2. {x:bool} �C 布尔认证

  3. {x:datetime} �C Date Time 认证

  4. {x:decimal} �C Decimal 认证

  5. {x:double} �C 64 位 Float 认证

  6. {x:float} �C 32 位 Float 认证

  7. {x:guid} �C GUID 认证

  8. {x:length(6)} �C 长度认证

  9. {x:length(1,20)} �C 最小和最大长度认证

  10. {x:long} �C 64 位 Int 认证

  11. {x:max(10)} �C 最大 Integer 长度认证

  12. {x:maxlength(10)} �C 最大长度认证

  13. {x:min(10)} �C 最小 Integer 长度认证

  14. {x:minlength(10)} �C 最小长度认证

  15. {x:range(10,50)} �C 整型 Range 认证

  16. {x:regex(SomeRegularExpression)} �C 正则表达式认证

在 RegisterRoutes 方法中 IgnoreRoutes 是用于做什么的?

当我们不想运用路由做指定扩展时,我们可以运用 IgnoreRoutes。作为 MVC 模板的一部分,如下的代码已经写入 RegisterRoutes 方法中。

routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

这意味着,当终端用户发出一个带有「.axd」扩展的请求时,将不会执行任何路由操作。请求将会直接定位到物理资源。我们也可以定义自己的 IgnoreRoute 语句。

9. 总结

在第 6 天的学习中,我们完成了简单的 MVC 项目。希望你能够享受完成系列学习的乐趣。

稍等一下!第 7 天的学习呢?

在第 7 天中,我们将会运用 MVC, JQuery 和 Ajax 来创建一个 Single Page 应用。这将会更加有趣,并富有挑战。

保持学习的热情吧!

原文地址:Learn MVC Project in 7 days

OneAPM for .NET 能够深入到所有 .NET 应用内部完成应用性能管理和监控,包括代码级别性能问题的可见性、性能瓶颈的快速识别与追溯、真实用户体验监控、服务器监控和端到端的应用性能管理。想阅读更多技术文章,请访问 OneAPM 官方博客。





你可能感兴趣的:(asp.net)