Jak zmusić MVC do walidacji IValidatableObject

Wygląda na to, że gdy MVC waliduje Model, najpierw uruchamia atrybuty DataAnnotation (takie jak wymagane lub zakres), a jeśli któryś z nich się nie powiedzie, pomija uruchamianie metody Validate na moim modelu IValidatableObject.

Czy istnieje sposób, aby MVC uruchomił tę metodę, nawet jeśli druga Walidacja nie powiedzie się?

Author: Mr Bell, 2011-06-21

2 answers

Możesz ręcznie wywołać validate (), przekazując nową instancję ValidationContext, tak:

[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);
    }
}

Zastrzeżeniem tego podejścia jest to, że w przypadkach, gdy nie ma błędów na poziomie właściwości (DataAnnotation), Walidacja zostanie uruchomiona dwukrotnie. Aby tego uniknąć, możesz dodać właściwość do modelu, powiedzmy zatwierdzoną logicznie, którą ustawisz na true w metodzie Validate() po uruchomieniu, a następnie sprawdzić przed ręcznym wywołaniem metody w kontrolerze.

Więc w Twoim Kontroler:

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);
}

I w twoim modelu:

public bool Validated { get; set; }

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
    // perform validation

    Validated = true;
}
 35
Author: gram,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-08-29 14:01:52

Jest sposób, aby to zrobić bez konieczności kodowania boilerplate u góry każdej akcji kontrolera.

Będziesz musiał wymienić domyślny segregator modelu na jeden z twoich własnych:

protected void Application_Start()
{
    // ...
    ModelBinderProviders.BinderProviders.Clear();
    ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider());
    // ...
}

Twój model dostawcy segregatora wygląda tak:

public class CustomModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(Type modelType)
    {
        return new CustomModelBinder();
    }
}

Teraz Utwórz niestandardowy segregator modelu, który wymusza walidację. W tym miejscu odbywa się podnoszenie ciężarów:

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's 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);
            }
        }
    }
}

Będziesz potrzebował również metody rozszerzenia IndexOf. Jest to tania implementacja, ale zadziała:

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;
}
 27
Author: uglybugger,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-09-23 03:39:47