Skip to content

4. 操作的模板

让我们回到 ASP.NET MVC 应用程序的架构:

在上一章中,我们探讨了将请求 [1] 引导至控制器及负责处理该请求的操作 [2a] 的过程,这一机制被称为路由。 此外,我们还介绍了操作可能向浏览器返回的各种响应。目前,我们介绍的操作尚未利用提交给它们的请求。 请求 [1] 携带了多种信息,ASP.NET 和 MVC 会将这些信息以模板的形式呈现给操作。 请勿将此术语与视图 V [2c] 的模板 M 混淆,该视图由以下操作生成:

  • 客户端的请求 HTTP 到达 [1];
  • 在 [2] 中,请求中的信息将被转换为操作模板 [3](通常但并非必然是一个类),该模板将作为操作 [4] 的输入;
  • 在 [4] 中,该操作将基于此模型生成响应。该响应包含两个组成部分:视图 V [6] 及其对应的模型 M [5];
  • 视图 V [6] 将使用其模型 M [5] 来生成发给客户端的响应 HTTP。

MVC 模型中,操作 [4] 属于 C(控制器),视图 [5] 的模型是 M,而视图 [6] 是 V

本章探讨了请求所携带的信息(本质上是字符串)与操作模型(可能是一个具有多种类型属性的类)之间的关联机制。

4.1. 初始化操作参数

我们在现有解决方案中添加一个新项目 [1],该项目基于基础项目 ASP.NET 和 MVC:

  • 在 [2] 中,即新项目的名称;
  • 在 [3, 4] 中,我们选择以 ASP.NET 和 MVC 为基础的项目;
  • 在 [5] 中,新建项目。

我们将把新项目设为解决方案的启动项目。

如同第 3.1 节所述,我们创建一个名为 [First] [1] 的控制器:

  

在此控制器中,我们创建以下操作 [Action01]:


using System.Web.Mvc;

namespace Exemple_02.Controllers
{
  public class FirstController : Controller
  {
    // 操作01
    public ContentResult Action01(string nom)
    {
      return Content(string.Format("Contrôleur=First, Action=Action01, nom={0}", nom));
    }

  }
}

新内容出现在第 8 行:方法 [Action01] 有一个参数。本章将重点介绍初始化操作参数的各种方法。上文中的参数 [nom] 按顺序初始化为以下值:

Request.Form["nom"]
由命令 POST 发送的名为 [nom] 的参数
RouteData.Values["nom"]
一个名为 [nom] 的 URL 元素
Request.QueryString["nom"]
由命令 GET 发送的名为 [nom] 的参数
Request.Files["nom"]
一个名为 [nom] 的上传文件

让我们来分析这些不同的情况。直接在浏览器中请求 URL [/First/Action01?nom=someone]。得到以下响应:

 

浏览器发出的 HTTP 请求如下:

1
2
3
GET /First/Action01?nom=someone HTTP/1.1
Host: localhost:55483
...
  • 第 1 行:该请求为 GET。所请求的 URL 包含参数 [nom]。 在服务器端,该请求到达了 [Action01] 操作,其签名如下:

public ContentResult Action01(string nom)

为了给参数 nom 赋值,ASP.NET 会依次按顺序尝试 MVC 的值:Request.Form、["nom"]、 RouteData.Values["nom"]、Request.QueryString["nom"]、Request.Files["nom"]。 一旦找到一个值,它就会停止。 框架将 GET 中 URL 所包含的参数 [nom] 放置到了 Request.QueryString["nom"] [Action01]中的参数[nom]将使用该值[someone]进行初始化。随后,[Action01]中的代码将执行:


return Content(string.Format("Contrôleur=First, Action=Action01, nom={0}", nom), "text/plain", Encoding.UTF8);

此代码提供了发送给客户端的响应:

 

注意:参数绑定机制不区分大小写。因此,如果我们的操作定义为:


public ContentResult Action01(string NOM)

而传递的参数是 [?NoM=zébulon],绑定仍会成功。[NOM] 参数将从 [Action01] 接收值 [zébulon]。

现在,让我们使用 POST 调用相同的 URL。为此,我们使用应用程序 [Advanced Rest Client]:

  • 在 [1] 中,请求的是 URL;
  • 在 [2] 中,将使用 POST 的命令;
  • 在 [3] 中,将使用 POST 的参数。

发送此请求并查看 HTTP 的日志。HTTP 的请求内容如下:

  • 在 [1] 中,包含 POST;
  • 在 [2] 中,包含 POST 的参数。从技术上讲,这些参数是在标记这些头部结束的空行之后,跟在 HTTP 头部之后发送的;
  • 在 [3] 中,显示了获得的响应。确实成功获取了 POST 的参数 [nom]。 在对参数名 Request.Form["nom"]、RouteData.Values["nom"]、 Request.QueryString["nom"]、Request.Files["nom"],其中第一个值成功生效。

现在,我们来修改[App_Start/RouteConfig]中的默认路由。目前,该路由如下:


      routes.MapRoute(
          name: "Default",
          url: "{controller}/{action}/{id}",
          defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

将其更改为:


      routes.MapRoute(
          name: "Default",
          url: "{controller}/{action}/{nom}",
          defaults: new { controller = "Home", action = "Index", nom = UrlParameter.Optional }
);
  • 第3行,我们将路由的第三个元素命名为[nom];
  • 第 4 行,声明该元素为可选。

现在,重新编译应用程序,并在浏览器中直接请求 URL [/First/Action01/zébulon]。我们得到以下响应:

 

在尝试过的参数值中,如 Request.Form["nom"]、RouteData.Values["nom"]、 Request.QueryString["nom"]、Request.Files["nom"],其中第二个参数有效。

现在用 POST 和 [Advanced Rest Client] 执行相同的查询:

  • 在 [1] 中,我们为路由的 {nom} 元素赋予了值;
  • 在 [2] 中,我们在发送的请求中添加了参数 [nom];
  • 获得的响应为 [3]。

在对参数 [nom]Request.Form["nom"]RouteData.Values["nom"]、 Request.QueryString["nom"]、Request.Files["nom"]中,有两个有效,即前两个。最终采用了第一个。

4.2. 验证操作参数的有效性

如果某个操作有一个名为 [p] 的参数, ASP.NET MVC 将尝试为其赋值 Request.Form["p"]、 RouteData.Values["p"]、Request.QueryString["p"]、Request.Files["p"]中的任意一个值。 前三个值是字符串。如果参数 [p] 的类型不是 [string],可能会出现问题。

创建以下新操作:


    // 操作02
    public ContentResult Action02(int age)
    {
      string texte = string.Format("Contrôleur={0}, Action={1}, âge={2}", RouteData.Values["controller"], RouteData.Values["action"],age);
      return Content(texte, "text/plain", Encoding.UTF8);
}
  • 第 2 行,操作 [Action02] 接受一个名为 [age] 的 int 类型参数。因此,获取的字符串必须能够转换为 int 类型

调用 URL [http://localhost:55483/First/Action02?age=21]。得到以下页面:

 

请求 URL [http://localhost:55483/First/Action02?age=21x]。得到以下页面:

 

这次,我们收到了一页错误页面。值得关注的是,在此情况下服务器发送的 HTTP 请求头:

1
2
3
4
5
HTTP/1.1 500 Internal Server Error
...
Content-Type: text/html; charset=utf-8
...
Content-Length: 12438
  • 第 1 行:服务器返回了状态码 [500 Internal Server Error],并发送了一页大小为 12438 字节(第 5 行)的 HTML 页面(第 3 行),用于说明该错误的可能原因。

现在,让我们创建以下 [Action03] 操作:


    // 操作03
    public ContentResult Action03(int? age)
    {
      ...
}

[Action03] 与 [Action02] 完全相同,只是将参数 [age] 的类型改为 int?,即整数空值

现在请求 URL 和 [http://localhost:55483/First/Action03?age=21x]。我们将得到以下页面:

 

ASP.NET 无法将 [21x] 转换为 int 类型。因此,它将参数 [age] 赋值为 null,这符合其 int? 类型的要求。 不过,我们可以判断该参数是否从请求中接收到了值。

我们构建以下新的操作 [Action04]:


    // 操作04
    public ContentResult Action04(int? age)
    {
      bool valide = ModelState.IsValid;
      string texte = string.Format("Contrôleur={0}, Action={1}, âge={2}, valide={3}", RouteData.Values["controller"], RouteData.Values["action"], age, valide);
      return Content(texte, "text/plain", Encoding.UTF8);
}
  • 第2行:保留了类型[int?]。这使得请求无需提供参数[age],该参数将自动被赋予null值
  • 第4行:检查操作模型是否有效。操作模型由其所有参数组成,此处为[age]。若所有参数均能从请求中获取值(或当参数类型允许时获取null值),则模型有效;
  • 第 5 行:将变量 [valide] 的值添加到发送给客户端的文本中。

请求 URL [http://localhost:55483/First/Action04?age=21x]。得到以下页面:

 

ASP.NET MVC 未能将 [21x] 转换为 int 类型。因此,它将参数 [age] 赋值为 null,这符合其 int 类型的要求。 但如 [valide] 的值所示,确实发生了转换错误。

可能会出现与转换失败相关的错误信息。让我们来看一下以下新操作:


    // 操作05
    public ContentResult Action05(int? age)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("Contrôleur={0}, Action={1}, âge={2}, valide={3}, erreurs={4}", RouteData.Values["controller"], RouteData.Values["action"], age, ModelState.IsValid, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

新内容出现在第 4 行。该行调用了私有方法 [getErrorMessagesFor],并向其传递了操作模型的状态。该方法返回一个字符串,其中汇总了所有已发生错误的消息。该方法如下:


private string getErrorMessagesFor(ModelStateDictionary état)
    {
      List<String> erreurs = new List<String>();
      string messages = string.Empty;
      if (!état.IsValid)
      {
        foreach (ModelState modelState in état.Values)
        {
          foreach (ModelError error in modelState.Errors)
          {
            erreurs.Add(getErrorMessageFor(error));
          }
        }
        foreach (string message in erreurs)
        {
          messages += string.Format("[{0}]", message);
        }
      }
      return messages;
    }
  • 第 1 行:传递给该方法的实际参数 [ModelState] 的类型为 [ModelStateDictionary];
  • 第3行:一个错误消息列表,初始为空;
  • 第 5 行:检查作为参数传递的状态是否有效。若无效,则将所有错误消息聚合为一个字符串;
  • 第7行:类型[ModelStateDictionary]具有一个名为[Values]的属性,该属性是一个由[ModelState]类型组成的集合。模型中的每个元素对应一个[ModelState]。例如:
    • ModelState["age"]:参数 [age] 的操作模型状态,
    • ModelState["age"].Errors:该参数的错误集合。错误类型为 [ModelError],
    • ModelState["age"]。Errors[i].ErrorMessage:模板中参数 [age] 的第 i 条错误消息(如有)
    • ModelState["age"].Errors[i].Exception:参数 [age] 错误集合中第 i 号错误的异常,
    • ModelState["age"].Errors[i].Exception.InnerException:此异常的原因,
    • ModelState["age"].Errors[i].Exception.InnerException.Message:异常原因的消息;
  • 第 9 行:遍历某个特定 [ModelState] 的集合 [Errors];
  • 第 11 行:获取某个特定 [ModelError] 的错误消息,并将其添加到第 3 行的错误消息列表中;
  • 第14-17行:将错误消息列表中的元素合并为一个字符串。

第 11 行中的 [getErrorMessageFor] 方法如下:


    private string getErrorMessageFor(ModelError error)
    {
      if (error.ErrorMessage != null && error.ErrorMessage.Trim() != string.Empty)
      {
        return error.ErrorMessage;
      }
      if (error.Exception != null && error.Exception.InnerException == null && error.Exception.Message != string.Empty)
      {
        return error.Exception.Message;
      }
      if (error.Exception != null && error.Exception.InnerException != null && error.Exception.InnerException.Message != string.Empty)
      {
        return error.Exception.InnerException.Message;
      }
      return string.Empty;
}
  • 第 1 行:接收类型为 [ModelError] 的对象,该对象封装了操作模板中某个元素的错误。我们将从三个不同位置查找错误消息:
    • 在 [ModelError].ErrorMessage 的第 3-6 行;
    • 在 [ModelError].Exception.Message 的第 7-10 行;
    • 在 [ModelError].Exception.InnerException.Message 文件中,第 11-14 行;

在测试过程中,我们注意到根据模型元素的性质,错误消息会出现在这三个位置。肯定存在某种规则可以确保获取与模型元素关联的错误消息,但我并不了解该规则。因此,我按照一定的顺序在可能找到错误消息的各个位置进行搜索。一旦找到非空消息,便立即返回。

试着请求 URL [http://localhost:55483/First/Action05?age=21x]。得到以下页面:

 

4.3. 多参数操作

考虑以下新操作:


    // 操作06
    public ContentResult Action06(double? poids, int? age)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("Contrôleur={0}, Action={1}, poids={2}, âge={3}, valide={4}, erreurs={5}", RouteData.Values["controller"], RouteData.Values["action"], poids, age, ModelState.IsValid, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}
  • 第 2 行:我们有两个参数 [poids] 和 [age]。

前面描述的规则现在适用于这两个参数。以下是一些执行示例:

 
 

4.4. 使用类作为操作的模板

定义一个将作为操作模板的类。我们将它放在文件夹 [Models] [1] 中。

其代码如下:


namespace Exemple_02.Models
{
  public class ActionModel01
  {
    public double? Poids { get; set; }
    public int? Age { get; set; }
  }
}

该类具有两个自动属性,即之前讨论过的两个参数 [Poids] 和 [Age]。该类将作为操作 [Action07] 的输入参数:


    // 操作07
    public ContentResult Action07(ActionModel01 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("Contrôleur={0}, Action={1}, poids={2}, âge={3}, valide={4}, erreurs={5}", RouteData.Values["controller"], RouteData.Values["action"], modèle.Poids, modèle.Age, ModelState.IsValid, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}
  • 第 2 行:该操作的模板是 [ActionModel01] 类型的实例。

让我们再次使用前面的两个示例:

 
 

请注意,参数绑定不区分大小写。 请求参数为 [age] 和 [poids]。它们为类 [ModelAction01] 的属性 [Age] 和 [Poids] 提供了数据。

此外,到目前为止我们一直使用的是 HTTP 和 [GET] 查询。让我们证明 [POST] 查询具有相同的行为。为此,我们再次使用 [Advanced Rest Client] 应用程序:

  • 在 [1] 中,请求的 URL;
  • 在 [2] 中,将通过 POST 命令调用它;
  • 在[3]中,POST的参数。

得到的响应与 GET 相同:

Image

4.5. 带有效性约束的动作模型 - 1

使用前一个模型:


namespace Exemple_02.Models
{
  public class ActionModel01
  {
    public double? Poids { get; set; }
    public int? Age { get; set; }
  }
}

参数 [poids] 和 [age] 可以不在请求中。 在此情况下,属性 [Poids] 和 [Age] 将被赋予值 [null],且不会报错。可能需要将模型转换为如下形式:


namespace Exemple_02.Models
{
  public class ActionModel01
  {
    public double Poids { get; set; }
    public int Age { get; set; }
  }
}

在第 5 行和第 6 行,属性 [Poids] 和 [Age] 不能再取值 [null]。 让我们看看当查询中缺少参数 [poids] 和 [age] 时,该新模型会发生什么情况。

 

未发生任何错误,且属性 [Poids] 和 [Age] 保留了其初始化值:0。ASP.NET MVC:

  • 通过 new ActionModel01 创建了模型的实例。此时,属性 [Poids] 和 [Age] 被赋值为 0;
  • 未为这两个属性赋值,因为没有同名的参数。

第一个模型允许我们检查参数是否缺失:此时对应的属性值为 [null]。第二个模型则不允许这样做。除了简单的参数类型之外,还可以添加其他验证约束。接下来我们将介绍这些约束。

考虑以下新的操作模型:

  

using System.ComponentModel.DataAnnotations;
namespace Exemple_02.Models
{
  public class ActionModel02
  {
    [Required]
    [Range(1, 200)]
    public double? Poids { get; set; }
    [Required]
    [Range(1, 150)]
    public int? Age { get; set; }
  }
}
  • 第 6 行:表示字段 [Poids] 为必填项;
  • 第 7 行:表示字段 [Poids] 的值必须在 [1,200] 范围内;
  • 第 9 行:表示字段 [Age] 是必填的;
  • 第 7 行:表示字段 [Age] 必须位于 [1,150] 范围内;

使用此模板的操作将是以下操作 [Action08]:


    // 操作08
    public ContentResult Action08(ActionModel02 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("Contrôleur={0}, Action={1}, poids={2}, âge={3}, valide={4}, erreurs={5}", RouteData.Values["controller"], RouteData.Values["action"], modèle.Poids, modèle.Age, ModelState.IsValid, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}
  • 第 2 行:该操作接收 [ActionModel02] 模型的一个实例;

让我们进行一些测试:

 
 
 
 

错误已被成功检测到。现在,让我们按以下方式改进模型:


using System.ComponentModel.DataAnnotations;
namespace Exemple_02.Models
{
  public class ActionModel02
  {
    [Required]
    [Range(1, 200)]
    public double Poids { get; set; }
    [Required]
    [Range(1, 150)]
    public int Age { get; set; }
  }
}

第 8 行和第 11 行,属性不能再取值 [null]。编译并重新进行无参数测试:

 

由于未传入参数,属性 [Poids] 和 [Age] 保留了模型实例化时设定的值:0。随后进行验证。此时属性 [Required] 满足条件。 可以看出,上述错误消息是关于属性 [Range] 的。因此,要验证参数是否存在,相关属性必须是 nullable,即能够接收值 null

让我们回到最初的 [ActionModel02] 模型,并考虑一个由 [ActionModel02] 实例和 [DateTime] nullable 类型组成的操作:


    // 操作09
    public ContentResult Action09(ActionModel02 modèle, DateTime? date)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("Contrôleur={0}, Action={1}, poids={2}, âge={3}, date={4}, valide={5}, erreurs={6}", RouteData.Values["controller"], RouteData.Values["action"], modèle.Poids, modèle.Age, date, ModelState.IsValid, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

进行一些测试:

 

我们没有向该操作传递任何参数。 属性 [Required] 针对属性 [Poids] 和 [Age] 发挥了作用。日期则被赋予了值 null,且未报告任何错误。

现在我们传递无效的参数:

 

现在传递有效值:

 

让我们考察其他有效性约束。新的操作模式如下:

  

using System.ComponentModel.DataAnnotations;
namespace Exemple_02.Models
{
  public class ActionModel03
  {
    [Required(ErrorMessage = "Le paramètre email est requis")]
    [EmailAddress(ErrorMessage = "Le paramètre email n'a pas un format valide")]
    public string Email { get; set; }

    [Required(ErrorMessage = "Le paramètre jour est requis")]
    [RegularExpression(@"^\d{1,2}$", ErrorMessage = "Le paramètre jour doit avoir 1 ou 2 chiffres")]
    public string Jour { get; set; }

    [Required(ErrorMessage = "Le paramètre info1 est requis")]
    [MaxLength(4, ErrorMessage = "Le paramètre info1 ne peut avoir plus de 4 caractères")]
    public string Info1 { get; set; }

    [Required(ErrorMessage = "Le paramètre info2 est requis")]
    [MinLength(2, ErrorMessage = "Le paramètre info2 ne peut avoir moins de 2 caractères")]
    public string Info2 { get; set; }

    [Required(ErrorMessage = "Le paramètre info3 est requis")]
    [MinLength(4, ErrorMessage = "Le paramètre info3 doit avoir 4 caractères exactement")]
    [MaxLength(4, ErrorMessage = "Le paramètre info3 doit avoir 4 caractères exactement")]
    public string Info3 { get; set; }
  }
}
  • 第 6 行:[Required] 属性,这次我们自己设定了一个错误信息;
  • 第7行:属性[EMailAddress]要求字段[Email]包含格式正确的电子邮件地址;
  • 第 11 行:属性 [RegularExpression] 要求字段 [Jour] 包含一个或两个数字的字符串。第一个参数是该字段必须验证的正则表达式;
  • 第 15 行:属性 [MaxLength] 要求字段 [Info1] 的长度不超过 4 个字符;
  • 第 19 行:属性 [MinLength] 要求字段 [Info2] 至少包含 2 个字符;
  • 第23-24行:[MaxLength]和[MinLength]两个属性共同规定,字段[Info3]必须恰好包含4个字符;

操作 [Action10] 将使用此模板:


// 操作10
    public ContentResult Action10(ActionModel03 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("email={0}, jour={1}, info1={2}, info2={3}, info3={4}, erreurs={5}",
        modèle.Email, modèle.Jour, modèle.Info1, modèle.Info2, modèle.Info3, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
    }

让我们用此操作进行一些测试。

首先不带参数:

 

然后使用无效参数:

 

接着使用有效参数:

 

4.6. 带有效性约束的操作模型 - 2

我们介绍其他完整性约束。新的操作模型将是以下 [ActionModel04] 类:


using System.ComponentModel.DataAnnotations;

namespace Exemple_02.Models
{
  public class ActionModel04
  {
    [Required(ErrorMessage="Le paramètre url est requis")]
    [Url(ErrorMessage="URL invalide")]
    public string Url { get; set; }
    [Required(ErrorMessage = "Le paramètre info1 est requis")]
    public string Info1 { get; set; }
    [Required(ErrorMessage = "Le paramètre info2 est requis")]
    [Compare("Info1",ErrorMessage="Les paramètres info1 et info2 doivent être identiques")]
    public string Info2 { get; set; }
    [Required(ErrorMessage = "Le paramètre cc est requis")]
    [CreditCard(ErrorMessage = "Le paramètre cc n'est pas un n° de carte de crédit valide")]
    public string Cc { get; set; }
  }
}
  • 第 8 行:要求注释字段为有效的 URL;
  • 第13行:要求属性[Info1]和[Info2]具有相同的值;
  • 第 16 行:要求注释字段为有效的信用卡号。

使用此模板的操作如下:


    // 操作11
    public ContentResult Action11(ActionModel04 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("URL={0}, Info1={1}, Info2={2}, CC={3},erreurs={4}",
        modèle.Url, modèle.Info1, modèle.Info2, modèle.Cc, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

为了测试 [Action11] 操作,我们使用 [Advanced Rest Client] 应用程序:

  • 在 [1] 中,调用动作 [Action11] 的 URL;
  • 在 [2] 中,将要求提供 URL 以及 POST;
  • 在 [3] 中,选择 [Form] 选项卡;
  • 在 [4] 中,输入四个预期参数的值。此初始化功能由 [ARC] 提供。实际发送的参数可在 [Raw] 和 [5] 选项卡中查看;
  • 在 [6] 中,显示的是 POST 的参数。

针对此查询,收到以下响应:

现在输入无效的参数:

 

随后我们得到以下响应:

4.7. 带有效性约束的操作模型 - 3

有时现有的完整性约束条件并不足够。此时我们可以自行创建。特别是可以使用一个实现 [IValidatableObject] 接口的模型。在这种情况下,我们将自定义的模型验证逻辑添加到该接口的 [Validate] 方法中。让我们看一个示例。 该操作的新模型将是以下 [ActionModel05] 类:


using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace Exemple_02.Models
{
  public class ActionModel05 : IValidatableObject
  {
    [Required(ErrorMessage = "Le paramètre taux est requis")]
    public double? Taux { get; set; }
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
      List<ValidationResult> résultats = new List<ValidationResult>();
      bool ok = Taux < 4.2 || Taux > 6.7;
      if (!ok)
      {
        résultats.Add(new ValidationResult("Le paramètre taux doit être < 4.2 ou > 6.7", new string[] { "Taux" }));
      }
      return résultats;
    }
  }
}
  • 第 6 行:该模型实现了 [IValidatableObject] 接口;
  • 第 10 行:该接口的 [Validate] 方法。它返回一个 [ValidationResult] 类型的元素集合。该类型封装了需要报告的错误;
  • 第 9 行:有效的利率是小于 4.2 或大于 6.7 的数值;
  • 第 12 行:创建一个类型为 [ValidationResult] 的空元素列表;
  • 第 13 行:验证属性 [Taux] 的有效性;
  • 第14-17行:如果属性[Taux]无效,则在结果列表中添加一个类型为[ValidationResult]的元素。第一个参数是错误消息。第二个参数(可选)是与该错误相关的属性集合。

使用此模板的操作如下:


    // 操作12
    public ContentResult Action12(ActionModel05 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("taux={0}, erreurs={1}", modèle.Taux, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

以下是一个执行示例:

 

4.8. 表格或列表类型的操作模板

考虑以下操作 [Action13]:


// Action13
    public ContentResult Action13(string[] data)
    {
      string strData = "";
      if (data != null && data.Length != 0)
      {
        strData = string.Join(",", data);
      }
      string texte = string.Format("data=[{0}]", strData);
      return Content(texte, "text/plain", Encoding.UTF8);
    }
  • 第 2 行:该操作的模板由一个 [string] 数组构成。它允许我们获取一个名为 [data] 的参数,该参数可能在查询参数中多次出现,例如 [?data=data1&data=data2&data=data3]。 请求中的各个参数 [data] 将填充到操作模型的数组 [data] 中。这种情况常见于多选列表。此时,浏览器会发送用户所选的不同值,且这些值的参数名称相同。

以下是一个示例:

 

模型也可以是一个列表:


    // 操作14
    public ContentResult Action14(List<int> data)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string strData = "";
      if (data != null && data.Count != 0)
      {
        strData = string.Join(",", data);
      }
      string texte = string.Format("data=[{0}], erreurs=[{1}]", strData, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

此处的模板是一个整数列表(第 2 行)。以下是首次执行结果:

 

以及第二次运行结果:

 

4.9. 过滤操作模板

有时我们拥有一个模板,但希望仅由请求 HTTP 初始化模板中的某些元素。考虑以下操作模板 [ActionModel06]:


using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;

namespace Exemple_02.Models
{
  [Bind(Exclude = "Info2")]
  public class ActionModel06
  {
    [Required(ErrorMessage = "Le paramètre [info1] est requis")]
    public string Info1 { get; set; }

    public string Info2 { get; set; }
  }
}
  • 第 9-10 行:参数 [info1] 为必填项;
  • 第 6 行:第 12 行的参数 [info2] 被排除在请求 HTTP 与其模板的关联之外。

操作将如下所示 [Action15]:


    // 操作15
    public ContentResult Action15(ActionModel06 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("valide={0}, info1={1}, info2={2}, erreurs={3}", ModelState.IsValid, modèle.Info1, modèle.Info2, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

以下是一个执行示例:

  • 在 [1] 中:将参数 [info2] 传递给 URL;
  • 在 [2] 中:操作模型的 [Info2] 属性保持为空。

4.10. 扩展数据绑定模型

让我们回顾一下操作的执行架构:

操作类在客户端请求开始时被实例化,并在请求结束时被销毁。因此,即使该操作被反复调用,它也不能用于在两次请求之间存储数据。我们可能希望存储两种类型的数据:

  • 由 Web 应用程序所有用户共享的数据。这些通常是只读数据。有三个文件用于实现这种数据共享:
    • [Web.Config]:应用程序配置文件
    • [Global.asax, Global.asax.cs]:用于定义一个称为“应用程序全局类”的类,其生命周期与应用程序一致,并包含该应用程序某些事件的处理程序。

应用程序全局类用于定义可供所有用户的所有请求访问的数据。

  • 同一客户端的请求共享的数据。这些数据存储在一个名为Session的对象中。此时,我们称之为客户端会话,以指代客户端的内存。同一客户端的所有请求均可访问该会话,并在其中存储和读取信息。

上文展示了操作可访问的内存类型:

  • 应用程序内存,其中大多包含只读数据,且所有用户均可访问;
  • 特定用户的内存(即会话),其中包含可读写数据,且同一用户的后续请求均可访问;
  • 上图未显示的还有请求内存(或称请求上下文)。用户的请求可能由多个连续的操作处理。请求上下文使操作 1 能够向操作 2 传递信息。

让我们通过一个示例来了解这些不同的内存:

首先,我们按以下方式修改项目 [Exemple-02] 中的文件 [Web.config]:


  <appSettings>
    <add key="webpages:Version" value="2.0.0.0" />
    ...
    <add key="infoAppli1" value="infoAppli1"/>
</appSettings>

我们添加第 4 行,将键 [infoAppli1] 与值 [infoAppli1] 关联。这将成为我们的全局数据 [Application]:所有用户的所有查询均可访问该数据。

接下来,我们修改文件 [Global.asax] 中的方法 [Application_Start]。该方法在应用程序启动时仅执行一次。此时需要利用文件 [Web.config]:


    protected void Application_Start()
    {
      AreaRegistration.RegisterAllAreas();

      WebApiConfig.Register(GlobalConfiguration.Configuration);
      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      BundleConfig.RegisterBundles(BundleTable.Bundles);
      // 应用程序初始化
      Application["infoAppli1"] = ConfigurationManager.AppSettings["infoAppli1"];
}

我们添加第 10 行代码。它执行两项操作:

  • 通过类 [System.Configuration.ConfigurationManager] 从文件 [Web.config] 中获取键 [infoAppli1] 的值;
  • 将其存储在字典 [HttpApplication.Application] 中,并关联键 [infoAppli1]。所有操作均可访问该字典。

在同一文件 [Gloabal.asax] 中,添加以下方法 [Session_Start]:


    protected void Session_Start()
    {
      // 计数器初始化
      Session["compteur"] = 0;
}

对于所有新用户,都会执行方法 [Session_Start]。什么是新用户?用户通过会话令牌进行“跟踪”。该令牌由:

  • 由 Web 服务器创建,并在发给新用户的首次响应的 HTTP 标头中发送;
  • 在用户每次发出新请求时,由其浏览器重新发送。这使得服务器能够识别用户,并为其管理一个称为“用户会话”的内存空间。

当用户未发送会话令牌时,Web 服务器会识别其为新用户,并为其生成一个会话令牌。

在上面的第 4 行,我们在用户的会话中放置了一个计数器,该计数器将在该用户的每次请求时递增。这将展示与用户关联的存储空间。类 [Session] 像字典一样使用(第 4 行)。

完成上述设置后,我们编写以下 [Action16] 操作:


// 操作16
    public ContentResult Action16()
    {
      // 获取请求上下文HTTP
      HttpContextBase contexte = ControllerContext.HttpContext;
      // 获取应用程序作用域信息
      string infoAppli1 = contexte.Application["infoAppli1"] as string;
      // 以及会话范围的信息
      int? compteur = contexte.Session["compteur"] as int?;
      compteur++;
      contexte.Session["compteur"] = compteur;
      // 向客户端返回响应
      string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
      return Content(texte, "text/plain", Encoding.UTF8);
}
  • 第 5 行:获取正在处理的 HTTP 请求的上下文。该上下文将使我们能够访问作用域 [Application] 和 [Session] 中的数据;
  • 第7行:获取作用域[Application]的信息;
  • 第 9 行:从会话中获取计数器;
  • 第10-11行:将其递增后重新存入会话;
  • 第13-14行:将这两项信息发送给客户端。

以下是执行示例:

首次请求 [Action16],随后页面刷新,两次请求 [F5],最后请求 [2]:

在 [2] 时,客户端总共发出了三次请求。每次请求中,它都能获取到由前一次请求更新的计数器。

为了模拟第二个用户,我们使用第二个浏览器请求相同的 URL:

在 [3] 中,第二位用户确实获取到了与 [Application] 相同的范围信息,但拥有其独立的范围计数器 [Session]。

让我们回到操作代码 [Action16]:


// 操作16
    public ContentResult Action16()
    {
      // 获取请求上下文HTTP
      HttpContextBase contexte = ControllerContext.HttpContext;
      // 获取应用程序作用域的信息
      string infoAppli1 = contexte.Application["infoAppli1"] as string;
      // 以及会话范围的信息
      int? compteur = contexte.Session["compteur"] as int?;
      compteur++;
      contexte.Session["compteur"] = compteur;
      // 向客户端返回响应
      string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
      return Content(texte, "text/plain", Encoding.UTF8);
}

ASP.NET MVC 框架的目标之一,是让控制器和操作能够在不依赖 Web 服务器的情况下进行隔离测试。 然而,从第 5 行可以看出,需要 HTTP 请求上下文来获取 [Application] 和 [Session] 作用域中的信息。 我们建议创建一个新的操作 [Action17],该操作将接收作用域 [Application] 和 [Session] 的数据作为参数:


    // 操作17
    public ContentResult Action17(ApplicationModel applicationData, SessionModel sessionData)
    {
      // 获取应用程序作用域的信息
      string infoAppli1 = applicationData.InfoAppli1;
      // 以及会话作用域的信息
      int compteur = sessionData.Compteur++;
      // 向客户端返回响应
      string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
      return Content(texte, "text/plain", Encoding.UTF8);
}

该代码现已不再依赖于查询 HTTP。因此,它可以在脱离 Web 服务器的情况下进行独立测试。

让我们看看如何实现。 首先,我们需要创建类 [ApplicationModel] 和 [SessionModel],它们将分别封装作用域 [Application] 和 [Session] 中的数据。具体如下:


namespace Exemple_02.Models
{
  public class ApplicationModel
  {
    public string InfoAppli1 { get; set; }
  }
}

namespace Exemple_02.Models
{
  public class SessionModel
  {
    public int Compteur { get; set; }
    public SessionModel()
    {
      Compteur = 0;
    }
  }
}

接下来,我们需要修改文件 [Global.asax] 中的方法 [Application_Start] 和 [Session_Start]:


public class MvcApplication : System.Web.HttpApplication
  {
    protected void Application_Start()
    {
      AreaRegistration.RegisterAllAreas();

      WebApiConfig.Register(GlobalConfiguration.Configuration);
      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      BundleConfig.RegisterBundles(BundleTable.Bundles);
      // 应用程序初始化 - 情况 1
      Application["infoAppli1"] = ConfigurationManager.AppSettings["infoAppli1"];
      // 应用程序初始化 - 情况 2
      ApplicationModel data=new ApplicationModel();
      data.InfoAppli1=ConfigurationManager.AppSettings["infoAppli1"];
      Application["data"] = data;
    }

    protected void Session_Start()
    {
      // 计数器初始化 - 情况 1
      Session["compteur"] = 0;
      // 计数器初始化 - 情况 2
      Session["data"] = new SessionModel();
    }
  }
  • 第 14 行:创建 [ApplicationModel] 的实例;
  • 第 15 行:对其进行初始化;
  • 第 16 行:将其放入 [Application] 的字典中,并关联键 [data]。[Application] 是第 1 行中 [HttpApplication] 类的属性;
  • 第24行:创建一个[SessionModel]实例,并将其放入[Session]字典中,关联键为[data]。 [Session] 是第 1 行中 [HttpApplication] 类的属性;

根据我们迄今所见,该签名


    public ContentResult Action17(ApplicationModel applicationData, SessionModel sessionData)

意味着由该操作处理的请求 HTTP 必须包含名为 [applicationData] 和 [sessionData] 的参数。但实际情况并非如此。 我们需要创建一个新的数据绑定模型,以便当操作接收类型为:

  • [ApplicationModel],系统会向其提供作用域为 [Application]、键名为 [data] 的数据;
  • [SessionModel],需向其提供作用域为[Session]、键为[data]的数据。

为此,需要创建实现 [IModelBinder] 接口的类。

首先,我们在项目 [Exemple-02] 中创建一个名为 [Infrastructure] 的文件夹:

  

并在其中创建以下 [ApplicationModelBinder] 类:


using System.Web.Mvc;

namespace Exemple_02.Infrastructure
{
  public class ApplicationModelBinder : IModelBinder
  {
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
      // 获取作用域数据 [Application]
      return controllerContext.RequestContext.HttpContext.Application["data"];
    }
  }
}
  • 第 5 行:该类实现了接口 [IModelBinder]。要理解其代码,需知每当某个操作包含类型为 [ApplicationModel] 的参数时,该类都会被调用。 [ApplicationModel] --> [ApplicationModelBinder] 的关联将在应用程序启动时,通过 [Global.asax] 中的 [Application_Start] 方法建立;
  • 第 7 行:接口 [IModelBinder] 的唯一方法;
  • 第 7 行:类型为 [ControllerContext] 的参数使我们能够访问正在处理的 HTTP 请求;
  • 第 7 行:类型为 [ModelBindingContext] 的参数使我们能够访问待构建模型的信息,此处为类型 [ApplicationModel];
  • 第 7 行:[BindModel] 的结果是将被赋值给关联参数的对象,此处为类型为 [ApplicationModel] 的参数;
  • 第10行:我们只需返回作用域为[Application]、键为[data]的对象。

类 [SessionModelBinder] 遵循相同的模式:


using System.Web.Mvc;

namespace Exemple_02.Infrastructure
{
  public class SessionModelBinder : IModelBinder
  {
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
      // 返回作用域数据 [Session]
      return controllerContext.HttpContext.Session["data"];
    }
  }
}

接下来只需将每个 [XModel] 模型分别与对应的 binder 和 [XModelBinder] 关联即可。 这在 [Global.asax] 的 [Application_Start] 方法中完成:


    protected void Application_Start()
    {
....
      // 应用程序初始化 - 情况 2
      ApplicationModel data=new ApplicationModel();
      data.InfoAppli1=ConfigurationManager.AppSettings["infoAppli1"];
      Application["data"] = data;
      // 模型绑定器
      ModelBinders.Binders.Add(typeof(ApplicationModel), new ApplicationModelBinder());
      ModelBinders.Binders.Add(typeof(SessionModel), new SessionModelBinder());
}
  • 第 9 行:当某个操作具有 [ApplicationModel] 类型的参数时,将调用 [ApplicationModelBinder.Bind] 方法。 已知该方法会返回与键 [data] 关联的范围数据 [Application];
  • 第10行:对于类型[SessionModel]也是如此。

让我们回到我们的操作 [Action17]:


    // 操作17
    public ContentResult Action17(ApplicationModel applicationData, SessionModel sessionData)
    {
      // 获取应用程序作用域的信息
      string infoAppli1 = applicationData.InfoAppli1;
      // 以及会话范围的信息
      sessionData.Compteur++;
      int compteur = sessionData.Compteur;
      // 向客户端返回响应
      string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
      return Content(texte, "text/plain", Encoding.UTF8);
}
  • 第2行:当调用[Action17]时,其
    • 第一个参数:与键 [data] 关联的范围数据 [Application],
    • 第二个参数:与键 [data] 关联的范围数据 [Session];

这两个数据可以任意复杂,其中一个可以包含所有范围为 [Application] 的数据,另一个可以包含所有范围为 [Session] 的数据。

以下是操作 [Action17] 的执行示例:

 

4.11. 操作模型的延迟绑定

我们编写了以下 [Action12] 操作:


// 操作12
    public ContentResult Action12(ActionModel05 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("taux={0}, erreurs={1}", modèle.Taux, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

在后台,ASP.NET 会调用 MVC:

  • 使用无参构造函数创建一个 [ActionModel05] 类型的实例;
  • 并使用与 [ActionModel05] 中的某个属性同名(不区分大小写)的请求信息对其进行初始化。

有时这种行为并不符合我们的需求。特别是当我们希望使用操作模型的特定构造函数时。此时可以按以下方式操作:


    // 操作18
    public ContentResult Action18()
    {
      ActionModel05 modèle = new ActionModel05();
      TryUpdateModel(modèle);
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("taux={0}, erreurs={1}", modèle.Taux, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}
  • 第 2 行:操作不再接收参数。因此不再进行自动数据绑定;
  • 第 4 行:我们自行创建操作模型的实例。此时可以使用不同的构造函数;
  • 第 5 行:使用请求信息初始化模型。这项工作由 ASP.NET MVC 负责。其处理方式与模型作为参数传入时完全一致;
  • 第 6 行:此时的情况与 [Action12] 操作中的情况相同。

以下是一个执行示例:

 

4.12. Conclusion

让我们回到 ASP.NET MVC 应用程序的架构:

一个 [1] 请求携带了各种信息,ASP.NET MVC 将其以我们称为“操作模型”的形式呈现给 [2a] 操作。

  • 客户端的请求 HTTP 到达 [1];
  • 在 [2] 中,请求中的信息被转换为动作模型 [3];
  • 在 [4] 中,操作将基于该模板生成响应。该响应包含两个组成部分:视图 V [6] 及其对应的模型 M [5];
  • 视图 V [6] 将使用其模型 M [5] 来生成发给客户端的响应 HTTP。

在 MVC 模型中,操作 [4] 属于 C(控制器),视图 [5] 的模型是 M,而视图 [6] 是 V。

本章探讨了请求所携带的信息(本质上是字符串)与操作模型(可能是一个具有多种类型属性的类)之间的关联机制。我们还看到,可以验证提交给操作的模型的有效性。 最后,我们探讨了如何将该模型扩展到 [Session] 和 [Application] 范围内的数据。

现在,我们将关注请求 [1] 处理链的最后环节:创建视图 [6] 及其模型 [5]。 这两个组件由操作 [4] 生成。