Ответ 1
Вам нужно будет создать свой собственный атрибут проверки (например, [CompositeField]
), который проверяет дочерние свойства.
Могу ли я автоматически проверять сложные дочерние объекты при проверке родительского объекта и включать результаты в заполненный ICollection<ValidationResult>
?
Если я запустил следующий код:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ConsoleApplication1
{
public class Person
{
[Required]
public string Name { get; set; }
public Address Address { get; set; }
}
public class Address
{
[Required]
public string Street { get; set; }
[Required]
public string City { get; set; }
[Required]
public string State { get; set; }
}
class Program
{
static void Main(string[] args)
{
Person person = new Person
{
Name = null,
Address = new Address
{
Street = "123 Any St",
City = "New York",
State = null
}
};
var validationContext = new ValidationContext(person, null, null);
var validationResults = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(person, validationContext, validationResults);
Console.WriteLine(isValid);
validationResults.ForEach(r => Console.WriteLine(r.ErrorMessage));
Console.ReadKey(true);
}
}
}
Я получаю следующий вывод:
False
The Name field is required.
Но я ожидал чего-то подобного:
False
The Name field is required.
The State field is required.
Я предложил щедрость для лучшего решения для проверки дочерних объектов, но не получил ни одного получателя, в идеале
Я все еще удивлен, что фреймворк не поддерживает это.
Вам нужно будет создать свой собственный атрибут проверки (например, [CompositeField]
), который проверяет дочерние свойства.
Это, к сожалению, стандартное поведение Validator.TryValidateObject
, которое
не рекурсивно проверяет значения свойств объекта
Как указано в статье Джеффа Хандли о Проверка объекта и свойств с помощью Validator, по умолчанию валидатор будет проверять в порядке:
IValidatableObject
Проблема в том, что на каждом шагу пути...
Если какие-либо валидаторы недействительны,
Validator.ValidateObject
отменяет проверку и возвращает сбой (-ы)
Другая возможная проблема заключается в том, что привязка модели будет выполнять проверку только на объектах, которые она решила связать. Например, если вы не предоставляете входы для полей в сложных типах вашей модели, связующее устройство модели не обязательно будет проверять эти свойства вообще, потому что оно не вызвало конструктор на этих объектах. Согласно Брэду Уилсону, хорошая статья о Проверка ввода в сравнении с проверкой модели в ASP.NET MVC:
Причина, по которой мы не "погружаемся" в объект Address рекурсивно, состоит в том, что в форме не было ничего, что связывало любые значения внутри.
Одним из способов решения этой проблемы является преобразование проверки уровня объекта на проверку уровня свойства путем добавления специального атрибута проверки к свойству, которое вернется с результатом проверки самого объекта.
Статья Джоша Кэрролла о Рекурсивная проверка с использованием DataAnnotations обеспечивает реализацию одной из таких стратегий (первоначально в этот вопрос SO). Если мы хотим проверить сложный тип (например, "Адрес" ), мы можем добавить собственный атрибут ValidateObject
к свойству, поэтому он оценивается на первом шаге
public class Person {
[Required]
public String Name { get; set; }
[Required, ValidateObject]
public Address Address { get; set; }
}
Вам нужно добавить следующую реализацию ValidateObjectAttribute:
public class ValidateObjectAttribute: ValidationAttribute {
protected override ValidationResult IsValid(object value, ValidationContext validationContext) {
var results = new List<ValidationResult>();
var context = new ValidationContext(value, null, null);
Validator.TryValidateObject(value, context, results, true);
if (results.Count != 0) {
var compositeResults = new CompositeValidationResult(String.Format("Validation for {0} failed!", validationContext.DisplayName));
results.ForEach(compositeResults.AddResult);
return compositeResults;
}
return ValidationResult.Success;
}
}
public class CompositeValidationResult: ValidationResult {
private readonly List<ValidationResult> _results = new List<ValidationResult>();
public IEnumerable<ValidationResult> Results {
get {
return _results;
}
}
public CompositeValidationResult(string errorMessage) : base(errorMessage) {}
public CompositeValidationResult(string errorMessage, IEnumerable<string> memberNames) : base(errorMessage, memberNames) {}
protected CompositeValidationResult(ValidationResult validationResult) : base(validationResult) {}
public void AddResult(ValidationResult validationResult) {
_results.Add(validationResult);
}
}
Для объектов, которые реализуют IValidatableObject
, когда мы проверяем ModelState, мы также можем проверить, действительно ли сама модель верна, прежде чем возвращать список ошибок. Мы можем добавить любые ошибки, которые мы хотим, вызывая ModelState.AddModelError(field, error)
. Как указано в Как заставить MVC проверять объект IValidatableObject, мы можем сделать это следующим образом:
[HttpPost]
public ActionResult Create(Model model) {
if (!ModelState.IsValid) {
var errors = model.Validate(new ValidationContext(model, null, null));
foreach (var error in errors)
foreach (var memberName in error.MemberNames)
ModelState.AddModelError(memberName, error.ErrorMessage);
return View(post);
}
}
Также, если вы хотите более элегантное решение, вы можете написать код один раз, предоставив свою собственную реализацию привязки модели в Application_Start() с помощью
ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider());
. Существуют хорошие варианты здесь и здесь
Я также столкнулся с этим и нашел этот поток. Здесь первый проход:
namespace Foo
{
using System.ComponentModel.DataAnnotations;
using System.Linq;
/// <summary>
/// Attribute class used to validate child properties.
/// </summary>
/// <remarks>
/// See: http://stackoverflow.com/info/2493800/how-can-i-tell-the-data-annotations-validator-to-also-validate-complex-child-pro
/// Apparently the Data Annotations validator does not validate complex child properties.
/// To do so, slap this attribute on a your property (probably a nested view model)
/// whose type has validation attributes on its properties.
/// This will validate until a nested <see cref="System.ComponentModel.DataAnnotations.ValidationAttribute" />
/// fails. The failed validation result will be returned. In other words, it will fail one at a time.
/// </remarks>
public class HasNestedValidationAttribute : ValidationAttribute
{
/// <summary>
/// Validates the specified value with respect to the current validation attribute.
/// </summary>
/// <param name="value">The value to validate.</param>
/// <param name="validationContext">The context information about the validation operation.</param>
/// <returns>
/// An instance of the <see cref="T:System.ComponentModel.DataAnnotations.ValidationResult"/> class.
/// </returns>
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var isValid = true;
var result = ValidationResult.Success;
var nestedValidationProperties = value.GetType().GetProperties()
.Where(p => IsDefined(p, typeof(ValidationAttribute)))
.OrderBy(p => p.Name);//Not the best order, but at least known and repeatable.
foreach (var property in nestedValidationProperties)
{
var validators = GetCustomAttributes(property, typeof(ValidationAttribute)) as ValidationAttribute[];
if (validators == null || validators.Length == 0) continue;
foreach (var validator in validators)
{
var propertyValue = property.GetValue(value, null);
result = validator.GetValidationResult(propertyValue, new ValidationContext(value, null, null));
if (result == ValidationResult.Success) continue;
isValid = false;
break;
}
if (!isValid)
{
break;
}
}
return result;
}
}
}