7 天玩转 ASP.NET MVC — 第 6 天
目录
0. 前言
欢迎来到第六天的 MVC 系列学习中。希望你在阅读此篇文章的时候,已经学习了前五天的内容,这也是第六天学习的前提条件。
1. Lab 27 — 添加批量上传选项
在这个实验中,我们将会创建一个选项,用于从 CSV 文件中上传多个 Employees。
我们将会做两件事。
-
学会如何运用文件上传控件。
-
异步控制器。
第一步:创建 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>
<a href="/BulkUpload/Index">BulkUpload</a>
第八步:执行并测试
为测试创建一个简单的文件
创建一个简单的文件如下,然后将其保存在电脑中。
执行并测试
按下 F5,然后执行应用。完成登录操作,然后通过点击链接导航到 BulkUpload 选项。
选择一个文件,然后点击上传。
注意:在上述的例子中,我们没有在视图中用到任何客户端或者服务器端的认证。它也许会导致如下的错误。
「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 将会通过客户端来设置这个属性值,实现文件上传。
提供多个文件输入控件是否可行?
答案是肯定的。我们可以通过两种方式实现它。
-
创建多个文件输入控件。每一个控件都需要有唯一的名字。在 FileUploadViewModel 类中为每个控件创建一个 HttpPostedFileBase 的类型属性。每一个属性的名称应该与控件的名称相匹配。剩下的工作会由 ModelBinder 来处理。
-
创建多个文件输入控件。每一个控件都需要有唯一的名字。这次不是创建多个 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 线程。
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。
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 存储在变量中。
现在让我理解下代码。
-
当客户端点击上传按钮时,一个新的请求将被发送到服务器。
-
Webserver 从线程池中获取一个 Worker 线程,然后将其分配给请求用于服务。
-
Worker 线程使得行为方法用于执行。
-
Worker 方法通过 Task.Factory.StartNew 方法执行异步操作。
-
正如你所看见的,行为方法通过关键字 Async被标记为异步的,这将会确保一旦异步方法操作开始执行,Worker 线程就会得到释放。这个时候逻辑的异步操作将会通过独立的 CLR 线程继续在后台执行。
-
现在异步操作调用将被标记为 Await 关键字。这将会确保接下来的代码行不会被执行,除非异步操作完成。
-
一旦异步操作完成了,接下来的行为方法中的代码就需要被执行。因此又要需要一个 Worker 线程。因此 Webserver 将会从线程池中取出一个空闲线程,然后将其分配给剩余的请求用于服务,并返回响应。
第三步:执行并测试
执行应用。导航到 BulkUpload 选项。
在你做任何操作之前,先导航到代码,然后在最后一行代码中打个断点。
现在选择一个简单的文件,然后点击 Upload。
正如你所看见的,在方法的开始和结束时,线程 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 文件
创建一个简单的上传文件,就像之前一样。但是这次,文件中包含一些非法值。
正如你所看见的,Salary 是非法的。
第二步:执行并测试应用
按下 F5,执行应用。导航到 Bulk Upload 选项,选择上述的文件,然后点击 Upload。
第三步:使异常过滤器可用
自定义异常开启后,异常过滤器也被开启。为了开启自定义异常,打开 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 级别。
第六步:执行并测试
像之前的方式一样,让我们来看一下应用的测试结果。
第七步:在视图中展示错误信息
为了达到这个目的,我们需要将错误视图转换为 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>
第八步:执行并测试
这次测试结果,我们将会得到如下的错误视图。
我们是否错失了什么?
Handle Error 属性确保了无论何时行为方法发生异常时,自定义视图都会被呈现。但是仅限于控制器和行为方法。它不会处理「Resource not found」错误。
执行应用,输入一些古怪的 URL。
第九步:创建 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。
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。
这次的输出将会有所不同,我们将会得到一些错误视图,就像之前一样。唯一的不同便是我们会在「C:\Errors」文件夹发现一些错误日志文件。
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 模式相对比。
下述的规则将会在匹配中被考虑到。
- 请求 URL 中参数的数字以及在路由中定义的 URL 模式。例如:
- URL 模式中定义的可选参数。例如:
- 在参数中定义的静态参数。
第三步:创建 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 模式包含如下四个要素。
-
{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
...
}
}
第五步:创建 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 链接如下。
<a href="/Employee/BulkUpload">BulkUpload</a>
第三步:执行并测试
执行应用,将会看到神奇的地方。
正如你所看见的,URL 不再是“Controller/Action”的形式。它看起来更加用户友好,但是输出是一样的。
我建议你定义更多的路由,尝试更多的 URLs。
Lab 31 的 Q&A
之前的 URL 还是否起作用?
答案是肯定的,之前的 URL 也会起作用。
现在 BulkUploadController 中的 Index 方法可以通过两个 URLs 访问。
默认路由中的「Id」是什么?
我们之前提到过它。它被称作路由参数。它可以通过 URL 来用于获取值。它是一个可被替换的查询字符串。
路由参数和查询字符串的区别是什么?
-
查询字符串有大小限制,然而我们可以定义路由参数的任意数字。
-
我们不能向查询字符串值添加限制,但是我们可以向路由参数添加限制。
-
可以设定路由参数的默认值,然而查询字符串的默认值不可设定。
-
查询字符串使得 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」来发出请求,行为方法将会得到执行,但是如果用户通过 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()
{
第三步:执行并测试
执行应用程序,然后完成登录操作。
正如你所看见的,我们拥有相同的输出结果,但是不同的是拥有了更加用户友好性的 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}");
这意味着,当终端用户发出一个带有「.axd」扩展的请求时,将不会执行任何路由操作。请求将会直接定位到物理资源。我们也可以定义自己的 IgnoreRoute 语句。
9. 总结
在第 6 天的学习中,我们完成了简单的 MVC 项目。希望你能够享受完成系列学习的乐趣。
稍等一下!第 7 天的学习呢?
在第 7 天中,我们将会运用 MVC, JQuery 和 Ajax 来创建一个 Single Page 应用。这将会更加有趣,并富有挑战。
保持学习的热情吧!