本节又带了一些常用的,却很难理解的问题,本节从文件上传功能的实现引出了线程使用,介绍了线程饥饿的解决方法,异常处理方法,了解 RouteTable 自定义路径 。目录
实验 27——添加批量上传选项
关于实验 27
实验 27 存在的问题
解决方法
实验 28——解决线程饥饿问题
实验 29——异常处理—显示自定义错误页面
关于实验 29
理解实验 29 中的限制
实验 30—异常处理—日志异常
关于实验 30
理解 RouteTable
理解 Asp.net MVC 请求周期
实验 31—实现用户友好 URLs
关于实验 31
总结
实验 27——添加批量上传选项
在实验 27 中,我们将提供一个选项,供用户选择上传 Employee 记录文件(CSV 格式)。
我们会学习以下知识:
1. 如何使用文件上传控件
2. 异步控制器
1. 创建 FileUploadViewModel
在 ViewModels 文件夹下新建类 "FileUploadViewModel",如下:
- 1 : public class FileUploadViewModel: BaseViewModel
- 2 : {
- 3: public HttpPostedFileBase fileUpload {get; set ;}
- 4: }
HttpPostedFileBase 将通过客户端提供上传文件的访问入口。
2. 创建 BulkUploadController 和 Index action 方法
新建 controller"BulkUploadController",并实现 Index Action 方法,如下:
- 1 : public class BulkUploadController: Controller
- 2 : {
- 3 : [HeaderFooterFilter]
- 4 : [AdminFilter]
- 5 : public ActionResult Index()
- 6 : {
- 7 : return View(new FileUploadViewModel());
- 8: }
- 9: }
Index 方法与 HeaderFooterFilter 和 AdminFilter 属性绑定。HeaderFooterFilter 会确保页眉和页脚数据能够正确传递到 ViewModel 中,AdminFilter 限制非管理员用户的访问。
3. 创建上传 View
创建以上 Action 方法的 View。View 名称应为 index.cshtml,且存放在 "~/Views/BulkUpload" 文件夹下。
4. 设计上传 View
在 View 中输入以下内容:
- 1 : @using WebApplication1.ViewModels
- 2 : @model FileUploadViewModel
- 3 : @ {
- 4 : Layout = "~/Views/Shared/MyLayout.cshtml";
- 5: }
- 6 :
- 7 : @section TitleSection {
- 8 : Bulk Upload
- 9: }
- 10 : @section ContentBody {
- 11 : <div >
- 12 : <a href = "/Employee/Index" > Back < /a>/
- 13 : <form action = "/BulkUpload/Upload"method = "post"enctype = "multipart/form-data" >
- 14: Select File : <input type=
- "file" name="fileUpload" value="" />
- 15: <input type=
- "submit" name="name" value="Upload" />
- 16 : </form>/
- 17 : </div>/
- 18: }
如上,FileUploadViewModel 中属性名称与 input[type="file"] 的名称类似,都称为"fileUpload"。我们在 Model Binder 中已经讲述了名称属性的重要性,注意:在表单标签中,有一个额外的属性是加密的,会在实验结尾处讲解。
5. 创建业务层上传方法
在 EmployeeBusinessLayer 中新建方法 UploadEmployees,如下:
- 1 : public void UploadEmployees(List < Employee > employees)
- 2 : {
- 3 : SalesERPDAL salesDal = new SalesERPDAL();
- 4 : salesDal.Employees.AddRange(employees);
- 5 : salesDal.SaveChanges();
- 6: }<employee>
- 7 : </employee>/
6. 创建 Upload Action 方法
创建 Action 方法,并命名为 "BulkUploadController", 如下:
- 1 : [AdminFilter]
- 2 : public ActionResult Upload(FileUploadViewModel model)
- 3 : {
- 4 : List < Employee > employees = GetEmployees(model);
- 5 : EmployeeBusinessLayer bal = new EmployeeBusinessLayer();
- 6 : bal.UploadEmployees(employees);
- 7 : return RedirectToAction("Index", "Employee");
- 8: }
- 9 :
- 10 : private List < Employee > GetEmployees(FileUploadViewModel model)
- 11 : {
- 12 : List < Employee > employees = new List < Employee > ();
- 13 : StreamReader csvreader = new StreamReader(model.fileUpload.InputStream);
- 14: csvreader.ReadLine(); // Assuming first line is header
- 15 : while (!csvreader.EndOfStream)
- 16 : {
- 17 : var line = csvreader.ReadLine();
- 18: var values = line.Split(',');//Values are comma separated
- 19 : Employee e = new Employee();
- 20 : e.FirstName = values[0];
- 21 : e.LastName = values[1];
- 22 : e.Salary = int.Parse(values[2]);
- 23 : employees.Add(e);
- 24: }
- 25 : return employees;
- 26: }
AdminFilter 会绑定到 Upload action 方法中,限制非管理员用户的访问。
7. 创建 BulkUpload 链接
打开 "Views/Employee" 文件夹下的 AddNewLink.cshtml 文件,输入 BulkUpload 链接,如下:
- <a href="/Employee/AddNew">Add New</a>
- <
- a
- href
- ="/BulkUpload/Index"
- >BulkUpload</a>
8. 运行
8.1 创建一个样本文件来测试,如图所示
8.2 运行,点击 BulkUpload 链接
选择文件并点击确认
关于实验 27
为什么在实验 27 中不需要验证?
在该选项中添加客户端和服务器端验证需要读者自行添加的,以下是添加验证的提示:
- 服务器端验证可使用 Data Annotations。
- 客户端验证可利用客户端的数据解释和执行 jQuery 的验证。必须手动设置自定义数据属性,因为并没有将 Htmlhelper 方法设置为文件输入。
- 客户端验证可编写 JavaScript 代码,通过点击按钮来实现。这个方法并不是很难,由于文件输入是由输入控件完成,值可以在 JavaScript 中获取及验证 。
什么是 HttpPostedFileBase?
HttpPostedFileBase 将通过客户端提供文件上传的访问入口,Model Binder 会在 Post 请求期间更新 FileUploadViewModel 类中的所有属性值。我们在 FileUploadViewModel 内部只有一个属性,Model Binder 会通过客户端设置它实现文件上传。
是否会提供多文件的输入控件?
是,有两种方法可以实现:
1. 创建多文件输入控件,每个控件有唯一的名称,FileUploadViewModel 类会为每个控件创建 HttpPostedFileBase 类型的属性,每个属性名称应该与控件名称匹配。
2. 创建多文件输入控件,每个控件有相同的名称,创建类型的 List 列表,代替创建多个 HttpPostedFileBase 类型的属性。
enctype="multipart/form-data" 是用来做什么的?
该属性指定了 post 数据的编码类型,默认属性值是 "application/x-www-form-urlencoded"
例 1—登录窗体会给服务器发送以下 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
所有输入值会被作为发送的值的一部分,以 "key/value" 的形式发送。
当 enctype="multipart/form-data" 属性被加入 Form 标签中,以下 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 :
- 11 : Admin
- 12 : ------WebKitFormBoundary7hciuLuSNglCR8WC
- 13: Content-Disposition: form-data; name="Password"
- 14 :
- 15 : Admin
- 16 : ------WebKitFormBoundary7hciuLuSNglCR8WC
- 17: Content-Disposition: form-data; name="BtnSubmi"
- 18 :
- 19 : Login
- 20 : ------WebKitFormBoundary7hciuLuSNglCR8WC--
如上所示,Form 会在多部分 post 发送,每部分都是被分界线分割的,每部分包含单值。
如果 form 标签包含文件输入控件的话,enctype 必须被设置为 "multipart/form-data"。
为什么有时候需要设置 encType 为 "multipart/form-data",而有时候不需要设置?
当 encType 设置为 "multipart/form-data",将会实现 Post 数据和上传文件的功能,当然也会增加请求的 size 增加,请求 size 越大意味着性能越低。因此得出的最佳实践经验需要设置为默认的 "application/x-www-form-urlencoded"。
为什么在实验 27 中创建 ViewModel?
在 View 中已经有一个控件了,我们需要通过直接添加 HttpPostedFileBase 类型的参数,并命名为 "fileUpload" 实现相同的结果,从而替代创建独立的 ViewModel。
- 1 : public ActionResult Upload(HttpPostedFileBase fileUpload)
- 2 : {
- 3: }
创建 ViewModel 是最好的方法,Controller 应该以 ViewModel 的形式给 View 发送数据,且数据必须来自 Controller。
以上问题的解决方法
是否存在疑虑,当发送请求时,如何获取响应?
众人皆知的编程规则,程序中任何事件都是由线程执行的,请求事件也是。
Asp.net framework 维护线程池,每次当请求发送到 webserver 时,会从线程池中分配空闲的线程处理此请求。这种线程被称为 worker 线程。
当请求处理完成,该线程无法服务其他请求时,worker 线程会被阻塞。现在我们来了解什么是线程饥饿,如果一个应用程序接收到很多请求,且处理每个请求都非常耗时。在这种情况下,我们就必须指定一个点来结束请求,当有新的请求进入状态时,没有 worker 线程可使用,这种现象称为线程饥饿。
在我们的示例程序中只包含 2 个员工记录,而在实际使用情况下,会包含成千上万的记录,这就意味着将耗费大量的时间来处理请求。这种情况就可能导致线程饥饿.
线程饥饿的解决方法:
截至现在我们讨论的请求类型都是同步请求。如果使用异步请求来代替同步请求,那么线程饥饿的问题就得到解决了。
- 异步请求的情况下,会分配 worker 线程来服务请求。
- worker 线程初始化异步操作,并返回到线程池服务其他请求。异步操作可使用 CLR 线程来继续执行。
- 存在的问题就是,CLR 线程无法返回响应,一旦它完成了异步操作,它会通知 Asp.net。
- Webserver 再次获取一个 worker 线程来处理剩余的请求,并返回响应。
上述使用场景中,会获取两次 worker 线程,这两次获取的线程可能相同,也可能会不同。
文件读取是 I/O 操作,不需要使用 worker 线程处理。因此最好将同步请求转换为异步。
同步请求的响应时间能提升吗?
不可以,响应时间是相同的,线程会被释放来服务其他请求。
实验 28——解决线程饥饿问题
在 Asp.net MVC 中会通过将同步 Action 方法转换为异步 Action 方法,将同步请求转换为异步请求。
1. 创建异步控制器
在控制器中将基类 UploadController 修改为 AsynController。
- 1 : {
- 2 : public class BulkUploadController: AsyncController
- 3 : {
2. 转换同步 Action 方法
该功能通过两个关键字就可实现:"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 = new EmployeeBusinessLayer();
- 9 : bal.UploadEmployees(employees);
- 10 : return RedirectToAction("Index", "Employee");
- 11: }<actionresult><employee><list<employee>
- 12 : </list<employee></employee > </actionresult>/
在 action 方法的开始或结束处,使用变量存储线程 ID。
理一下思路:
- 当上传按钮被点击时,新请求会被发送到服务器。
- Webserver 从线程池中产生 Worker 线程 ,并分配给服务器请求。
- worker 线程会使 Action 方法执行
- Worker 方法在 Task.Factory.StartNew 方法的辅助下,开启异步操作
- 使用 async 关键字将 Action 方法标记为异步方法,由此会保证异步操作一旦开启,Worker 线程就会释放。
- 使用 await 关键字也可标记异步操作,能够保证异步操作完成时才能够继续执行下面的代码。
- 一旦异步操作在 Action 方法中完成执行,必须执行 worker 线程。因此 webserver 将会新建一个空闲 worker 线程,并用来服务剩下的请求,提供响应。
3. 测试运行
运行应用程序,并跳转到 BulkUpload 页面。会在代码中显示断点,输入样本文件,点击上传。
如图所示,在项目启动或关闭时有的线程 ID 是不同的。
实验 29——异常处理—显示自定义错误页面
如果一个项目不考虑异常处理,那么可以说这个项目是不完整的。到目前为止,我们已经了解了 MVC 中的两个过滤器:Action filter 和 Authorization filter。现在我们来学习第三个过滤器,异常过滤器(Exception Filters)。
什么是异常过滤器(Exception Filters)?
异常过滤器与其他过滤器的用法相同,可当作属性使用。使用异常过滤器的基本步骤:
1. 使它们可用
2. 将过滤器作为属性,应用到 action 方法或控制器中。我们也可以在全局层次使用异常过滤器。
异常过滤器的作用是什么?,是否有自动执行的异常过滤器?
一旦 action 方法中出现异常,异常过滤器就会控制程序的运行过程,开始内部自动写入运行的代码。MVC 为我们提供了编写好的异常过滤器:HandeError。
当 action 方法中发生异常时,过滤器就会在 "~/Views/[current controller]" 或 "~/Views/Shared" 目录下查找到名称为 "Error" 的 View,然后创建该 View 的 ViewResult,并作为响应返回。
接下来我们会讲解一个 Demo,帮助我们更好的理解异常过滤器的使用。
已经实现的上传文件功能,很有可能会发生输入文件格式错误。因此我们需要处理异常。
1. 创建含错误信息的样本文件,包含一些非法值,如图,Salary 就是非法值。
2. 运行,查找异常,点击上传按钮,选择已建立的样本数据,选择上传。
3. 激活异常过滤器
当自定义异常被捕获时,异常过滤器变为可用。为了能够获得自定义异常,打开 Web.config 文件,在 System.Web.Section 下方添加自定义错误信息。
- 1 : <system.web >
- 2 : <customErrors mode = "On" > </customErrors>/
4. 创建 Error View
在 "~/Views/Shared" 文件夹下,会发现存在 "Error.cshtml" 文件,该文件是由 MVC 模板提供的,如果没有自动创建,该文件也可以手动完成。
- 1 : @ {
- 2 : Layout = null;
- 3: }
- 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 : </body>/
- 17 : </html>/
5. 绑定异常过滤器
将过滤器绑定到 action 方法或 controller 上,不需要手动执行,打开 App_Start folder 文件夹中的 FilterConfig.cs 文件。在 RegisterGlobalFilters 方法中会看到 HandleError 过滤器已经以全局过滤器绑定成功。
- 1 : public static void RegisterGlobalFilters(GlobalFilterCollection filters)
- 2 : {
- 3: filters.Add(new HandleErrorAttribute());//ExceptionFilter
- 4 : filters.Add(new AuthorizeAttribute());
- 5: }
如果需要删除全局过滤器,那么会将过滤器绑定到 action 或 controller 层,但是不建议这么做,最好是在全局中应用如下:
- 1 : [AdminFilter]
- 2 : [HandleError]
- 3 : public async Task < ActionResult > Upload(FileUploadViewModel model)
- 4 : { < actionresult >
- 5 : </actionresult>/
6. 运行
7. 在 View 中显示错误信息
将 Error View 转换为 HandleErrorInfo 类的强类型 View,并在 View 中显示错误信息。
- 1 : @model HandleErrorInfo
- 2 : @ {
- 3 : Layout = null;
- 4: }
- 5 :
- 6 : <!DOCTYPE html >
- 7 : <html >
- 8 : <head >
- 9 : <meta name = "viewport"content = "width=device-width" / >
- 10 : <title > Error < /title>/
- 11 : </head>/
- 12 : <body >
- 13 : <hgroup >
- 14 : <h1 > Error. < /h1>/
- 15: <h2>An error occurred while processing your request.</h2>
- 16 : </hgroup>/
- 17: Error Message :@Model.Exception.Message<br />
- 18: Controller: @Model.ControllerName<br />
- 19 : Action: @Model.ActionName
- 20 : </body>/
- 21 : </html>/
8. 运行测试
Handle error 属性能够确保无论是否出现异常,自定义 View 都能够显示,但是它的能力在 controller 和 action 方法中是受限的。不会处理 "Resource not found" 这类型的错误。
运行应用程序,输一些奇怪的 URL
9. 创建 ErrorController 控制器,并创建 Index 方法,代码如下:
- 1 : public class ErrorController: Controller
- 2 : {
- 3: // GET: Error
- 4 : public ActionResult Index()
- 5 : {
- 6 : Exception e = new Exception("Invalid Controller or/and Action Name");
- 7 : HandleErrorInfo eInfo = new HandleErrorInfo(e, "Unknown", "Unknown");
- 8 : return View("Error", eInfo);
- 9: }
- 10: }
10. 在非法 URL 中显示自定义 Error 视图
可在 web.config 中定义 "Resource not found error" 的设置,如下:
- 1 : <system.web >
- 2:
- <
- customErrors mode="On">
- 3:
- <
- error
- statusCode
- ="404" redirect="~/Error/Index"/>
- 4 : </customErrors>/
11. 使 ErrorController 全局可访问。
将 AllowAnonymous 属性应用到 ErrorController 中,因为错误控制器和 index 方法不应该只绑定到认证用户,也很有可能用户在登录之前已经输入错误的 URL。
- 1 : [AllowAnonymous]
- 2 : public class ErrorController: Controller
- 3 : {
12. 运行
关于实验 29
View 的名称是否可以修改?
可以修改,不一定叫 Error,也可以指定其他名字。如果 Error View 的名称改变了,当绑定 HandleError 过滤器时,必须制定 View 的名称。
- 1 : [HandleError(View = "MyError")]
- 2 : Or
- 3 : filters.Add(new HandleErrorAttribute()
- 4 : {
- 5 : View = "MyError"
- 6: });
是否可以为不同的异常获取不同的 Error View?
可以,在这种情况下,必须多次应用 Handle error filter。
- 1 : [HandleError(View = "DivideError", ExceptionType = typeof(DivideByZeroException))]
- 2 : [HandleError(View = "NotFiniteError", ExceptionType = typeof(NotFiniteNumberException))]
- 3 : [HandleError]
- 4 :
- 5 : OR
- 6 :
- 7 : filters.Add(new HandleErrorAttribute()
- 8 : {
- 9: ExceptionType = typeof(DivideByZeroException),
- 10 : View = "DivideError"
- 11: });
- 12 : filters.Add(new HandleErrorAttribute()
- 13 : {
- 14: ExceptionType = typeof(NotFiniteNumberException),
- 15 : View = "NotFiniteError"
- 16: });
- 17 : filters.Add(new HandleErrorAttribute());
前两个 Handle error filter 都指定了异常,而最后一个更为常见更通用,会显示所有其他异常的 Error View。
上述实验中并没有处理登录异常,我们会在实验 30 中讲解登录异常。
实验 30——异常处理—登录异常
1. 创建 Logger 类
在根目录下,新建文件夹,命名为 Logger。在 Logger 文件夹下新建类 FileLogger
- 1 : namespace WebApplication1.Logger
- 2 : {
- 3 : public class FileLogger
- 4 : {
- 5 : public void LogException(Exception e)
- 6 : {
- 7 : File.WriteAllLines("C://Error//" + DateTime.Now.ToString("dd-MM-yyyy mm hh ss") + ".txt",
- 8 : new string[]
- 9 : {
- 10: "Message:"+e.Message,
- 11 : "Stacktrace:" + e.StackTrace
- 12: });
- 13: }
- 14: }
- 15: }
2. 创建 EmployeeExceptionFilter 类
在 Filters 文件夹下,新建 EmployeeExceptionFilter 类
- 1 : namespace WebApplication1.Filters
- 2 : {
- 3 : public class EmployeeExceptionFilter
- 4 : {
- 5: }
- 6: }
3. 扩展 Handle Error 实现登录异常处理
让 EmployeeExceptionFilter 继承 HandleErrorAttribute 类,重写 OnException 方法:
- 1 : public class EmployeeExceptionFilter: HandleErrorAttribute
- 2 : {
- 3 : public override void OnException(ExceptionContext filterContext)
- 4 : {
- 5 : base.OnException(filterContext);
- 6: }
- 7: }
4. 定义 OnException 方法
在 OnException 方法中包含异常登录代码。
- 1 : public override void OnException(ExceptionContext filterContext)
- 2 : {
- 3 : FileLogger logger = new FileLogger();
- 4 : logger.LogException(filterContext.Exception);
- 5 : base.OnException(filterContext);
- 6: }
5. 修改默认的异常过滤器
打开 FilterConfig.cs 文件,删除 HandErrorAtrribute,添加上步中创建的。
- 1 : public static void RegisterGlobalFilters(GlobalFilterCollection filters)
- 2 : {
- 3: //filters.Add(new HandleErrorAttribute());//ExceptionFilter
- 4 : filters.Add(new EmployeeExceptionFilter());
- 5 : filters.Add(new AuthorizeAttribute());
- 6: }
6. 运行
会在 C 盘中创建 "Error" 文件夹,存放一些 error 文件。
关于实验 30
当异常出现后,Error View 是如何返回响应的?
查看 OnException 方法的最后一行代码:
- 1 : base.OnException(filterContext);
即基类的 OnException 方法执行并返回 Error View 的 ViewResult。
在 OnException 中,是否可以返回其他结果?
可以,代码如下:
- 1 : public override void OnException(ExceptionContext filterContext)
- 2 : {
- 3 : FileLogger logger = new FileLogger();
- 4 : logger.LogException(filterContext.Exception);
- 5: //base.OnException(filterContext);
- 6 : filterContext.ExceptionHandled = true;
- 7 : filterContext.Result = new ContentResult()
- 8 : {
- 9 : Content = "Sorry for the Error"
- 10: };
- 11: }
当返回自定义响应时,做的第一件事情就是通知 MVC 引擎,手动处理异常,因此不需要执行默认的操作,不会显示默认的错误页面。使用以下语句可完成:
- 1 : filterContext.ExceptionHandled = true
Routing
到目前为止,我们已经解决了 MVC 的很多问题,但忽略了最基本的最重要的一个问题:当用户发送请求时,会发生什么?
最好的答案是 "执行 Action 方法",但仍存在疑问:对于一个特定的 URL 请求,如何确定控制器和 action 方法。在开始实验 31 之前,我们首先来解答上述问题,你可能会困惑为什么这个问题会放在最后来讲,因为了解内部结构之前,需要更好的了解 MVC。
理解 RouteTable
在 Asp.net mvc 中有 RouteTable 这个概念,是用来存储 URL 路径的,简而言之,是保存已定义的应用程序的可能的 URL pattern 的集合。
默认情况下,路径是项目模板组成的一部分。可在 Global.asax 文件中检查到,在 Application_Start 中会发现以下语句:
- 1 : RouteConfig.RegisterRoutes(RouteTable.Routes);
App_Start 文件夹下的 RouteConfig.cs 文件,包含以下代码块:
- 1 : namespace WebApplication1
- 2 : {
- 3 : public class RouteConfig
- 4 : {
- 5 : public static void RegisterRoutes(RouteCollection routes)
- 6 : {
- 7 : routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
- 8 :
- 9 : routes.MapRoute(
- 10: name: "Default",
- 11: url: "{controller}/{action}/{id}",
- 12: defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
- 13 : );
- 14: }
- 15: }
- 16: }
RegisterRoutes 方法已经包含了由 routes.MapRoute 方法定义的默认的路径。已定义的路径会在请求周期中确定执行的是正确的控制器和 action 方法。如果使用 route.MapRoute 创建了多个路径,那么内部路径的定义就意味着创建 Route 对象。
MapRoute 方法也可与 RouteHandler 关联。
理解 ASP.NET MVC 请求周期
在本节中我们只讲解请求周期中重要的知识点
1. UrlRoutingModule
当最终用户发送请求时,会通过 UrlRoutingModule 对象传递,UrlRoutingModule 是 HTTP 模块。
2. Routing
UrlRoutingModule 会从 route table 集合中获取首次匹配的 Route 对象,为了能够匹配成功,请求 URL 会与 route 中定义的 URL pattern 匹配。
当匹配的时候必须考虑以下规则:
- 数字参数的匹配(请求 URL 和 URL pattern 中的数字)
- URL pattern 中的可选参数:
- 参数中定义的静态参数
3. 创建 MVC Route Handler
一旦 Route 对象被选中,UrlRoutingModule 会获得 Route 对象的 MvcRouteHandler 对象。
4. 创建 RouteData 和 RequestContext
UrlRoutingModule 使用 Route 对象创建 RouteData,可用于创建 RequestContext。RouteData 封装了路径的信息如 Controller 名称,action 名称以及 route 参数值。
Controller 名称
为了从 URL 中获取 Controller 名称,需要按规则执行如在 URL pattern 中 {Controller} 是标识 Controller 名称的关键字。
Action Method 名称
为了获取 action 方法名称,{action} 是标识 action 方法的关键字。
Route 参数
URL pattern 能够获得以下值:
1.{controller}
2.{action}
3. 字符串,如 "MyCompany/{controller}/{action}","MyCompany" 是字符串。
4. 其他,如 "{controller}/{action}/{id}","id" 是路径的参数。
例如:
Route pattern - > "{controller}/{action}/{id}"
请求 URL ->http://localhost:8870/BulkUpload/Upload/5
测试 1
- 1 : public class BulkUploadController: Controller
- 2 : {
- 3 : public ActionResult Upload(string id)
- 4 : {
- 5: //value of id will be 5 -> string 5
- 6 : ...
- 7: }
- 8: }
测试 2
- 1 : public class BulkUploadController: Controller
- 2 : {
- 3 : public ActionResult Upload(int id)
- 4 : {
- 5: //value of id will be 5 -> int 5
- 6 : ...
- 7: }
- 8: }
测试 3
- 1 : public class BulkUploadController: Controller
- 2 : {
- 3 : public ActionResult Upload(string MyId)
- 4 : {
- 5: //value of MyId will be null
- 6 : ...
- 7: }
5. 创建 MVC Handler
- 8: }
MvcRouteHandler 会创建 MVCHandler 的实例传递 RequestContext 对象
6. 创建 Controller 实例
MVCHandler 会根据 ControllerFactory 的帮助创建 Controller 实例
7. 执行方法
MVCHandler 调用 Controller 的执行方法,执行方法是由 Controller 的基类定义的。
8. 调用 Action 方法
每个控制器都有与之关联的 ControllerActionInvoker 对象。在执行方法中 ControllerActionInvoker 对象调用正确的 action 方法。
9. 运行结果
Action 方法会接收到用户输入,并准备好响应数据,然后通过返回语句返回执行结果,返回类型可能是 ViewResult 或其他。
实验 31——实现对用户有好的 URL
1. 重新定义 RegisterRoutes 方法
在 RegisterRoutes 方法中包含 additional route
- 1 : public static void RegisterRoutes(RouteCollection routes)
- 2 : {
- 3 : routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
- 4 :
- 5 : routes.MapRoute(
- 6: name: "Upload",
- 7: url: "Employee/BulkUpload",
- 8: defaults: new { controller = "BulkUpload", action = "Index" }
- 9 : );
- 10 :
- 11 : routes.MapRoute(
- 12: name: "Default",
- 13: url: "{controller}/{action}/{id}",
- 14: defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
- 15 : );
- 16: }
2. 修改 URL 引用
打开 "~/Views/Employee" 文件下的 AddNewLink.cshtml ,修改 BulkUpload 链接,如下:
- 1 :
- 2 : <a href = "/Employee/BulkUpload" > BulkUpload < /a>/
3. 运行测试
关于实验 31
之前的 URL 现在是否起作用?
是,仍然有用。BulkUploadController 中的 Index 方法可通过两个 URL 访问。
1. "http://localhost:8870/Employee/BulkUpload"
2. "http://localhost:8870/BulkUpload/Index"
Route 参数和 Query 字符串有什么区别?
- Query 字符串本身是有大小限制的,而无法定义 Route 参数的个数。
- 无法在 Query 字符串值中添加限制,但是可以在 Route 参数中添加限制。
- 可能会设置 Route 参数的默认值,而 Query String 不可能有默认值。
- Query 字符串可使 URL 混乱,而 Route 参数可保持它有条理。
如何在 Route 参数中使用限制?
可使用正则表达式。
如:
- 1 : routes.MapRoute(
- 2: "MyRoute",
- 3: "Employee/{EmpId}",
- 4: new {controller=" Employee ", action="GetEmployeeById"},
- 5: new { EmpId = @"\d+" }
- 6 : );
Action 方法:
- 1 : public ActionResult GetEmployeeById(int EmpId)
- 2 : {
- 3 : ...
- 4: }
为了保证每个路径参数都能独立,因此参数名称必须与 Route Parameter 一致。
是否需要将 action 方法中的参数名称与 Route 参数名称保持一致?
Route Pattern 也许会包含一个或多个 RouteParameter,为了区分每个参数,必须保证 action 方法的参数名称与 Route 参数名称相同。
定义路径的顺序重要吗?
有影响,在上面的实验中,我们定义了两个路径,一个是自定义的,一个是默认的。默认的是最先定义的,自定义路径是在之后定义的。
当用户输入 "http://.../Employee/BulkUpload" 地址后发送请求,UrlRoutingModule 会搜索与请求 URL 匹配的默认的 route pattern ,它会将 Employee 作为控制器的名称,"BulkUpload" 作为 action 方法名称。因此定义的顺序是非常重要的,更常用的路径应放在最后。
是否有什么简便的方法来定义 Action 方法的 URL pattern?
我们可使用基于 routing 的属性。
1. 基本的 routing 属性可用
在 RegisterRoutes 方法中在 IgnoreRoute 语句后输入代码如下:
- 1 : routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
- 2 :
- 3 : routes.MapMvcAttributeRoutes();
- 4 :
- 5 : routes.MapRoute(
- 6 : ...
2. 定义 action 方法的 route pattern
- 1 : [Route("Employee/List")]
- 2 : public ActionResult Index()
- 3 : {
3. 运行测试
routing 属性可定义 route 参数,如下:
- 1 : [Route("Employee/List/{id}")]
- 2: publicActionResult Index (string id) { ... }
IgnoreRoutes 的作用是什么?
当我们不想使用 routing 作为特别的扩展时,会使用 IgnoreRoutes。作为 MVC 模板的一部分,在 RegisterRoute 方法中下列语句是默认的:
- 1 : routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
这就是说如果用户发送以 ".axd" 为结束的请求,将不会有任何路径加载的操作,请求将直接定位到物理资源。
总结
本节内容中讲述的线程问题是我们在 MVC 开发过程中经常遇到的,所以希望大家深入学习。同时在进行 MVC 开发时,还可以借助一些开发工具来帮助开发过程。 ComponentOne Studio ASP.NET MVC 是一款针对 MVC 平台的控件包,它与 Visual Studio 无缝集成,完全与 MVC6 和 ASP.NET 5.0 兼容,将大幅提高工作效率。
6 天的 MVC 学习已经完成了,希望大家能够将所讲的知识充分理解,充分吸收。第 7 章我们会使用 MVC,JQUery 和 Ajax 创建简单的页面应用。欢迎大家持续关注!