MVC HtmlHelper vs FluentValidation 3.1: проблемы с получением ModelMetadata IsRequired
Я создал HtmlHelper для метки, который ставит звезду после имени этой метки, если требуется соответствующее поле:
public static MvcHtmlString LabelForR<TModel, TValue>(
this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
return LabelHelper(
html,
ModelMetadata.FromLambdaExpression(expression, html.ViewData),
ExpressionHelper.GetExpressionText(expression),
null);
}
private static MvcHtmlString LabelHelper(HtmlHelper helper, ModelMetadata metadata, string htmlFieldName, string text)
{
... //check metadata.IsRequired here
... // if Required show the star
}
Если я использую DataAnnotations и шлепаю [Required] для свойства в моей ViewModel, metadata.IsRequired в моем личном LabelHelper будет равно True, и все будет работать, как задумано.
Однако, если я использую FluentValidation 3.1 и добавлю простое правило, подобное этому:
public class CheckEmailViewModelValidator : AbstractValidator<CheckEmailViewModel>
{
public CheckEmailViewModelValidator()
{
RuleFor(m => m.Email)
.NotNull()
.EmailAddress();
}
}
... в моих метаданных LabelHelper.IsRequired будет неправильно установлено значение false. (Валидатор работает, хотя: вы не можете отправить пустое поле, и оно должно быть похоже на адрес электронной почты).
Остальные метаданные выглядят корректно (например, metadata.DisplayName = "Email").
Теоретически FluentValidator накладывает атрибут RequiredAttribute на свойство, если используется Rule .NotNull().
Для справок: Моя ViewModel:
[Validator(typeof(CheckEmailViewModelValidator))]
public class CheckEmailViewModel
{
//[Required]
[Display(Name = "Email")]
public string Email { get; set; }
}
Мой контроллер:
public class MemberController : Controller
{
[HttpGet]
public ActionResult CheckEmail()
{
var model = new CheckEmailViewModel();
return View(model);
}
}
Любая помощь приветствуется.
2 ответа
По умолчанию MVC использует атрибуты DataAnnotations для двух отдельных целей - метаданных и проверки.
Когда вы включаете FluentValidation в приложении MVC, FluentValidation подключается к инфраструктуре проверки, но не к метаданным - MVC будет продолжать использовать атрибуты для метаданных. Если вы хотите использовать FluentValidation для метаданных, а также для проверки, вам нужно написать собственную реализацию MVC ModelMetadataProvider, которая знает, как запрашивать классы валидатора - это не то, что FluentValidation поддерживает из коробки.
У меня есть пользовательский ModelMetadataProvider, который расширяет DataAnnotations по умолчанию, давая следующее:
- заполняет "DisplayName" из строки разделения имени свойства из Camel Case, если ни один не указан через DisplayAttribute.
- Если для ModelMetadata.IsRequired установлено значение false, он проверяет, существуют ли какие-либо текущие правила проверки (типа NotNull или NotEmpty).
Я определенно проверил исходный код, который подготовил Джереми, но я не был готов к полному пересмотру, поэтому я смешал и сопоставил, чтобы не потерять поведение по умолчанию. Вы можете найти это здесь
Вот код с некоторыми дополнительными преимуществами, взятыми из этого поста.
public class CustomModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
readonly IValidatorFactory factory;
public CustomModelMetadataProvider(IValidatorFactory factory)
: base() {
this.factory = factory;
}
// Uppercase followed by lowercase but not on existing word boundary (eg. the start)
Regex _camelCaseRegex = new Regex(@"\B\p{Lu}\p{Ll}", RegexOptions.Compiled);
// Creates a nice DisplayName from the model’s property name if one hasn't been specified
protected override ModelMetadata GetMetadataForProperty(
Func<object> modelAccessor,
Type containerType,
PropertyDescriptor propertyDescriptor) {
ModelMetadata metadata = base.GetMetadataForProperty(modelAccessor, containerType, propertyDescriptor);
metadata.IsRequired = metadata.IsRequired || IsNotEmpty(containerType, propertyDescriptor.Name);
if (metadata.DisplayName == null)
metadata.DisplayName = displayNameFromCamelCase(metadata.GetDisplayName());
if (string.IsNullOrWhiteSpace(metadata.DisplayFormatString) &&
(propertyDescriptor.PropertyType == typeof(DateTime) || propertyDescriptor.PropertyType == typeof(DateTime?))) {
metadata.DisplayFormatString = "{0:d}";
}
return metadata;
}
string displayNameFromCamelCase(string name) {
name = _camelCaseRegex.Replace(name, " $0");
if (name.EndsWith(" Id"))
name = name.Substring(0, name.Length - 3);
return name;
}
bool IsNotEmpty(Type type, string name) {
bool notEmpty = false;
var validator = factory.GetValidator(type);
if (validator == null)
return false;
IEnumerable<IPropertyValidator> validators = validator.CreateDescriptor().GetValidatorsForMember(name);
notEmpty = validators.OfType<INotNullValidator>().Cast<IPropertyValidator>()
.Concat(validators.OfType<INotEmptyValidator>().Cast<IPropertyValidator>()).Count() > 0;
return notEmpty;
}
}