Pull to refresh

ASP.NET MVC: Условная валидация на клиенте с использованием FluentValidation

Reading time3 min
Views15K
Передо мной стояла задача сделать условную валидацию для свойства модели в зависимости от значения другого свойства. Гугл говорит, что такая задача встречается довольно часто. Поэтому я решил поделиться тем, как я решил ее в своем проекте.

Самый простым вариантом было бы написать атрибут, который через рефлексию проверял бы значение другого свойства и дело в шляпе. Но такое значение не было универсальным, т.к. даже если задавать имя свойства, которое нужно проверить, то все равно тип другого свойства может отличаться от модели к модели.

После некоторого времени проведенного в поиске красивого решения мне попалась библиотека FluentValidation. Прелесть ее в том, что с ее помощью можно легко задавать проверки для модели практически любой сложности, а если возможностей не хватает — можно написать свой валидатор и легко его назначить для нужного свойства. На сайте проекта есть очень хорошая документация. Вот небольшой пример как это работет:

[Validator(typeof(MyModelValidator))]
public class MyModel
{
  public string Name { get; set; }
  public string Description { get; set; }
  public bool IsDescriptionRequired { get; set; }
}

public class MyModelValidator: AbstractValidator<MyModel>
{
  public MyModelValidator()
  {
    RuleFor(m => m.Name).NotEmpty();
    RuleFor(m => m.Description).NotEmpty().When(m => m.IsDescriptionRequired);
  }
}


Для простых условий типа Заполнено, Минимальная длина, Максимальная длина и т.п. валидация на клиенте реализована из-коробки. Для реализации более сложных проверок (в моем случае условной) нужно реализовать свой валидатор свойства и реализовать интерфейс IClientValidatable, о котором есть довольно много инфы.

Основная идея в том, что нужно реализовать метод, который возвращает набор данных для jQuery.validate.

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

using GetClientValidationRulesFunc = Func<ModelMetadata, ControllerContext, IEnumerable<ModelClientValidationRule>>;

  class ClientValidator : PropertyValidator, IClientValidatable
  {
    private readonly GetClientValidationRulesFunc _getClientValidationRulesFunc;

    public ClientValidator(GetClientValidationRulesFunc getClientValidationRulesFunc) : base((string)null)
    {
      _getClientValidationRulesFunc = getClientValidationRulesFunc;
    }

    protected override bool IsValid(PropertyValidatorContext context)
    {
      // Suppress any server side validation
      return true;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
      return _getClientValidationRulesFunc(metadata, context);
    }
  }


И использовать в его можно примерно так:

public class MyModelValidator: AbstractValidator<MyModel>
{
  public MyModelValidator()
  {
    RuleFor(m => m.Name).NotEmpty();
    RuleFor(m => m.Description).NotEmpty().When(m => m.IsDescriptionRequired).SetValidator(new ClientValidator(GetValidationRules));
  }

  public IEnumerable<ModelClientValidationRule> GetValidationRules(ModelMetadata metadata,
    ControllerContext context)
  {
    yield return new ModelClientValidationRule
      {
        ErrorMessage = "Description required",
        ValidationType = "validateDescription"
      };
  }
}


Таким образом можно сохранить гибкость библиотеки и отдельно задать правила для клиентской валидации. Еще стоит сказать что для того, чтобы на клиенте валидация тоже выполнялась нужно создать свою функции и адаптер для нее:

$.validator.unobtrusive.adapters.addBool("validateDescription");

$.validator.addMethod("validateDescription", function (value, element, param) { 
  if ( $("#IsDescriptionRequired").val() === "true" ) {
    return $.trim($("#Description").val()).length > 0;
  }
  return true;
});


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

Загрузить пакет можно с помощью NuGet: FluentValidation.Mvc3 с зависимостью FluentValidation

UPD:
Для того чтобы валидация работала при байндинге модели в global.asax нужно зарегистрировать провайдер валидаторов:

protected void Application_Start(Object sender, EventArgs e)
{
    FluentValidationModelValidatorProvider.Configure();
}


А к классу, для которого создается валидатор нужно добавить атрибут, который находится в пространстве имен FluentValidation.Attributes:

[FluentValidation.Attributes.Validator(typeof(MyModelValidator))]
public class MyModel
Tags:
Hubs:
Total votes 12: ↑9 and ↓3+6
Comments24

Articles