当 DataAnnotations 失败时触发 IValidatableObject Validate 方法

发布于 2024-12-15 15:05:18 字数 3091 浏览 1 评论 0原文

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

我期望的行为是这个:首先所有DataAnnotations,然后,仅当没有错误时,才使用 Validate 方法。我怎么发现这并不总是正确的。我的 ViewModel(演示版)具有三个字段:一个字符串、一个十进制 和一个十进制?。所有三个属性都只有Required 属性。对于 stringdecimal? ,行为是预期的,但对于 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

空气里的味道 2024-12-22 15:05:18

意见交换后的考虑因素:

双方同意的和预期行为开发人员的一个问题是,只有在没有触发验证属性时才会调用 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 中删除必需的属性,它的工作方式也与那里的属性一样。

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

在您的示例中:

  • 如果存在 [Required] 并且未给出任何值,则会触发“DataAnnotation”。从我的角度来看,
  • 如果不存在 [Required] 但值不可为空,则“DataAnnotation”会触发,这是完全可以理解的。有争议,但我倾向于同意它,因为如果属性不可为空,则必须输入一个值,否则不向用户显示它或仅使用可为空的小数

看起来,这种行为可能会在您的 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");
    }
}

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

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?

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文