Asp.net

IValidatableObject 驗證方法在 DataAnnotations 失敗時觸發

  • April 6, 2017

我有一個 ViewModel,它有一些 DataAnnotations 驗證,然後為了更複雜的驗證實現 IValidatableObject 並使用 Validate 方法。

我期待的行為是這樣的:首先是所有 DataAnnotations,然後,只有在沒有錯誤的情況下,才使用 Validate 方法。我怎麼發現這並不總是正確的。我的 ViewModel(展示版)有三個文件 one string、 onedecimal和 one decimal?。所有三個屬性都只有Required 屬性。對於string和 ,decimal?行為是預期的,但對於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)
}

意見交流後的注意事項:

開發人員之間的共識和預期行為IValidatableObject’ 方法Validate()僅在沒有觸發驗證屬性時才被呼叫。簡而言之,預期的算法是這樣的(取自上一個連結):

  1. 驗證屬性級屬性
  2. 如果任何驗證器無效,則中止驗證返回失敗
  3. 驗證對象級屬性
  4. 如果任何驗證器無效,則中止驗證返回失敗
  5. 如果在桌面框架上並且對象實現了 IValidatableObject,則呼叫其 Validate 方法並返回任何失敗

但是,使用問題的程式碼,Validate即使在[Required]triggers之後也會被呼叫。這似乎是一個明顯的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);
   }));
  1. 忘記IValidatableObject並只使用屬性。它乾淨、直接、更好地處理本地化,最重要的是它可以在所有模型中重複使用。只需為您要執行的每個驗證實現ValidationAttribute 。您可以驗證所有模型或特定屬性,這取決於您。除了預設可用的屬性(DataType、Regex、Required 和所有這些東西)之外,還有幾個庫具有最常用的驗證。實現*“缺失”的一個*是FluentValidation
  2. 只實現IValidatableObject介面丟棄數據註釋。如果它是一個非常特殊的模型並且不需要太多驗證,這似乎是一個合理的選擇。在大多數情況下,如果使用了屬性,開發人員將執行所有正常和常見的驗證(即必需等),這會導致預設情況下已經實現的驗證程式碼重複。也沒有可重用性。

評論前回答:

首先,我只使用您提供的程式碼從頭開始創建了一個新項目。它從不同時觸發數據註釋和驗證方法。

無論如何,知道這一點,

按照設計,MVC3[Required]為不可為空的值類型添加了一個屬性,例如int,DateTime或者, yes, decimal。因此,即使您從中刪除了所需的屬性,decimal它的工作方式就像它在那裡一樣。

這對於它的錯誤(或不是)是有爭議的,但它的設計方式。

在你的例子中:

  • ‘DataAnnotation’ 觸發,如果$$ Required $$存在並且沒有給出值。從我的角度來看完全可以理解
  • ‘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");
   }
}

您認為這種方法有什麼問題或不應該這樣?

引用自:https://stackoverflow.com/questions/8153602