目录

0. 前言

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

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

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

我们将会做两件事。

  1. 学会如何运用文件上传控件。

  2. 异步控制器。

第一步:创建 FileUploadViewModel

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

public class FileUploadViewModel: BaseViewModel
{
    public HttpPostedFileBase fileUpload {get; set ;}
}

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

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

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

public class BulkUploadController : Controller
{
        [HeaderFooterFilter]
        [AdminFilter]
        public ActionResult Index()
        {
            return View(new FileUploadViewModel());
        } 
}

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

第三步:创建上传视图

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

需要注意的是,视图的名称应该为 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 标签中,有一个额外的指定加密属性,我们将会在实验结尾处讨论它。

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

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

public void UploadEmployees(List<Employee> employees)
{
    SalesERPDAL salesDal = new SalesERPDAL();
    salesDal.Employees.AddRange(employees);
    salesDal.SaveChanges();
}

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

在 BulkUploadController 中创建一个新的行为方法,命名为 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 创建链接

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

<a href="/Employee/AddNew">Add New</a>
&nbsp;
&nbsp;
<a href="/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

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

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

什么是 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 请求向服务器发送如下数据。

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 请求会发送到服务器上。

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—

正如你所看见的,表单以多个部分被发送。每一个部分都通过 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。代码如下所示。

public ActionResult Upload(HttpPostedFileBase fileUpload)
{
}

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

2. 上述解决方案的问题

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

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

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

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

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

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

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

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

解决方案

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

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

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

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

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

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

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

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

第一步:创建异步控制器

将 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 存储在变量中。

现在让我理解下代码。

第三步:执行并测试

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

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

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

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

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

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

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

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

什么是 Exception 过滤器?

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

运用 Exception 过滤器的步骤。

它们是用来做什么的?

一旦在行为方法内部发生异常时,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 区域,在该区域下增加自定义错误,如下所示。

<system.web>
<customErrors mode="On"></customErrors>

第四步:创建错误视图

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

@{
    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 级别。

第六步:执行并测试

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

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>

第八步:执行并测试

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

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

我们是否错失了什么?

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

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

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

第九步:创建 ErrorController

在 Controller 文件夹下创建一个名为 ErrorController 的控制器,然后创建一个行为方法,命名为 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
{

第十二步:执行并测试

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

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 — 异常处理 — 异常日志

第一步:创建 Logger 类

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

在 Logger 文件夹下创建一个类,命名为 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
                });
        }
    }
}

第二步:创建 EmployeeExceptionFilter 类

在 Filters 文件夹下创建一个新的类,命名为 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,然后附上我们上一步骤中所创建的。

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    //filters.Add(new HandleErrorAttribute());//ExceptionFilter
    filters.Add(new EmployeeExceptionFilter());
    filters.Add(new AuthorizeAttribute());
}

第六步:执行并测试

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

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

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

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

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

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

Lab 30 的 Q&A

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

在上述实验中,我们重写了 OnException 方法,然后实现了异常日志的功能。现在的问题是,默认的错误处理过滤器是如何继续工作的?答案是简单地,查看 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 的问题,但是除了一个基本和重要的概念。

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

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

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

理解 RouteTable

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

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

RouteConfig.RegisterRoutes(RouteTable.Routes);

你将会在 App_Start 文件夹下发现 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 请求周期中被用到,用于决定执行确切的控制器和方法。

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

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

理解 ASP.NET MVC 请求周期

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

第一步:UrlRoutingModule

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

第二步:路由

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

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

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

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 中的行为方法,需要遵循如下的简单规则。即「在 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
       ...
    }
}

第五步:创建 MVCHandler

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

第六步:创建控制器实例

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

第七步:执行方法

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

第八步:触发行为方法

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

第九步:执行结果

行为方法接收到用户的输入,然后准备合适的响应数据,并通过返回一个类型来执行结果。现在返回的结果可能是 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>

第三步:执行并测试

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

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

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

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

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

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」来发出请求,行为方法将会得到执行,但是如果用户通过 URL「http://..../Employee/Sukesh」,他将会得到「Resource Not Found」的错误。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

[Route("Employee/List")]
public ActionResult Index()
{

第三步:执行并测试

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

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}");

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

9. 总结

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

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

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

保持学习的热情吧!

原文地址:Learn MVC Project in 7 days

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


↙↙↙阅读原文可查看相关链接,并与作者交流