【问题标题】:IValidatableObject Validate method firing when DataAnnotations failsIValidatableObject 验证方法在 DataAnnotations 失败时触发
【发布时间】:2011-12-30 12:22:58
【问题描述】:

我有一个 ViewModel,它有一些 DataAnnotations 验证,然后为了更复杂的验证实现 IValidatableObject 并使用 Validate 方法。

我所期待的行为是 this one:首先是所有的 DataAnnotations,然后,只有在没有错误的情况下,才使用 Validate 方法。我怎么发现这并不总是正确的。我的 ViewModel(演示版)有三个文件,一个 string,一个 decimal 和一个 decimal?。所有三个属性都只有Required 属性。对于stringdecimal?,行为是预期的行为,但对于decimal,当为空时,必需的验证失败(到目前为止还不错),然后执行Validate 方法。如果我检查该属性,它的值为零。

这里发生了什么?我错过了什么?

注意:我知道Required 属性是用来检查值是否为空的。所以我希望被告知不要在不可为空的类型中使用 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
}

视图模型:

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)
}

【问题讨论】:

  • decimal 永远不会为空。它不能像stringdecimal? 那样为空。您期望的行为是什么?
  • 您必须将 decimal 设置为可为空,否则 [必需] 将永远不会触发。有什么理由不这样做?
  • 意外行为是Required IS 触发。我收到了所需的消息(来自 DataAnnotations),并且 Validate 方法正在触发。我希望除非所有 DataAnnotations 都成功,否则不会触发 Validate 方法。
  • 检查all this post,尤其是“Under-Posting”问题部分。我猜你正面临一个不那么预期的行为

标签: asp.net asp.net-mvc asp.net-mvc-3 data-annotations ivalidatableobject


【解决方案1】:

cmets交流后的注意事项:

expected behavior among developers一致的是IValidatableObject的方法Validate()只有在没有触发验证属性的情况下才会被调用。简而言之,预期的算法是这样的(摘自上一个链接):

  1. 验证属性级属性
  2. 如果任何验证器无效,则中止验证并返回失败
  3. 验证对象级属性
  4. 如果任何验证器无效,则中止验证并返回失败
  5. 如果在桌面框架上并且对象实现了 IValidatableObject,则调用其 Validate 方法并返回任何失败

但是,使用问题的代码,即使在[Required] 触发之后也会调用Validate。这似乎是一个明显的 MVC 错误。哪个报告here

三种可能的解决方法:

  1. 有一个解决方法here 尽管除了破坏 MVC 预期行为之外,它的使用存在一些问题。为避免对同一字段显示多个错误,进行了一些更改,代码如下:

    viewModel
        .Validate(new ValidationContext(viewModel, null, null))
        .ToList()
        .ForEach(e => e.MemberNames.ToList().ForEach(m =>
        {
            if (ModelState[m].Errors.Count == 0)
                ModelState.AddModelError(m, e.ErrorMessage);
        }));
    
  2. 忘记IValidatableObject,只使用属性。它干净、直接、更好地处理本地化,最重要的是它可以在所有模型中重复使用。只需为您想要执行的每个验证实施ValidationAttribute。您可以验证所有模型或特定属性,这取决于您。除了默认可用的属性(DataType、Regex、Required 和所有这些东西)之外,还有几个库具有最常用的验证。实现“缺失的”的一个是FluentValidation

  3. 只实现IValidatableObject接口丢弃data annotations。如果它是一个非常特殊的模型并且不需要太多验证,这似乎是一个合理的选择。在大多数情况下,开发人员将执行所有常规和常见验证(即必需等),如果使用属性,则会导致默认情况下已经实现的验证代码重复。也没有可重用性。

在cmets之前回答:

首先,我只使用您提供的代码从头开始创建了一个新项目。它从不同时触发数据注释和验证方法。

无论如何,知道这一点,

按照设计,MVC3 将[Required]属性添加到不可为空的值类型,例如intDateTime,或者,是的,decimal。因此,即使您从该 decimal 中删除所需的属性,它的工作原理就像它在那里一样。

这是有争议的,因为它的错误(或不正确),但它的设计方式。

在你的例子中:

  • 如果存在 [Required] 且未给出值,则触发“DataAnnotation”。从我的角度来看完全可以理解
  • “DataAnnotation”在不存在 [Required] 但值不可为空时触发。值得商榷,但我倾向于同意它,因为如果属性不可为空,则必须输入一个值,否则不要向用户显示它或只使用可空的 decimal

看起来,这种行为可能会在您的 Application_Start 方法中被关闭:

DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;

我猜该属性的名称是不言自明的。

无论如何,我不明白您为什么要让用户输入不需要的内容并且不要使该属性可以为空。如果它是 null,那么您的工作就是在控制器内进行验证之前检查它,如果您不希望它为 null

public ActionResult Index(HomeViewModel viewModel)
{
    // Complete values that the user may have 
    // not filled (all not-required / nullables)

    if (viewModel.Decimal == null) 
    {
        viewModel.Decimal = 0m;
    }

    // Now I can validate the model

    if (ModelState.IsValid)
    {
        HomeModel.ProcessHome(viewModel);
        return RedirectToAction("Ok");
    }
}

您认为这种方法有什么问题或不应该这样?

【讨论】:

  • 我同意你的观点,当你说它是对非可空类型的隐式要求时是正确的。我的问题(您似乎无法重现)是触发了隐式(或显式)Required 属性,然后调用了 Validate 方法。我希望当某些 DataAnnotations 验证失败时(无论是隐式还是显式),永远不会调用此方法
  • 只有一种情况我可以重现这种行为(忘记存在 AddImplicitRequiredAttributeForValueTypes):使用不可为空的decimal 而不使用 [Required] 时。似乎隐含的 required 不算作 DataAnnotation 触发器并调用了 Validate。
  • 是的,但不仅如此。我也可以使用Required 属性来重现它。比如我的代码是,你不行吗?
  • Ups,你是对的,这种情况也会触发属性和验证。正常(或至少想要的行为)是在属性验证后调用Validate。如果这没有发生,我会称之为错误。
  • 这种行为在开发者之间是自愿的。在 Microsoft ASP.Net 员工的帖子中查看 here 甚至 here
猜你喜欢
  • 2012-01-29
  • 2019-11-15
  • 1970-01-01
  • 2023-03-03
  • 1970-01-01
  • 2016-10-30
  • 2011-11-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多