8

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

我期待的行为是this one:首先是所有DataAnnotations,然后,只有在没有错误的情况下,Validate方法。我怎么会发现这并非总是如此。我的ViewModel(演示版)有三个分区,一个为string,一个为decimal,另一个为decimal?。所有这三个属性都只有必需的属性。对于stringdecimal?,行为是预期行为,但对于空行为decimal,Required验证失败(迄今为止很好),然后执行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) 
} 
+1

'decimal'不会为空。它不能像'字符串'或'小数?'那样空。你期待的行为是什么? – Joao

+0

@Jota我会认为或必需属性将无法正常工作,因为'decimal'永远不会为null或Validate方法不会被调用,因为某些DataAnnotation验证失败。 – Diego

+1

您必须将“decimal”设置为可空,否则[必需]将不会触发。任何理由不这样做? – Joao

回答

14

考虑交换:

的协商一致和expected behavior among developersIValidatableObject“如果没有验证属性触发S分析Validate()只调用。总之,预期的算法是这样的(从以前的链接所):

  1. 验证属性级别的属性
  2. 如果任何验证无效,终止确认返回失败(或多个)
  3. 验证对象级属性
  4. 如果任何验证无效,中止验证返回失败(一个或多个)
  5. 如果在桌面上的框架和对象实现IValidatableObje CT,然后调用它的验证方法以及使用问题的代码返回任何失败(S)

然而,Validate被称为[Required]触发即使经过。这似乎是一个明显的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。这似乎是一个合理的选择,如果它是一个非常特殊的模型,并且不需要太多验证。在大多数情况下,如果使用属性,开发人员将执行所有常规验证和常见验证(即必需等),导致验证中已经实现的验证代码重复。也没有可重用性。

评论前回答:所有我已经创建了一个新的项目,从无到有,只有您提供的代码的

第一。它永远不会同时触发数据注释和验证方法。

反正知道这一点,

通过设计,MVC3增加了[Required]属性非空值类型,如intDateTime或者,是的,decimal。所以,即使你从decimal中删除了必要的属性,它的工作原理就像它在那里一样。

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

在你例如:

  • 'DataAnnotation' 触发器如果[必要]存在并且没有给定值。从我的角度完全可以理解
  • '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"); 
    } 
} 

您认为这种方法是错误的或者不应该这样?

+0

我同意你的看法,当你说它的权利隐含要求不可空的类型。我的问题(你似乎无法重现)是触发隐式(或显式)Required属性,然后调用Validate方法。我希望这个方法在某些DataAnnotations验证失败时不会被调用(不管它是隐式的还是显式的) – Diego

+0

只有一种场景可以重现这种行为(并且忘记* AddImplicitRequiredAttributeForValueTypes存在):当使用不可空在它上面没有[必需]的'decimal'。看起来,隐含的所需不被计为DataAnnotation触发器,并且调用Validate。 – Joao

+0

这是你的情况吗? – Joao