Skip to content

4. 操作的模型

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

在上一章中,我们探讨了将请求 [1] 路由到负责处理它的控制器和操作 [2a] 的过程,这一机制被称为路由。 我们还介绍了操作可以返回给浏览器的各种响应。到目前为止,我们介绍的操作并未处理提交给它们的请求。一个请求 [1] 携带了各种信息,ASP.NET MVC 会以模型的形式将这些信息 [2a] 呈现给操作。这个术语不应与操作生成的 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]:

Image

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


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

新功能位于第 8 行:[Action01] 方法有一个参数。在本章中,我们将探讨初始化操作参数的不同方法。上文中的 [name] 参数按顺序使用以下值进行初始化:

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

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

Image

浏览器的 HTTP 请求如下:

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

public ContentResult Action01(string nom)

为了为 name 参数赋值,ASP.NET MVC 会按以下顺序检查这些值:*Request.Form[&quot;name&quot;], RouteData.Values[&quot;name&quot;],* *<span style="color: #2323dc">Request.QueryString[&quot;name&quot;]</span>**, Request.Files[&quot;name&quot;]*。一旦找到值,它就会停止查找。 嵌入在 GET URL 中的 [name] 参数已被框架放置在 Request.QueryString["name"]正是通过这个值 [someone],[Action01] 的 [name] 参数将被初始化。随后 [Action01] 的代码开始执行:


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

该代码生成发送给客户端的响应:

Image

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


public ContentResult Action01(string NOM)

而传递的参数是 [?Name=zébulon],绑定仍会生效。[Action01] 的 [Name] 参数将接收值 [zébulon]。

现在,让我们使用 POST 方法请求同一 URL。为此,我们将使用 [Advanced Rest Client] 应用程序:

  • 在 [1] 中,输入请求的 URL;
  • 在 [2] 中,将使用 POST 方法;
  • 在 [3] 中,填写 POST 参数。

让我们发送此请求并查看 HTTP 日志。HTTP 请求如下:

  • 在 [1] 中,POST 请求;
  • 在 [2] 中,是 POST 参数。从技术上讲,它们是在 HTTP 头部之后发送的,紧跟在表示头部结束的空行之后;
  • 在 [3] 中,是收到的响应。我们成功从 POST 请求中获取了 [name] 参数。在对 name 参数进行的各项测试中——包括 Request.Form["name"]、RouteData.Values["name"]、Request.QueryString["name"] 和 Request.Files["name"]——第一个方法有效。

现在,让我们修改 [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 行,我们将路由的第三个元素命名为 [name];
  • 第 4 行,将该元素声明为可选。

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

Image

在对“name”参数进行的各项测试中——包括 Request.Form["name"]、RouteData.Values["name"]、Request.QueryString["name"] 和 Request.Files["name"]——第二种方式成功了。

让我们使用 POST 请求和 [Advanced Rest Client] 发出相同的请求:

  • 在 [1] 中,我们为路由的 {name} 元素赋值;
  • 在 [2] 中,我们在 POST 请求中添加了 [name] 参数;
  • 获得的响应如[3]所示。

在对 [name] 参数进行的值测试——Request.Form["name"]RouteData.Values["name"]、Request.QueryString["name"]、Request.Files["name"]——其中两个是有效的:前两个。最终采用了第一个。

4.2. 验证操作参数

如果某个操作方法包含名为 [p] 的参数,ASP.NET MVC 将尝试为其赋值以下其中一个值:Request.Form["p"]、RouteData.Values["p"]、Request.QueryString["p"] 或 Request.Files["p"]。前三个值均为字符串。如果 [p] 参数的类型不是 [string],可能会引发问题。

让我们创建以下新操作:


    // Action02
    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]。我们将获得以下页面:

Image

让我们请求 URL [http://localhost:55483/First/Action02?age=21x]。我们将获得以下页面:

Image

这次,我们收到了一页错误页面。此时查看服务器发送的 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] 状态码,并发送了一页 12,438 字节(第 5 行)的 HTML 页面(第 3 行),以解释此错误的可能原因。

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


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

[Action03] 与 [Action02] 完全相同,只是我们将 [age] 参数的类型改为 int?,这意味着该参数可以是整数或 null

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

Image

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

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


    // Action04
    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 行:我们将 [valid] 变量的值添加到发送给客户端的文本中。

现在访问 URL [http://localhost:55483/First/Action04?age=21x]。我们将看到以下页面:

Image

ASP.NET MVC 无法将 [21x] 转换为 int 类型。因此,根据其 int? 类型的允许,它将 [age] 参数的值设为 null。然而,如 [valid] 的值所示,确实发生了转换错误。

我们可以获取与转换失败相关的错误消息。让我们来查看以下新的操作:


    // Action05
    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] 参数的错误消息(如有)
    • 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]。我们会得到以下页面:

Image

4.3. 带有多个参数的操作

考虑以下新操作:


    // Action06
    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 行:我们有两个参数 [weight] 和 [age]。

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

Image

Image

4.4. 使用类作为动作模型

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

其代码如下:


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

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


    // Action07
    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] 类型的实例。

让我们重新回顾之前的两个示例:

Image

Image

请注意,参数绑定不区分大小写。请求参数为 [age] 和 [weight]。它们填充了 [ModelAction01] 类的 [Age] 和 [Weight] 属性。

此外,到目前为止我们一直使用的是 [GET] HTTP 请求。现在让我们验证 [POST] 请求是否也具有相同的行为。为此,让我们再次使用 [Advanced Rest Client] 应用程序:

  • 在 [1] 中,请求的 URL;
  • 在 [2] 中,将通过 POST 请求发送;
  • 在 [3] 中,是 POST 参数。

我们得到的响应与 GET 请求时相同:

Image

4.5. 带验证约束的 Action 模型 - 1

使用前面的模型:


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

请求中可以省略 [weight] 和 [age] 参数。在这种情况下,[Weight] 和 [Age] 属性将被设置为 [null],且不会报告错误。您可能希望将模型转换如下:


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

第 5 行和第 6 行:[Weight] 和 [Age] 属性不再允许取值为 [null]。让我们看看当请求中缺少 [weight] 和 [age] 参数时,这个新模型会发生什么情况。

Image

没有出现错误,且 [Weight] 和 [Age] 属性保留了其初始化值:0。ASP.NET MVC:

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

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

请看以下这个新的操作模型:

Image


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 行:表示 [Weight] 字段是必填的;
  • 第 7 行:表示 [Weight] 字段必须在 [1,200] 范围内;
  • 第 9 行:表示 [Age] 字段为必填项;
  • 第7行:表示 [Age] 字段必须在 [1,150] 范围内;

使用此模板的操作将如下所示 [Action08]:


    // Action08
    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] 的实例;

让我们运行一些测试:

Image

Image

Image

Image

错误已被正确检测到。现在,让我们按以下方式更新模型:


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]。让我们编译并再次运行不带参数的测试:

Image

由于没有参数,[Weight] 和 [Age] 属性保留了模型实例化时获取的值:0。随后进行验证。此时 [Required] 属性被满足。 我们可以看到,上面的错误信息是针对 [Range] 属性的。因此,要检查参数是否存在,关联的属性必须是可为空的,即它必须能够接受 null 值。

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


    // Action09
    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);
}

让我们运行一些测试:

Image

我们没有向该操作传递任何参数。[Weight] 和 [Age] 属性上的 [Required] 属性发挥了作用。然而,date 属性接收到了 null 值,且未报告任何错误。

现在让我们传入无效的参数:

Image

现在让我们传入有效值:

Image

让我们检查其他验证约束。新的操作模型如下:

Image


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] 属性要求 [Day] 字段包含一个或两个数字的字符串。第一个参数是该字段必须通过验证的正则表达式;
  • 第 15 行:[MaxLength] 属性要求 [Info1] 字段的长度不超过 4 个字符;
  • 第 19 行:[MinLength] 属性要求 [Info2] 字段至少包含 2 个字符;
  • 第 23-24 行:结合 [MaxLength] 和 [MinLength] 属性,要求 [Info3] 字段的字符数必须恰好为 4 个;

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


// Action10
    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);
    }

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

首先,不带参数:

Image

然后使用无效参数:

Image

接着是带有效参数的情况:

Image

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 行:要求注释字段为有效的信用卡号。

使用此模板的操作如下:


    // Action11
    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] 中,将通过 POST 方法发送此 URL;
  • 在 [3] 中,选择 [表单] 选项卡;
  • 在 [4] 中,显示四个预期参数的值。此初始化功能由 [ARC] 提供。实际发送的参数可在 [Raw] 选项卡 [5] 中查看;
  • 在 [6] 中,显示 POST 参数。

对于此请求,我们收到以下响应:

Image

让我们传入无效的参数:

Image

随后我们收到以下响应:

Image

4.7. 带有效性约束的 Action 模型 - 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 行:我们检查 [Rate] 属性的有效性;
  • 第 14–17 行:如果 [Rate] 属性无效,则向结果列表中添加一个 [ValidationResult] 类型的元素。第一个参数是错误消息。第二个参数(可选)是受此错误影响的属性集合。

使用此模型的操作如下:


    // Action12
    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);
}

以下是一个执行示例:

Image

4.8. 类型为 Table 或 List 的操作模型

考虑以下操作 [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] 数组。这种情况常见于下拉列表。此时,浏览器会发送用户选择的不同值,且所有值都使用相同的参数名称。

以下是一个示例:

Image

模型也可以是一个列表:


    // Action14
    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 行)。以下是首次运行结果:

Image

以及第二次运行:

Image

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]:


    // Action15
    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] 中:我们在 URL 中传递了 [info2] 参数;
  • 在 [2] 中:操作模型的 [Info2] 属性保持为空。

4.10. 扩展数据绑定模型

让我们重新审视一个操作的执行架构:

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

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

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

  • 在同一客户端的不同请求之间共享的数据。此类数据存储在一个名为 Session 的对象中。我们将其称为客户端会话,以表示客户端的内存。来自同一客户端的所有请求均可访问该会话,并在其中存储和读取信息。

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

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

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

首先,我们将 [Example-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);
      // intialisation application
      Application["infoAppli1"] = ConfigurationManager.AppSettings["infoAppli1"];
}

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

  • 它使用 [System.Configuration.ConfigurationManager] 类从 [Web.config] 文件中检索 [infoAppli1] 键的值;
  • 将其存储在 [HttpApplication.Application] 字典中,并关联 [infoAppli1] 键。所有操作都可以访问此字典。

在同一个 [Global.asax] 文件中,我们添加以下 [Session_Start] 方法:


    protected void Session_Start()
    {
      // initialisation compteur
      Session["compteur"] = 0;
}

[Session_Start] 方法会在每个新用户登录时执行。什么是新用户?用户通过会话令牌进行“追踪”。该令牌由:

  • 由 Web 服务器生成,并通过发送给新用户的首次响应的 HTTP 头部传递给用户;
  • 在用户每次发起新请求时,由其浏览器一并发送回来。这使得服务器能够识别用户,并为其管理一个称为“用户会话”的内存空间。

当用户未发送会话令牌时,Web 服务器会识别出正在处理的是新用户。此时,服务器会为其创建一个会话令牌。

在上文第 4 行中,我们在用户的会话中放置了一个计数器,该计数器将随着该用户的每次请求而递增。这展示了与用户关联的内存。 [Session] 类的使用方式类似于字典(第 4 行)。

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


// Action16
    public ContentResult Action16()
    {
      // on récupère le contexte de la requête HTTP
      HttpContextBase contexte = ControllerContext.HttpContext;
      // on récupère les infos de portée Application
      string infoAppli1 = contexte.Application["infoAppli1"] as string;
      // et celles de portée Session
      int? compteur = contexte.Session["compteur"] as int?;
      compteur++;
      contexte.Session["compteur"] = compteur;
      // la réponse au client
      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] 被请求一次 [1],随后页面被刷新 [F5] 两次 [2]:

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

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

在[3]中,第二位用户成功获取了相同的[Application]作用域信息,但拥有其独立的[Session]作用域计数器。

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


// Action16
    public ContentResult Action16()
    {
      // on récupère le contexte de la requête HTTP
      HttpContextBase contexte = ControllerContext.HttpContext;
      // on récupère les infos de portée Application
      string infoAppli1 = contexte.Application["infoAppli1"] as string;
      // et celles de portée Session
      int? compteur = contexte.Session["compteur"] as int?;
      compteur++;
      contexte.Session["compteur"] = compteur;
      // la réponse au client
      string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
      return Content(texte, "text/plain", Encoding.UTF8);
}

ASP.NET MVC 框架的目标之一是使控制器和操作能够在不依赖 Web 服务器的情况下独立进行测试。然而,如第 5 行所示,从 [Application] 和 [Session] 作用域中检索数据时需要 HTTP 请求上下文。我们建议创建一个新的操作 [Action17],该操作将 [Application] 和 [Session] 作用域的数据作为参数接收:


    // Action17
    public ContentResult Action17(ApplicationModel applicationData, SessionModel sessionData)
    {
      // retrieve range info Application
      string infoAppli1 = applicationData.InfoAppli1;
      // and Session range
      int compteur = sessionData.Compteur++;
      // customer response
      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);
      // intialisation application - case 1
      Application["infoAppli1"] = ConfigurationManager.AppSettings["infoAppli1"];
      // intialisation application - case 2
      ApplicationModel data=new ApplicationModel();
      data.InfoAppli1=ConfigurationManager.AppSettings["infoAppli1"];
      Application["data"] = data;
    }
 
    protected void Session_Start()
    {
      // counter initialization - case 1
      Session["compteur"] = 0;
      // counter initialization - case 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] 作为参数时:

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

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

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

Image

在该文件夹中,我们创建以下 [ApplicationModelBinder] 类:


using System.Web.Mvc;
 
namespace Exemple_02.Infrastructure
{
  public class ApplicationModelBinder : IModelBinder
  {
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
      // render scope data [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] 的返回结果是将被赋值给 bound 参数的对象,此处为 [ApplicationModel] 类型的参数;
  • 第 10 行:我们只需返回一个作用域为 [Application]、键名为 [data] 的对象。

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


using System.Web.Mvc;
 
namespace Exemple_02.Infrastructure
{
  public class SessionModelBinder : IModelBinder
  {
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
      // render scope data [Session]
      return controllerContext.HttpContext.Session["data"];
    }
  }
}

剩下的就是将每个 [XModel] 模型与其 [XModelBinder] 关联起来。这在 [Global.asax] 的 [Application_Start] 方法中完成:


    protected void Application_Start()
    {
....
      // intialisation application - case 2
      ApplicationModel data=new ApplicationModel();
      data.InfoAppli1=ConfigurationManager.AppSettings["infoAppli1"];
      Application["data"] = data;
      // model binders
      ModelBinders.Binders.Add(typeof(ApplicationModel), new ApplicationModelBinder());
      ModelBinders.Binders.Add(typeof(SessionModel), new SessionModelBinder());
}

  • 第 9 行:当一个操作具有 [ApplicationModel] 类型的参数时,将调用 [ApplicationModelBinder.Bind] 方法。我们知道该方法会返回 [Application] 作用域中与 [data] 键关联的数据;
  • 第 10 行:对于 [SessionModel] 类型也是如此。

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


    // Action17
    public ContentResult Action17(ApplicationModel applicationData, SessionModel sessionData)
    {
      // retrieve range info Application
      string infoAppli1 = applicationData.InfoAppli1;
      // and Session range
      sessionData.Compteur++;
      int compteur = sessionData.Compteur;
      // customer response
      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] 执行的示例:

Image

4.11. 操作模型的延迟绑定

我们编写了以下 [Action12]:


// Action12
    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] 中的某个属性名称(不区分大小写)相同的请求信息对其进行初始化。

有时这种行为并非我们所期望的。当我们希望使用操作模型的特定构造函数时,这种情况尤为明显。此时,我们可以按以下步骤操作:


    // Action18
    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] 操作相同的情况。

以下是一个执行示例:

Image

4.12. 结论

让我们回到 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]生成。