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

在此控制器中,我们创建以下操作 [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]。我们会得到以下响应:

浏览器的 HTTP 请求如下:
- 第 1 行:该请求为 GET 请求。请求的 URL 包含 [name] 参数。在服务器端,该请求被路由到 [Action01] 操作,其签名如下:
public ContentResult Action01(string nom)
为了为 name 参数赋值,ASP.NET MVC 会按以下顺序检查这些值:*Request.Form["name"], RouteData.Values["name"],* *<span style="color: #2323dc">Request.QueryString["name"]</span>**, Request.Files["name"]*。一旦找到值,它就会停止查找。 嵌入在 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);
该代码生成发送给客户端的响应:

注意:参数绑定机制不区分大小写。因此,如果我们的操作定义为:
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]。我们会得到以下响应:

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

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

这次,我们收到了一页错误页面。此时查看服务器发送的 HTTP 头部信息会很有意思:
- 第 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]。我们将获得以下页面:

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]。我们将看到以下页面:

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]。我们会得到以下页面:

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]。
上述规则现在适用于这两个参数。以下是一些执行示例:


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] 类型的实例。
让我们重新回顾之前的两个示例:


请注意,参数绑定不区分大小写。请求参数为 [age] 和 [weight]。它们填充了 [ModelAction01] 类的 [Age] 和 [Weight] 属性。
此外,到目前为止我们一直使用的是 [GET] HTTP 请求。现在让我们验证 [POST] 请求是否也具有相同的行为。为此,让我们再次使用 [Advanced Rest Client] 应用程序:
![]() |
- 在 [1] 中,请求的 URL;
- 在 [2] 中,将通过 POST 请求发送;
- 在 [3] 中,是 POST 参数。
我们得到的响应与 GET 请求时相同:
![]()
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] 参数时,这个新模型会发生什么情况。

没有出现错误,且 [Weight] 和 [Age] 属性保留了其初始化值:0。ASP.NET MVC:
- 通过 `new ActionModel01` 创建了模型实例。此时,[Weight] 和 [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 行:表示 [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] 的实例;
让我们运行一些测试:




错误已被正确检测到。现在,让我们按以下方式更新模型:
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]。让我们编译并再次运行不带参数的测试:

由于没有参数,[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);
}
让我们运行一些测试:

我们没有向该操作传递任何参数。[Weight] 和 [Age] 属性上的 [Required] 属性发挥了作用。然而,date 属性接收到了 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] 属性要求 [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);
}
让我们用这个操作进行一些测试。
首先,不带参数:

然后使用无效参数:

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

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 参数。
对于此请求,我们收到以下响应:

让我们传入无效的参数:

随后我们收到以下响应:
![]()
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);
}
以下是一个执行示例:

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

模型也可以是一个列表:
// 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 行)。以下是首次运行结果:

以及第二次运行:

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] 文件夹:

在该文件夹中,我们创建以下 [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] 执行的示例:

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] 操作相同的情况。
以下是一个执行示例:

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]生成。




















