Как заставить MVC проверять объект IValidatableObject
Похоже, что когда MVC проверяет модель, которая сначала выполняется через атрибуты DataAnnotation (например, требуемый или диапазон), и если кто-либо из них не работает, он пропускает запуск метода Validate в моей модели IValidatableObject.
Можно ли запустить MVC и запустить этот метод, даже если другая проверка не выполнена?
Ответы
Ответ 1
Вы можете вручную вызвать Validate(), передав новый экземпляр ValidationContext, например:
[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);
}
}
Опасность этого подхода заключается в том, что в тех случаях, когда ошибки на уровне свойств (DataAnnotation) отсутствуют, проверка будет выполняться дважды. Чтобы этого избежать, вы можете добавить свойство в свою модель, скажем, логическое значение Validated, которое вы установили в true в вашем методе Validate() после его запуска, а затем проверите перед тем, как вручную вызвать метод в вашем контроллере.
Итак, в вашем контроллере:
if (!ModelState.IsValid) {
if (!model.Validated) {
var validationResults = model.Validate(new ValidationContext(model, null, null));
foreach (var error in validationResults)
foreach (var memberName in error.MemberNames)
ModelState.AddModelError(memberName, error.ErrorMessage);
}
return View(post);
}
И в вашей модели:
public bool Validated { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
// perform validation
Validated = true;
}
Ответ 2
Есть способ сделать это, не требуя кода шаблона в верхней части каждого действия контроллера.
Вам нужно заменить стандартное связующее устройство на свой собственный:
protected void Application_Start()
{
// ...
ModelBinderProviders.BinderProviders.Clear();
ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider());
// ...
}
Поставщик привязки вашей модели выглядит следующим образом:
public class CustomModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(Type modelType)
{
return new CustomModelBinder();
}
}
Теперь создайте настраиваемое связующее устройство, которое фактически заставляет проверять. Здесь тяжелый подъем:
public class CustomModelBinder : DefaultModelBinder
{
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
base.OnModelUpdated(controllerContext, bindingContext);
ForceModelValidation(bindingContext);
}
private static void ForceModelValidation(ModelBindingContext bindingContext)
{
var model = bindingContext.Model as IValidatableObject;
if (model == null) return;
var modelState = bindingContext.ModelState;
var errors = model.Validate(new ValidationContext(model, null, null));
foreach (var error in errors)
{
foreach (var memberName in error.MemberNames)
{
// Only add errors that haven't already been added.
// (This can happen if the model Validate(...) method is called more than once, which will happen when
// there are no property-level validation failures.)
var memberNameClone = memberName;
var idx = modelState.Keys.IndexOf(k => k == memberNameClone);
if (idx < 0) continue;
if (modelState.Values.ToArray()[idx].Errors.Any()) continue;
modelState.AddModelError(memberName, error.ErrorMessage);
}
}
}
}
Вам также понадобится метод расширения IndexOf. Это дешевая реализация, но она будет работать:
public static int IndexOf<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
if (source == null) throw new ArgumentNullException("source");
if (predicate == null) throw new ArgumentNullException("predicate");
var i = 0;
foreach (var item in source)
{
if (predicate(item)) return i;
i++;
}
return -1;
}