IValidatableObject在DataAnnotations失败时触发验证方法

时间:2011-11-16 14:51:34

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

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

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

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

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

1 个答案:

答案 0 :(得分:14)

评论交换后的注意事项:

同意和expected behavior among developersIValidatableObject的方法Validate()仅在未触发验证属性时才会被调用。简而言之,预期的算法就是这个(取自上一个链接):

  1. 验证属性级属性
  2. 如果任何验证器无效,则中止验证将返回失败
  3. 验证对象级属性
  4. 如果任何验证器无效,则中止验证将返回失败
  5. 如果在桌面框架上并且该对象实现了IValidatableObject,则调用其Validate方法并返回任何失败
  6. 但是,使用问题代码,即使在Validate触发之后,也会调用[Required]。这似乎是一个明显的 MVC bug 。据报道here

    三种可能的解决方法:

    1. 除了打破MVC预期的行为之外,还有一个解决方法here虽然有一些明确的使用问题。通过一些更改以避免在同一字段中显示多个错误,代码为:

      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。如果它是一个非常特殊的模型并且不需要太多验证,这似乎是一个合理的选择。在大多数情况下,开发人员将执行所有常规和常见验证(即必需等),如果使用了属性,则会导致默认情况下已经实现的验证上的代码重复。也没有可重用性。

    4. 在评论前回答:

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

      无论如何,知道这一点,

      按照设计,MVC3会为不可为空的值类型添加[Required]属性,例如intDateTime或是,decimal。因此,即使您从decimal中删除了必需的属性,它就像是那里的那个。

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

      在你的例子中:

        如果[Required]存在且没有给出值,则
      • 'DataAnnotation'触发。从我的观点来看,完全可以理解
      • 如果不存在[Required]但值不可为空,则触发'DataAnnotation'。有争议但我倾向于同意它,因为如果属性不可为空,则必须输入一个值,否则不要向用户显示或只使用可空的decimal

      在您的Application_Start方法中,可能会关闭此行为:

      DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;
      

      我猜这个属性的名字是不言自明的。

      无论如何,我不明白你为什么要让用户输入不需要的东西,不要让那个属性可以为空。如果它是 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");
          }
      }
      

      您认为这种方法有什么不对,或者不应该这样?