Я хочу расширить возможности работы RequiredAttribute
с помощью строго типизированных моделей представлений. Например, у меня есть модель представления справочных данных, которая выглядит так:
public class ReferenceDataViewModel
{
public int? Id { get; set; }
public string? Name { get; set; }
}
Это можно использовать на других моделях представления, например:
public class MyEditViewModel
{
// Optional: this property can be null or this property's Id value can be null
public ReferenceViewModel? OptionalRef { get; set; }
// Required: this property should not be null nor should this property's Id value
[Required]
public ReferenceViewModel? RequiredRef { get; set; }
}
С RequiredRef
Само свойство не должно быть null
(которое обрабатывается RequiredAttribute
), но также Id
не должно быть нулевым.
Я знаю, что могу добиться этого, создав свой собственный ValidationAttribute
(см. ниже), но для единообразия я бы хотел использовать встроенный RequiredAttribute
.
public class ExtendedRequiredAttribute : RequiredAttribute
{
public ExtendedRequiredAttribute() { }
public ExtendedRequiredAttribute(RequiredAttribute attributeToCopy)
{
AllowEmptyStrings = attributeToCopy.AllowEmptyStrings;
if (attributeToCopy.ErrorMessage != null)
{
ErrorMessage = attributeToCopy.ErrorMessage;
}
if (attributeToCopy.ErrorMessageResourceType != null)
{
ErrorMessageResourceName = attributeToCopy.ErrorMessageResourceName;
ErrorMessageResourceType = attributeToCopy.ErrorMessageResourceType;
}
}
public override bool IsValid(object? value)
{
if (value is ReferenceDataViewModel entityReference)
{
return entityReference?.Id != null;
}
return base.IsValid(value);
}
}
Я пробовал использовать AttributeAdapter
(который, кажется, работает в ASP.NET MVC), но это не помогает. Такое ощущение, что я что-то упускаю:
public class ExtendedRequiredAttributeAdapter : AttributeAdapterBase<RequiredAttribute>
{
public ExtendedRequiredAttributeAdapter(ExtendedRequiredAttribute attribute, IStringLocalizer? stringLocalizer)
: base(attribute, stringLocalizer)
{ }
public override void AddValidation(ClientModelValidationContext context)
{
MergeAttribute(context.Attributes, "data-val", "true");
MergeAttribute(context.Attributes, "data-val-required", GetErrorMessage(context));
}
public override string GetErrorMessage(ModelValidationContextBase validationContext)
{
ArgumentNullException.ThrowIfNull(validationContext);
return GetErrorMessage(validationContext.ModelMetadata, validationContext.ModelMetadata.GetDisplayName());
}
}
public class CustomValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
{
private readonly IValidationAttributeAdapterProvider _innerProvider = new ValidationAttributeAdapterProvider();
public IAttributeAdapter? GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer? stringLocalizer)
{
var type = attribute.GetType();
if (type == typeof(RequiredAttribute))
{
var requiredAttribute = (RequiredAttribute)attribute;
var extendedRequiredAttrib = new ExtendedRequiredAttribute(requiredAttribute);
return new ExtendedRequiredAttributeAdapter(extendedRequiredAttrib, stringLocalizer);
}
return _innerProvider.GetAttributeAdapter(attribute, stringLocalizer);
}
}
Это регистрируется при запуске:
services.AddSingleton<IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();
К сожалению, это не работает, поскольку это требуется не во всех случаях использования. (См. свойство «OptionalRef» в MyEditViewModel)
Вы хотите сказать, что дляOptionalRef это нормально, что, когда он сам не равен нулю, идентификатор может быть там нулевым? Странная концепция.
Странно? Не совсем. Когда форма отправляется в MVC, она может содержать пустые значения идентификаторов. Они преобразуются в нулевые, поэтому MyEditViewModel может содержать ненулевое значение дляOptionalRef, но иметь нулевое значение дляOptionalRef.Id.
Как насчет создания модели с двумя представлениями: одна для нулевого значения, другая для ненулевого? Боюсь, это будет яснее...
Я об этом подумал, но это кажется немного неуклюжим и неуклюжим. Я бы предпочел использовать собственный атрибут ExtendedRequired.
В некотором смысле я согласен с вами, я также чувствую себя немного неуклюжим и неуклюжим, но элегантный способ, который вы предпочитаете, приведет к гораздо большему количеству логических кодов и может затруднить поддержку классов... Неуклюжий способ может значительно сократить время наоборот. Посмотрим, поделится ли здесь кто-нибудь еще своей мудростью.
Я решил эту проблему, выбрав немного другой путь. По сути, я использовал инфраструктуру проверки ядра MVC, создав IModelValidator
, а затем задействовав его через IModelValidatorProvider
, а затем зарегистрировав этого провайдера в MvcOptions
. Вот код:
public class ReferenceDataViewModelRequiredValidator : IModelValidator
{
public IEnumerable<ModelValidationResult> Validate(ModelValidationContext context)
{
var model = context.Model as ReferenceDataViewModel;
if (model == null || model.Id.HasValue == false)
{
var message = GetErrorMessage(context.ModelMetadata);
yield return new ModelValidationResult("", message);
}
}
private string GetErrorMessage(ModelMetadata metadata)
{
string? message = null;
var parent = metadata.ContainerType;
var modelName = metadata.PropertyName;
if (parent != null && !string.IsNullOrWhiteSpace(modelName))
{
var modelProperty = parent.GetProperty(modelName);
var requiredAttrib = modelProperty?.GetCustomAttribute<RequiredAttribute>();
if (requiredAttrib != null)
{
message = requiredAttrib.ErrorMessage;
if (string.IsNullOrWhiteSpace(message))
{
message = requiredAttrib.FormatErrorMessage(modelName);
}
}
}
if (string.IsNullOrWhiteSpace(message))
{
message = $"{metadata.GetDisplayName()} is required.";
}
return message;
}
}
Это можно интегрировать в проверку MVC через ModelValidatorProvider
:
public class MyModelValidatorProvider : IModelValidatorProvider
{
public void CreateValidators(ModelValidatorProviderContext context)
{
if (HasRequiredAttribute(context.ModelMetadata))
{
if (context.ModelMetadata.ModelType == typeof(ReferenceDataViewModel))
{
var validatorItem = new ValidatorItem()
{
IsReusable = true,
Validator = new ReferenceDataViewModelRequiredValidator()
};
// put the validator in early
context.Results.Insert(0, validatorItem);
}
}
/// <summary>
/// Determines if the property being validated has a [Required] attribute
/// </summary>
/// <remarks>
/// There is an IsRequired property on the <see cref = "ModelMetadata"/>, but always seems to be "true".
/// Not sure why.
/// </remarks>
private static bool HasRequiredAttribute(ModelMetadata metadata)
{
if (metadata.ContainerType != null && !string.IsNullOrWhiteSpace(metadata.PropertyName))
{
var property = metadata.ContainerType.GetProperty(metadata.PropertyName);
return property?.HasAttribute<RequiredAttribute>() ?? false;
}
return false;
}
}
ModelValidatorProvider
необходимо зарегистрировать при запуске:
builder
.AddControllersWithViews(opts =>
{
opt.ModelValidatorProviders.Add(new MyModelValidatorProvider())
});
Используйте RequiredAttribute также в ReferenceDataViewModel.Id?