目录
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 访问。
http://localhost:8870/Employee/BulkUpload
http://localhost:8870/BulkUpload/Index
默认路由中的「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)} – 最小和最大长度认证
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 应用。这将会更加有趣,并富有挑战。
保持学习的热情吧!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。