IValidatableObject Проверка метода запуска при сбое DataAnnotations

Я ViewModel, который имеет некоторые проверки DataAnnotations, а затем для более сложных проверок реализует IValidatableObject и использует метод Validate.

Я ожидал такого поведения: сначала все DataAnnotations, а затем, только при отсутствии ошибок, метод Validate. Как только я узнаю, что это не всегда так. Моя ViewModel (демо) имеет три поля один string, один decimal и один decimal?, Все три свойства имеют только обязательный атрибут. Для 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
}

ViewModel:

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 ответ

Решение

Соображения после обмена комментариями:

Согласованное и ожидаемое поведение разработчиков IValidatableObject метод Validate() вызывается только в том случае, если атрибуты проверки не запущены. Короче говоря, ожидаемый алгоритм таков (взят из предыдущей ссылки):

  1. Проверьте атрибуты уровня свойства
  2. Если какие-либо валидаторы недействительны, прервите валидацию, возвращая ошибки
  3. Проверьте атрибуты уровня объекта
  4. Если какие-либо валидаторы недействительны, прервите валидацию, возвращая ошибки
  5. Если на платформе рабочего стола и объект реализует IValidatableObject, то вызовите его метод Validate и верните любые ошибки

Однако, используя код вопроса, Validate называется даже после [Required] триггеры Это кажется очевидной ошибкой 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);
        }));
    
  2. забывать IValidatableObject и использовать только атрибуты. Он чистый, прямой, лучше справляется с локализацией и, что лучше всего, его можно использовать повторно среди всех моделей. Просто внедрите ValidationAttribute для каждой проверки, которую вы хотите выполнить. Вы можете проверить все модели или конкретные свойства, которые вам решать. Помимо атрибутов, доступных по умолчанию (DataType, Regex, Required и все такое), есть несколько библиотек с наиболее часто используемыми проверками. Тот, который реализует "отсутствующие", - это FluentValidation.

  3. Внедрить только IValidatableObject интерфейс, выбрасывающий аннотации данных. Это кажется разумным вариантом, если это очень специфическая модель, и она не требует большой проверки. В большинстве случаев разработчик будет выполнять все эти регулярные и обычные проверки (т. Е. Обязательные и т. Д.), Что приводит к дублированию кода в проверках, уже выполненных по умолчанию, если использовались атрибуты. Там также нет повторного использования.

Ответ перед комментариями:

Прежде всего, я создал новый проект с нуля только с предоставленным вами кодом. Это НИКОГДА не вызывало одновременно аннотации данных и метод проверки.

В любом случае, знай это,

По замыслу MVC3 добавляет [Required] приписывать необнуляемым типам значений, например int, DateTime или да, decimal, Таким образом, даже если вы удалите обязательный атрибут из этого decimal это работает так же, как это там.

Это спорно для его неправильности (или нет), но его так, как это предназначено.

В вашем примере:

  • "DataAnnotation" срабатывает, если [Обязательный] присутствует и значение не указано. Абсолютно понятно с моей точки зрения
  • DataAnnotation срабатывает, если нет [Обязательный], но значение не обнуляется. Спорно, но я склонен согласиться с ним, потому что, если свойство не обнуляемым, значение должно быть введено, в противном случае не показать его пользователю или просто использовать обнуляемого decimal,

Такое поведение, как кажется, может быть отключено с помощью вашего метода Application_Start:

DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;

Я предполагаю, что название объекта говорит само за себя.

В любом случае, я не понимаю, почему вы хотите, чтобы пользователь вводил что-то ненужное и не делал это свойство обнуляемым. Если он нулевой, тогда ваша задача - проверить его, если вы не хотите, чтобы он был нулевым, до проверки в контроллере.

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

Как вы думаете, что это неправильно в этом подходе или не должно быть так?

Другие вопросы по тегам