当 DataAnnotations 失败时触发 IValidatableObject Validate 方法
我有一个 ViewModel,它有一些 DataAnnotations 验证,然后对于更复杂的验证实现 IValidatableObject 并使用 Validate 方法。
我期望的行为是这个:首先所有DataAnnotations,然后,仅当没有错误时,才使用 Validate 方法。我怎么发现这并不总是正确的。我的 ViewModel(演示版)具有三个字段:一个字符串
、一个十进制
和一个十进制?
。所有三个属性都只有Required 属性。对于 string
和 decimal?
,行为是预期的,但对于 decimal
,当为空时,必需的验证失败(到目前为止)好),然后执行 Validate 方法。如果我检查该财产,其价值为零。
这是怎么回事?我缺少什么?
注意:我知道Required属性应该检查该值是否为空。因此,我希望被告知不要在不可为空的类型中使用必需属性(因为它永远不会触发),或者该属性以某种方式理解 POST 值并注意该字段未填充。在第一种情况下,不应触发该属性,而应触发 Validate 方法。在第二种情况下,应该触发该属性,并且不应触发 Validate 方法。但我的结果是:属性触发并触发 Validate 方法。
这是代码(没什么特别的):
控制器:
public ActionResult Index()
{
return View(HomeModel.LoadHome());
}
[HttpPost]
public ActionResult Index(HomeViewModel viewModel)
{
try
{
if (ModelState.IsValid)
{
HomeModel.ProcessHome(viewModel);
return RedirectToAction("Index", "Result");
}
}
catch (ApplicationException ex)
{
ModelState.AddModelError(string.Empty, ex.Message);
}
catch (Exception ex)
{
ModelState.AddModelError(string.Empty, "Internal error.");
}
return View(viewModel);
}
模型:
public static HomeViewModel LoadHome()
{
HomeViewModel viewModel = new HomeViewModel();
viewModel.String = string.Empty;
return viewModel;
}
public static void ProcessHome(HomeViewModel viewModel)
{
// Not relevant code
}
ViewModel:
public class HomeViewModel : IValidatableObject
{
[Required(ErrorMessage = "Required {0}")]
[Display(Name = "string")]
public string String { get; set; }
[Required(ErrorMessage = "Required {0}")]
[Display(Name = "decimal")]
public decimal Decimal { get; set; }
[Required(ErrorMessage = "Required {0}")]
[Display(Name = "decimal?")]
public decimal? DecimalNullable { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
yield return new ValidationResult("Error from Validate method");
}
}
视图:
@model MVCTest1.ViewModels.HomeViewModel
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
@using (Html.BeginForm(null, null, FormMethod.Post))
{
<div>
@Html.ValidationSummary()
</div>
<label id="lblNombre" for="Nombre">Nombre:</label>
@Html.TextBoxFor(m => m.Nombre)
<label id="lblDecimal" for="Decimal">Decimal:</label>
@Html.TextBoxFor(m => m.Decimal)
<label id="lblDecimalNullable" for="DecimalNullable">Decimal?:</label>
@Html.TextBoxFor(m => m.DecimalNullable)
<button type="submit" id="aceptar">Aceptar</button>
<button type="submit" id="superAceptar">SuperAceptar</button>
@Html.HiddenFor(m => m.Accion)
}
I've a ViewModel which has some DataAnnotations validations and then for more complex validations implements IValidatableObject and uses Validate method.
The behavior I was expecting was this one: first all the DataAnnotations and then, only if there were no errors, the Validate method. How ever I find out that this isn't always true. My ViewModel (a demo one) has three fileds one string
, one decimal
and one decimal?
. All the three properties have only Required attribute. For the string
and the decimal?
the behavior is the expected one, but for the decimal
, when empty, Required validation fails (so far so good) and then executes the Validate method. If I inspect the property its value is zero.
What is going on here? What am I missing?
Note: I know that Required attribute is suppose to check if the value is null. So I'd expect to be told not to use Required attribute in not-nullable types (because it wont ever trigger), or, that somehow the attribute understand the POST values and note that the field wasn't filled. In the first case the attribute shouldn't trigger and the Validate method should fire. In the second case the attribute should trigger and the Validate method shouldn't fire. But my result are: the attributes triggers and the Validate method fires.
Here is the code (nothing too special):
Controller:
public ActionResult Index()
{
return View(HomeModel.LoadHome());
}
[HttpPost]
public ActionResult Index(HomeViewModel viewModel)
{
try
{
if (ModelState.IsValid)
{
HomeModel.ProcessHome(viewModel);
return RedirectToAction("Index", "Result");
}
}
catch (ApplicationException ex)
{
ModelState.AddModelError(string.Empty, ex.Message);
}
catch (Exception ex)
{
ModelState.AddModelError(string.Empty, "Internal error.");
}
return View(viewModel);
}
Model:
public static HomeViewModel LoadHome()
{
HomeViewModel viewModel = new HomeViewModel();
viewModel.String = string.Empty;
return viewModel;
}
public static void ProcessHome(HomeViewModel viewModel)
{
// Not relevant code
}
ViewModel:
public class HomeViewModel : IValidatableObject
{
[Required(ErrorMessage = "Required {0}")]
[Display(Name = "string")]
public string String { get; set; }
[Required(ErrorMessage = "Required {0}")]
[Display(Name = "decimal")]
public decimal Decimal { get; set; }
[Required(ErrorMessage = "Required {0}")]
[Display(Name = "decimal?")]
public decimal? DecimalNullable { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
yield return new ValidationResult("Error from Validate method");
}
}
View:
@model MVCTest1.ViewModels.HomeViewModel
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
@using (Html.BeginForm(null, null, FormMethod.Post))
{
<div>
@Html.ValidationSummary()
</div>
<label id="lblNombre" for="Nombre">Nombre:</label>
@Html.TextBoxFor(m => m.Nombre)
<label id="lblDecimal" for="Decimal">Decimal:</label>
@Html.TextBoxFor(m => m.Decimal)
<label id="lblDecimalNullable" for="DecimalNullable">Decimal?:</label>
@Html.TextBoxFor(m => m.DecimalNullable)
<button type="submit" id="aceptar">Aceptar</button>
<button type="submit" id="superAceptar">SuperAceptar</button>
@Html.HiddenFor(m => m.Accion)
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
意见交换后的考虑因素:
双方同意的和预期行为开发人员的一个问题是,只有在没有触发验证属性时才会调用
IValidatableObject
的方法Validate()
。简而言之,预期的算法是这样的(取自上一个链接):但是,使用问题的代码中,即使在
[Required]
触发之后也会调用Validate
。这似乎是一个明显的MVC 错误。 此处报告。三种可能的解决方法:
这里有一个解决方法 > 尽管除了破坏 MVC 预期行为之外,它的使用也存在一些明显的问题。为了避免同一字段显示多个错误,进行了一些更改,代码如下:
忘记
IValidatableObject
并仅使用属性。它干净、直接,可以更好地处理本地化,最重要的是它可以在所有模型中重复使用。只需为您想要的每个验证实现 ValidationAttribute去做。您可以验证所有模型或特定属性,这取决于您。除了默认可用的属性(DataType、Regex、Required 等)之外,还有几个具有最常用验证的库。实现“缺失部分”的一个是 FluentValidation。IValidatableObject
接口,丢弃 数据注释。如果它是一个非常特殊的模型并且不需要太多验证,这似乎是一个合理的选择。在大多数情况下,开发人员将执行所有常规和常见验证(即必需的验证等),这会导致在使用属性时默认已实现的验证上出现代码重复。也没有可重用性。评论前回答:
首先,我仅使用您提供的代码从头开始创建了一个新项目。它永远不会同时触发数据注释和验证方法。
无论如何,请知道这一点,
根据设计,MVC3 为不可空值类型添加了一个
[Required]
属性,例如int
、DateTime
或者,是的,十进制
。因此,即使您从该decimal
中删除必需的属性,它的工作方式也与那里的属性一样。这是有争议的(或没有)错误,但它的设计方式。
在您的示例中:
小数
。看起来,这种行为可能会在您的 Application_Start 方法中被关闭:
我想该属性的名称是不言自明的。
无论如何,我不明白为什么您希望用户输入不需要的内容并且不使该属性可为空。如果它是null,那么您的工作就是在验证之前在控制器内检查它,如果您不希望它为null。
您认为这种方法有什么问题或者不应该这样?
Considerations after comments' exchange:
The consensual and expected behavior among developers is that
IValidatableObject
's methodValidate()
is only called if no validation attributes are triggered. In short, the expected algorithm is this (taken from the previous link):However, using question's code,
Validate
is called even after[Required]
triggers. This seems an obvious MVC bug. Which is reported here.Three possible workarounds:
There's a workaround here although with some stated problems with it's usage, apart from breaking the MVC expected behavior. With a few changes to avoid showing more than one error for the same field here is the code:
Forget
IValidatableObject
and use only attributes. It's clean, direct, better to handle localization and best of all its reusable among all models. Just implement ValidationAttribute for each validation you want to do. You can validate the all model or particular properties, that's up to you. Apart from the attributes available by default (DataType, Regex, Required and all that stuff) there are several libraries with the most used validations. One which implements the "missing ones" is FluentValidation.IValidatableObject
interface throwing away data annotations. This seems a reasonable option if it's a very particular model and it doesn't requires much validation. On most cases the developer will be doing all that regular and common validation (i.e. Required, etc.) which leads to code duplication on validations already implemented by default if attributes were used. There's also no re-usability.Answer before comments:
First of all I've created a new project, from scratch with only the code you provided. It NEVER triggered both data annotations and Validate method at the same time.
Anyway, know this,
By design, MVC3 adds a
[Required]
attribute to non-nullable value types, likeint
,DateTime
or, yes,decimal
. So, even if you remove required attribute from thatdecimal
it works just like it is one there.This is debatable for its wrongness (or not) but its the way it's designed.
In you example:
decimal
.This behavior, as it seems, may be turned off with this within your Application_Start method:
I guess the property's name is self-explanatory.
Anyway, I don't understand why do you want to the user to input something not required and don't make that property nullable. If it's null then it is your job to check for it, if you don't wan't it to be null, before validation, within the controller.
What do you think it's wrong on this approach or shouldn't be this way?