65.9K
CodeProject 正在变化。 阅读更多。
Home

7 天学习 MVC 项目 - 第 6 天

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (80投票s)

2015 年 6 月 18 日

CPOL

24分钟阅读

viewsIcon

242952

downloadIcon

23271

本文是《7 天学会 MVC 项目》系列第 6 天的延续。

引言

欢迎来到“7 天学会 MVC 项目”系列第 6 天。希望您在阅读第 1 天到第 5 天时度过了愉快的时光。在学习第 6 天之前,必须完成前几天。

完整系列

我们很高兴地宣布,本文现已提供纸质版书籍,您可以从 www.amazon.comwww.flipkart.com 购买。

议程

实验 27 – 添加批量上传选项
关于实验 27 的讨论
上述解决方案中的问题
解决方案
实验 28 – 解决线程饥饿问题
实验 29 – 异常处理 – 显示自定义错误页面
关于实验 29 的讨论
了解上述实验的局限性
实验 30 – 异常处理 – 记录异常
关于实验 30 的讨论
路由
了解 RouteTable
了解 ASP.NET MVC 请求周期
实验 31 – 实现用户友好的 URL
关于实验 31 的讨论
结论

实验 27 – 添加批量上传选项

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

我们将在这里学习两个新事物。

  1. 如何使用文件上传控件
  2. 异步控制器。

步骤 1 – 创建 FileUploadViewModel

在 ViewModels 文件夹中创建一个名为 FileUploadViewModel 的新类,如下所示。

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

HttpPostedFileBase 将提供对客户端上传文件的访问。

步骤 2 - 创建 BulkUploadController 和 Index Action

创建一个名为 BulkUploadController 的新控制器和一个名为 Index Action 的操作方法,如下所示。

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

正如您所看到的,Index action 附带有 HeaderFooterFilter 和 AdminFilter 属性。HeaderFooterFilter 确保将正确的页眉和页脚数据传递给 ViewModel,AdminFilter 限制非管理员用户对操作方法的访问。

步骤 3 – 创建上传视图

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

请注意,视图名称应为 index.cshtml,并应放置在“~/Views/BulkUpload”文件夹中。

步骤 4 – 设计上传视图

将以下内容放入视图中。

@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 中的属性名称与输入 [type="file"] 的名称相同。它是“fileUpload”。我们在模型绑定器实验中谈到了名称属性的重要性。注意:在表单标签中指定了一个额外的属性,即 enctype。我们将在实验结束时讨论它。

步骤 5 - 创建业务层上传方法

在 EmployeeBusinessLayer 中创建一个名为 UploadEmployees 的新方法,如下所示。

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

步骤 6 – 创建上传操作方法

在 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 Action 的 AdminFilter 限制非管理员用户访问。

步骤 7 – 为批量上传创建链接

打开“Views/Employee”文件夹中的 AddNewLink.cshtml,并为批量上传创建一个链接,如下所示。

<a href="/Employee/AddNew">Add New</a>
&nbsp;
&nbsp;
<a href="/BulkUpload/Index">BulkUpload</a>

步骤 8 – 执行和测试

步骤 8.1 – 创建一个用于测试的示例文件

创建如下所示的示例文件并将其保存在计算机的某个位置。

步骤 8.2 – 执行和测试

按 F5 并执行应用程序。完成登录过程并通过单击链接导航到批量上传选项。

选择文件并单击上传。

注意

在上面的示例中,我们没有在视图中应用任何客户端或服务器端验证。这可能会导致以下错误。
“一个或多个实体验证失败。有关更多详细信息,请参阅 'EntityValidationErrors' 属性。”

要查找错误的精确原因,只需在发生异常时添加一个带有以下监视表达式的监视。
((System.Data.Entity.Validation.DbEntityValidationException)$exception).EntityValidationErrors

监视表达式“$exception”显示当前上下文中抛出的任何异常,即使它尚未被捕获并分配给变量。

关于实验 27 的讨论

为什么我们这里没有验证?

为此选项添加客户端和服务器端验证将作为读者的一个任务。我给你一个提示。

  • 对于服务器端验证,请使用数据注释。
  • 对于客户端,您可以利用数据注释并实现 jQuery 非侵入式验证。显然,这次您必须手动设置自定义数据属性,因为我们没有现成的 Htmlhelper 方法用于文件输入。
    注意:如果您不理解这一点,我建议您再次阅读“在登录视图中植入客户端验证”。
  • 对于客户端验证,您可以编写自定义 JavaScript 并在按钮单击时调用它。这不会太困难,因为文件输入最终是一个输入控件,其值可以在 JavaScript 中检索并进行验证。

什么是 HttpPostedFileBase?

HttpPostedFileBase 将提供对客户端上传文件的访问。模型绑定器将在 post 请求期间更新 FileUploadViewModel 类所有属性的值。目前,FileUploadViewModel 中只有一个属性,模型绑定器会将其设置为客户端上传的文件。

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

是的,我们可以通过两种方式实现。

  1. 创建多个文件输入控件。每个控件必须具有唯一的名称。现在在 FileUploadViewModel 类中,为每个控件创建一个 HttpPostedFileBase 类型的属性。每个属性名称应与一个控件的名称匹配。其余的魔法将由 ModelBinder 完成。☺
  2. 创建多个文件输入控件。每个控件必须具有相同的名称。现在,不是创建多个 HttpPostedFileBase 类型的属性,而是创建一个 List 类型的属性。.

注意:上述情况适用于所有控件。当您有多个同名控件时,如果属性是简单参数,ModelBinder 会用第一个控件的值更新属性。如果属性是列表属性,ModelBinder 会将每个控件的值放入列表中。

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" 属性添加到 form 标签时,将向服务器发送以下 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 定义的边界分隔,每个部分包含一个值。

如果 form 标签包含文件输入控件,则 encType 必须设置为“multipart/form-data”。

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

为什么我们不总是将 encType 设置为“multipart/form-data”?

当 encType 设置为“multipart/form-data”时,它将完成两件事——发布数据和上传文件。那么为什么我们不总是将其设置为“multipart/form-data”呢?

答案是,它还会增加请求的整体大小。请求大小越大意味着性能越差。因此,作为最佳实践,我们应该将其设置为默认值,即“application/x-www-form-urlencoded”。

为什么我们在这里创建了 ViewModel?

我们的视图中只有一个控件。我们可以通过直接在 Upload 操作方法中添加一个名为 fileUpload 的 HttpPostedFileBase 类型参数来达到相同的结果,而不是创建一个单独的 ViewModel。请看以下代码。

public ActionResult Upload(HttpPostedFileBase fileUpload)
{
}

那么为什么我们创建了一个单独的类呢?

创建 ViewModel 是一种最佳实践。控制器应始终以 ViewModel 的形式向视图发送数据,并且从视图发送的数据应以 ViewModel 的形式发送到控制器。

上述解决方案中的问题

你有没有想过当你发送请求时你是如何收到响应的?
现在不要说,动作方法接收请求等等等等!!!☺

虽然这是我预期的正确答案,但我期待一个稍微不同的答案。
我的问题是最初发生了什么。

一个简单的编程规则——程序中的所有内容都由线程执行,甚至一个请求也是如此。

在 Asp.net 的 Web 服务器上,.net 框架维护一个线程池。
每次向 Web 服务器发送请求时,都会从池中分配一个空闲线程来处理请求。此线程将被称为工作线程。

工作线程在请求处理期间将被阻塞,无法处理另一个请求。

现在假设一个应用程序接收到太多请求,并且每个请求都需要很长时间才能完全处理。在这种情况下,我们可能会遇到这样的情况:新请求将进入没有可用工作线程来处理该请求的状态。这称为线程饥饿。

在我们的示例中,示例文件有 2 条员工记录,但在实际情况中,它可能包含数千甚至数百万条记录。这意味着请求将花费大量时间来完成处理。这可能会导致线程饥饿。

解决方案

到目前为止我们讨论的请求是同步请求类型。

如果客户端发出异步请求而不是同步请求,则线程饥饿问题将得到解决。

  • 在异步请求的情况下,通常会从线程池中分配工作线程来处理请求。
  • 工作线程启动异步操作并返回到线程池以处理另一个请求。异步操作现在将由 CLR 线程继续。
  • 现在问题是,CLR 线程无法返回响应,因此一旦完成异步操作,它就会通知 ASP.NET。
  • Web 服务器再次从线程池中获取一个工作线程,处理剩余的请求并呈现响应。

在此整个场景中,工作线程两次从线程池中检索。现在它们可能相同,也可能不同。

现在在我们的示例中,文件读取是 I/O 密集型操作,不需要由工作线程处理。因此,这是将同步请求转换为异步请求的最佳位置。

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

不,响应时间将保持不变。在这里,线程将被释放以处理其他请求。

实验 28 – 解决线程饥饿问题

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

步骤 1 - 创建异步控制器

将 UploadController 的基类从 Controller 更改为 AsynController。

{
    public class BulkUploadController : AsyncController
    {

步骤 2 – 将同步动作方法转换为异步动作方法

这可以通过两个关键字轻松完成——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 存储在一个变量中。

让我们理解代码。

  • 当客户端点击上传按钮时,新的请求将被发送到服务器。
  • Web 服务器将从线程池中获取一个工作线程并分配它来处理请求。
  • 工作线程使动作方法执行。
  • 工作方法借助 Task.Factory.StartNew 方法启动一个异步操作。
  • 正如您所看到的,动作方法在 async 关键字的帮助下被标记为异步。它将确保工作线程在异步操作开始后立即释放。现在,从逻辑上讲,异步操作将由一个单独的 CLR 线程在后台继续执行。
  • 现在异步操作调用被标记为 await 关键字。它将确保在异步操作完成之前不会执行下一行。
  • 一旦异步操作完成,动作方法中的下一条语句应该执行,为此再次需要一个工作线程。因此,Web 服务器将简单地从线程池中取出一个新的空闲工作线程,并将其分配给处理剩余的请求并呈现响应。

步骤 3 – 执行和测试

执行应用程序。导航到批量上传选项。

现在在输出中做任何事情之前,导航到代码并在最后一行设置一个断点。

现在选择示例文件并单击上传。

正如您所看到的,我们开始时的线程 ID 与结束时的不同。输出将与上一个实验相同。

实验 29 – 异常处理 – 显示自定义错误页面

一个项目如果没有适当的异常处理,就不会被认为是一个完整的项目。

到目前为止,我们已经讨论了 Asp.Net MVC 中的两个过滤器——动作过滤器和授权过滤器。现在是第三个——异常过滤器。

什么是异常过滤器?

异常过滤器将以与其他过滤器相同的方式使用。我们将它们用作属性。

使用异常过滤器的步骤

  • 启用它们。
  • 将它们作为属性应用于动作方法或控制器。我们也可以在全局级别应用异常过滤器。

它们会做什么?

一旦动作方法中发生异常,异常过滤器将控制执行并自动开始执行其中编写的代码。

有自动化吗?

ASP.NET MVC 为我们提供了一个现成的异常过滤器,名为 HandeError。

如前所述,它会在动作方法中发生异常时执行。此过滤器将在“~/Views/[当前控制器]”或“~/Views/Shared”文件夹中查找名为“Error”的视图,创建该视图的 ViewResult 并将其作为响应返回。

让我们看一个演示,更好地理解它。

在上一个实验中,我们在项目中实现了批量上传选项。现在,输入文件中出现错误的可能性很高。

步骤 1 – 创建一个带有错误的示例上传文件

像以前一样创建一个示例上传文件,但这次放入一些无效值。

如您所见,工资无效。

步骤 2 – 执行并测试异常

按 F5 并执行应用程序。导航到批量上传选项。选择上述文件并单击上传。

步骤 3 – 启用异常过滤器

当启用自定义异常时,异常过滤器会被启用。要启用自定义异常,请打开 web.config 文件并导航到 System.Web 部分。添加一个新的自定义错误部分,如下所示。

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

步骤 4 – 创建错误视图

在“~/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>

步骤 5 – 附加异常过滤器

正如我们之前讨论的,在启用异常过滤器后,我们将其附加到动作方法或控制器。

好消息。☺ 不需要手动附加它。

打开 App_Start 文件夹中的 FilterConfig.cs 文件。在 RegisterGlobalFilters 方法中,您会看到 HandleError 过滤器已在全局级别附加。

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

如果需要,可以移除全局过滤器并将其附加到动作或控制器级别,如下所示。

[AdminFilter]
[HandleError]
public async Task<ActionResult> Upload(FileUploadViewModel model)
{

不建议这样做。最好在全局级别应用。

步骤 6 – 执行和测试

让我们以与之前相同的方式测试应用程序。

步骤 7 – 在视图中显示错误消息

为了实现这一点,将 Error 视图转换为 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>

步骤 8 – 执行和测试

执行相同的测试,但这次我们将得到以下错误视图。

我们是不是漏掉了什么?

Handle error 属性确保每当动作方法中发生异常时都会显示自定义视图。但是它的权限仅限于控制器和动作方法。它不会处理“资源未找到”错误。

执行应用程序并在 URL 中输入一些奇怪的内容

步骤 9 – 如下创建 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 构造函数接受 3 个参数 – 异常对象、控制器名称和动作方法名称。

步骤 10 – 在无效 URL 上显示自定义错误视图

在 web.config 中定义“资源未找到错误”的设置,如下所示。

<system.web>
    <customErrors mode="On">
      <error statusCode="404" redirect="~/Error/Index"/>
    </customErrors>

步骤 11 - 使 ErrorController 对所有人可访问

将 AllowAnonymous 属性应用于 ErrorController,因为错误控制器和索引动作不应绑定到经过身份验证的用户。用户可能在登录之前输入了无效的 URL。

[AllowAnonymous]
public class ErrorController : Controller
{

步骤 12 - 执行并测试

执行应用程序并在地址栏中输入一些无效 URL。

关于实验 29 的讨论

是否可以更改视图名称?

是的,不要求始终将视图名称保持为“Error”。

在这种情况下,我们必须在附加 HandlError 过滤器时指定视图名称。

[HandleError(View="MyError")]
Or
filters.Add(new HandleErrorAttribute()
                {
                    View="MyError"
                });

是否可以针对不同的异常获取不同的错误视图?

是的,这是可能的。在这种情况下,我们必须多次应用错误处理过滤器。

[HandleError(View="DivideError",ExceptionType=typeof(DivideByZeroException))]
[HandleError(View = "NotFiniteError", ExceptionType = typeof(NotFiniteNumberException))]
[HandleError]

OR

filters.Add(new HandleErrorAttribute()
    {
        ExceptionType = typeof(DivideByZeroException),
        View = "DivideError"
    });
filters.Add(new HandleErrorAttribute()
{
    ExceptionType = typeof(NotFiniteNumberException),
    View = "NotFiniteError"
});
filters.Add(new HandleErrorAttribute());

在上述情况下,我们三次添加了 Handle Error 过滤器。前两个是针对特定异常的,而最后一个更通用,它将显示所有其他异常的错误视图。

了解上述实验的局限性

上述实验的唯一限制是我们没有在任何地方记录异常。

实验 30 – 异常处理 – 记录异常

步骤 1 – 创建 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
                });
        }
    }
}

步骤 2 – 创建 EmployeeExceptionFilter 类

在 Filters 文件夹中创建一个名为 EmployeeExceptionFilter 的新类,如下所示。

namespace WebApplication1.Filters
{
    public class EmployeeExceptionFilter
    {
    }
}

步骤 3 - 扩展 Handle Error 以实现日志记录

从 HandleErrorAttribute 类继承 EmployeeExceptionFilter 并重写 OnException 方法,如下所示。

public class EmployeeExceptionFilter:HandleErrorAttribute
{
    public override void OnException(ExceptionContext filterContext)
    {
        base.OnException(filterContext);
    }
}

注意:请确保在顶部放置 using System.Web.MVC。HandleErrorAttribute 类存在于此命名空间中。

步骤 4 – 定义 OnException 方法

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

public override void OnException(ExceptionContext filterContext)
{
    FileLogger logger = new FileLogger();
    logger.LogException(filterContext.Exception);
    base.OnException(filterContext);
}

步骤 5 – 更改默认异常过滤器

打开 FilterConfig.cs 文件,移除 HandErrorAtrribute 并附加我们在上一步中创建的过滤器,如下所示。

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

步骤 6 – 执行和测试

首先在 C 盘创建一个名为“Error”的文件夹,因为错误文件将放置在该位置。

注意:如果需要,请将路径更改为您想要的路径。

按 F5 并执行应用程序。导航到批量上传选项。选择上述文件并单击上传。

这次输出不会有所不同。我们将获得与之前相同的错误视图。唯一的区别是这次我们还会在“C:\\Errors”文件夹中找到一个错误文件。

关于实验 30 的讨论

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

在上述实验中,我们重写了 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

您可以点击这里阅读更多关于 ASP.NET MVC 中的异常处理。

路由

到目前为止,我们已经讨论了许多概念,在 MVC 中回答了许多问题,除了一个基本且重要的问题。

“当最终用户发出请求时,究竟会发生什么?”

嗯,答案肯定是“动作方法执行”。但我的确切问题是,如何为一个特定的 URL 请求识别控制器和动作方法。

在我们开始实验“实现用户友好 URL”之前,首先让我们找出上述问题的答案。您可能想知道为什么这个话题会在最后出现。我故意将这个话题放在接近结尾的地方,因为我希望人们在理解内部原理之前就很好地了解 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 方法中,已经借助 routes.MapRoute 方法定义了一个默认路由。

RegisterRoutes 方法中定义的路由将在 Asp.Net MVC 请求周期后期用于确定要执行的确切控制器和动作方法。

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

MapRoute 函数还将 RouteHandler 附加到路由对象,在 ASP.NET MVC 中,它默认是 MVCRouteHandler。

了解 ASP.NET MVC 请求周期

在您开始之前,请允许我明确一点,我们不会在这里解释 100% 的请求周期。我们只会涉及重要的部分。

步骤 1 – UrlRoutingModule

当最终用户发出请求时,它首先通过 UrlRoutingModule 对象。UrlRoutingModule 是一个 HTTP 模块。

步骤 2 – 路由

UrlRoutingModule 将从路由表集合中获取第一个匹配的 Route 对象。现在为了匹配,请求 URL 将与路由中定义的 URL 模式进行比较。

匹配时将考虑以下规则。

  • 请求 URL(域名除外)和路由中定义的 URL 模式中的参数数量
    示例
  • URL 模式中定义的可选参数
    示例
  • 参数中定义的静态参数

步骤 3 – 创建 MVC 路由处理器

一旦选择了 Route 对象,UrlRoutingModule 将从 Route 对象获取 MvcRouteHandler 对象。

步骤 4 – 创建 RouteData 和 RequestContext

UrlRoutingModule 对象将使用 Route 对象创建 RouteData,然后使用它创建 RequestContext。

RouteData 封装了关于路由的信息,例如控制器名称、动作名称和路由参数值。

控制器名称

为了从请求 URL 中获取控制器名称,遵循以下简单规则:“在 URL 模式中,{controller} 是识别控制器名称的关键字”。

示例

  • 当 URL 模式为 {controller}/{action}/{id} 且请求 URL 为“https://:8870/BulkUpload/Upload/5”时,BulkUpload 将是控制器名称。
  • 当 URL 模式为 {action}/{controller}/{id} 且请求 URL 为“https://:8870/BulkUpload/Upload/5”时,Upload 将是控制器名称。

动作方法名称

为了从请求 URL 中获取动作方法名称,遵循以下简单规则:“在 URL 模式中,{action} 是识别动作方法名称的关键字”。

示例

  • 当 URL 模式为 {controller}/{action}/{id} 且请求 URL 为“https://:8870/BulkUpload/Upload/5”时,Upload 将是动作方法名称。
  • 当 URL 模式为 {action}/{controller}/{id} 且请求 URL 为“https://:8870/BulkUpload/Upload/5”时,BulkUpload 将是动作方法名称。

路由参数

基本上,URL 模式可以包含以下四项内容

  1. {controller} -> 标识控制器名称
  2. {action} -> 标识动作方法名称。
  3. SomeString -> 示例 – “MyCompany/{controller}/{action}” -> 在此模式中,“MyCompany”成为强制性字符串。
  4. {Something} -> 示例 – “{controller}/{action}/{id}” -> 在此模式中,“id”是路由参数。路由参数可在请求时用于获取 URL 中的值。

请看以下示例。

路由模式 - > “{controller}/{action}/{id}”

请求 URL ->https://:8870/BulkUpload/Upload/5

测试 1

public class BulkUploadController : Controller
{
    public ActionResult Upload (string id)
    {
       //value of id will be 5 -> string 5
       ...
    }
}

测试 2

public class BulkUploadController : Controller
{
    public ActionResult Upload (int id)
    {
       //value of id will be 5 -> int 5
       ...
    }
}

测试 3

public class BulkUploadController : Controller
{
    public ActionResult Upload (string MyId)
    {
       //value of MyId will be null
       ...
    }
}

步骤 5 – 创建 MVCHandler

MvcRouteHandler 将创建 MVCHandler 的实例,并传递 RequestContext 对象。

步骤 6 – 创建控制器实例

MVCHandler 将在 ControllerFactory 的帮助下创建 Controller 实例(默认情况下是 DefaultControllerFactory)。

步骤 7 – 执行方法

MVCHandler 将调用 Controller 的 execute 方法。execute 方法在控制器基类中定义。

步骤 8 – 调用动作方法

每个控制器都关联一个 ControllerActionInvoker 对象。在 execute 方法内部,ControllerActionInvoker 对象调用正确的动作方法。

步骤 9 – 执行结果。

动作方法接收用户输入并准备适当的响应数据,然后通过返回一个返回类型来执行结果。现在该返回类型可能是 ViewResult,可能是 RedirectToRoute Result,也可能是其他类型。

现在我相信您已经很好地理解了路由的概念,因此让我们借助路由使我们的项目 URL 更加用户友好。

实验 31 – 实现用户友好的 URL

步骤 1 – 重新定义 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 }
    );
}

正如您所看到的,现在我们定义了不止一个路由。

(默认路由保持不变。)

步骤 2 – 更改 URL 引用

打开“~/Views/Employee”文件夹中的 AddNewLink.cshtml 并更改批量上传链接,如下所示。

&nbsp;
<a href="/Employee/BulkUpload">BulkUpload</a>

步骤 3 – 执行和测试

执行应用程序并查看奇迹。

正如你所看到的,URL 不再是“控制器/动作”的形式。相反,它更加用户友好,但输出是相同的。

我建议您定义更多路由并尝试更多 URL。

关于实验 31 的讨论

以前的 URL 现在还能用吗?

是的,以前的 URL 也能用。

现在 BulkUploadController 中的 Index 动作可以通过两个 URL 访问

  1. “https://:8870/Employee/BulkUpload”
  2. “https://:8870/BulkUpload/Index”

默认路由中的“id”是什么?

我们已经讨论过了。它被称为路由参数。它可以用于通过 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”发出请求时,他/她将收到“资源未找到”错误。

动作方法中的参数名称是否必须与路由参数名称相同?

基本上,单个路由模式可能包含一个或多个路由参数。为了独立识别每个路由参数,动作方法中的参数名称必须与路由参数名称相同。

定义自定义路由时的顺序有关系吗?

是的,有关系。如果您还记得,UrlRoutingModule 将采用第一个匹配的路由对象。

在上述实验中,我们定义了两个路由。一个自定义路由和一个默认路由。现在假设默认路由首先定义,自定义路由其次定义。

在这种情况下,当最终用户使用 URL“http://.../Employee/BulkUpload”发出请求时,在比较阶段,UrlRoutingModule 会发现请求的 URL 与默认路由模式匹配,并会将“Employee”视为控制器名称,“BulkUpload”视为动作方法名称。

因此,定义路由时的顺序非常重要。最通用的路由应放在最后。

有没有更简单的方法来定义动作方法的 URL 模式?

我们可以使用基于属性的路由。

让我们试试看。

步骤 1 – 启用基于属性的路由。

在 RegisterRoutes 方法中,在 IgnoreRoute 语句之后保留以下行。

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

routes.MapMvcAttributeRoutes();

routes.MapRoute(
...

步骤 2 – 为动作方法定义路由模式

只需将 Route 属性附加到 EmployeeController 的 Index 动作方法,如下所示。

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

步骤 3 - 执行和测试

执行应用程序并完成登录过程。

正如您所看到的,我们得到了相同的输出,但使用了不同的更用户友好的 URL。

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

是的,请看以下语法。

[Route("Employee/List/{id}")]
publicActionResult Index (string id) { ... }

在这种情况下,约束如何?

这将更容易。

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

我们可以有以下约束

  1. {x:alpha} – 字符串验证
  2. {x:bool} – 布尔验证
  3. {x:datetime} – 日期时间验证
  4. {x:decimal} – 十进制验证
  5. {x:double} – 64 位浮点值验证
  6. {x:float} – 32 位浮点值验证
  7. {x:guid} – GUID 验证
  8. {x:length(6)} – 长度验证
  9. {x:length(1,20)} – 最小和最大长度验证
  10. {x:long} – 64 位整数验证
  11. {x:max(10)} – 最大整数验证
  12. {x:maxlength(10)} – 最大长度验证
  13. {x:min(10)} – 最小整数验证
  14. {x:minlength(10)} – 最小长度验证
  15. {x:range(10,50)} – 整数范围验证
  16. {x:regex(SomeRegularExpression)} – 正则表达式验证

RegisterRoutes 方法中的 IgnoreRoutes 是做什么的?

当我们不希望对某个特定扩展使用路由时,将使用 IgnoreRoutes。作为 MVC 模板的一部分,RegisterRoutes 方法中已经编写了以下语句。

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

这意味着如果最终用户使用“.axd”扩展发出请求,则不会有任何路由操作。请求将直接到达物理资源。

我们也可以定义自己的 IgnoreRoute 语句。

结论

通过第 6 天,我们已经完成了我们的 MVC 示例项目。希望您喜欢整个系列。

等等!!!第 7 天在哪里?

第 7 天会有的,我的朋友们。在第 7 天,我们将使用 MVC、jQuery 和 Ajax 创建一个单页应用程序。这将更加有趣和更具挑战性。敬请期待 ☺

您的评论、邮件总是激励我们做得更多。请在下方发表您的想法和评论或发送邮件至 SukeshMarla@Gmail.com

FacebookLinkedInTwitter 上关注我们,以获取最新版本。

如果您想从 MVC 5 开始,请从下面的视频“2 天学会 MVC 5”开始。

© . All rights reserved.