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 行:该请求为 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 行:服务器返回了状态码 [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 相同:
![]()
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] 生成。
























































