IValidatableObject Проверка метода запуска при сбое DataAnnotations - PullRequest
8 голосов
/ 16 ноября 2011

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

Я ожидал, что поведение будет этим : сначала всеDataAnnotations, а затем, только если не было ошибок, метод Validate.Как только я узнаю, что это не всегда так.Моя ViewModel (демонстрационная версия) имеет три файла: один string, один decimal и один decimal?.Все три свойства имеют только обязательный атрибут.Для string и decimal? поведение является ожидаемым, но для decimal, когда пусто, Обязательная проверка завершается неудачно (пока что хорошо), а затем выполняет метод Validate.Если я проверяю свойство, его значение равно нулю.

Что здесь происходит?Что мне не хватает?

Примечание : я знаю, что обязательный атрибут должен проверять, является ли значение нулевым.Поэтому я ожидаю, что мне скажут не использовать атрибут 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");
    }
}

View:

@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 Ответ

14 голосов
/ 17 ноября 2011

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

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

  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;

Полагаю, имя свойства не требует пояснений.

В любом случае, я не понимаю, почему вы хотите, чтобы пользователь вводил что-то ненужное и не делал это свойство обнуляемым. Если это 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");
    }
}

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

...