IValidatableObject Validate 方法在 DataAnnotations 失败时触发 [英] IValidatableObject Validate method firing when DataAnnotations fails

查看:32
本文介绍了IValidatableObject Validate 方法在 DataAnnotations 失败时触发的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

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

注意:我知道 Required 属性应该检查值是否为空.所以我希望被告知不要在不可为空的类型中使用 Required 属性(因为它永远不会触发),或者,该属性以某种方式理解 POST 值并注意该字段未填充.在第一种情况下,不应触发该属性,而应触发 Validate 方法.在第二种情况下,属性应该触发并且 Validate 方法不应该触发.但我的结果是:属性触发,Validate 方法触发.

这是代码(没什么特别的):

控制器:

public ActionResult Index(){返回视图(HomeModel.LoadHome());}[HttpPost]公共 ActionResult 索引(HomeViewModel viewModel){尝试{如果(模型状态.IsValid){HomeModel.ProcessHome(viewModel);return RedirectToAction("Index", "Result");}}捕获(应用程序异常前){ModelState.AddModelError(string.Empty, ex.Message);}捕获(异常前){ModelState.AddModelError(string.Empty, "内部错误.");}返回视图(视图模型);}

型号:

public static HomeViewModel LoadHome(){HomeViewModel viewModel = new HomeViewModel();viewModel.String = string.Empty;返回视图模型;}public static void ProcessHome(HomeViewModel viewModel){//不相关的代码}

视图模型:

公共类 HomeViewModel : IValidatableObject{[必需(ErrorMessage =必需{0}")][显示(名称=字符串")]公共字符串字符串 { 获取;放;}[必需(ErrorMessage =必需{0}")][显示(名称=十进制")]公共十进制十进制{得到;放;}[必需(ErrorMessage =必需{0}")][显示(名称=十进制?")]公共小数点?DecimalNullable { 获取;放;}公共 IEnumerable验证(ValidationContext 验证上下文){yield return new ValidationResult("验证方法出错");}}

查看:

@model MVCTest1.ViewModels.HomeViewModel@{布局 = "~/Views/Shared/_Layout.cshtml";}@using (Html.BeginForm(null, null, FormMethod.Post)){<div>@Html.ValidationSummary()

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

解决方案

评论交流后的思考:

双方同意和开发人员之间的预期行为IValidatableObject 的方法 Validate() 仅在未触发验证属性时调用.简而言之,预期的算法是这样的(取自上一个链接):

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

然而,使用问题的代码,即使在 [Required] 触发后也会调用 Validate .这似乎是一个明显的 MVC 错误.在此处报告.

三种可能的解决方法:

  1. 有一个解决方法这里尽管除了破坏 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 接口丢弃 数据注释.如果它是一个非常特殊的模型并且不需要太多验证,这似乎是一个合理的选择.在大多数情况下,开发人员将执行所有常规和通用验证(即必需等),如果使用属性,则会导致默认已实现的验证代码重复.也没有可重用性.


评论前先回答:

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

无论如何,知道这一点,

按照设计,MVC3 为不可为空的值类型添加了一个 [Required] 属性,例如 intDateTime 或,是的,<代码>十进制.因此,即使您从那个 decimal 中删除了必需的属性,它的工作方式就像它在那里一样.

这是有争议的,因为它的错误(或没有),但它的设计方式.

在你的例子中:

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

似乎可以在您的 Application_Start 方法中关闭此行为:

DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;

我猜酒店的名字是不言自明的.

无论如何,我不明白您为什么要让用户输入不需要的内容并且不将该属性设为可空.如果它是 null 那么你的工作就是检查它,如果你不想它为 null,在验证之前,在控制器中.

public ActionResult Index(HomeViewModel viewModel){//用户可能拥有的完整值//未填充(所有非必需/可为空的)如果(viewModel.Decimal == null){viewModel.Decimal = 0m;}//现在我可以验证模型如果(模型状态.IsValid){HomeModel.ProcessHome(viewModel);返回 RedirectToAction("确定");}}

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

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

解决方案

Considerations after comments' exchange:

The consensual and expected behavior among developers is that IValidatableObject's method Validate() is only called if no validation attributes are triggered. In short, the expected algorithm is this (taken from the previous link):

  1. Validate property-level attributes
  2. If any validators are invalid, abort validation returning the failure(s)
  3. Validate the object-level attributes
  4. If any validators are invalid, abort validation returning the failure(s)
  5. If on the desktop framework and the object implements IValidatableObject, then call its Validate method and return any failure(s)

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:

  1. 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:

    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. 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.

  3. Implement only 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, like int, DateTime or, yes, decimal. So, even if you remove required attribute from that decimal 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:

  • 'DataAnnotation' triggers if [Required] is present and no value is given. Totally understandable from my point of view
  • 'DataAnnotation' triggers if no [Required] is present but value is non-nullable. Debatable but I tend to agree with it because if the property is non-nullable, a value must be inputted, otherwise don't show it to the user or just use a nullable decimal.

This behavior, as it seems, may be turned off with this within your Application_Start method:

DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;

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.

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

What do you think it's wrong on this approach or shouldn't be this way?

这篇关于IValidatableObject Validate 方法在 DataAnnotations 失败时触发的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆