我正在尝试在ASP.NET Core MVC Web应用程序中实现我的自定义验证属性,包括服务器和客户端验证。现有数据注释的缺点是属性的静态性质,这使得无法将运行时数据传递给它。
我想制作GreaterThanAttribute,它将另一个属性的名称作为参数,以将给定值与该属性的值进行比较。像这样:
public class TestModel
{
public int PropertyA { get; set; }
[GreaterThan(nameof(PropertyA))]
public int PropertyB { get; set; }
}
我已经通过以下方式实现了该属性:
[AttributeUsage(AttributeTargets.Property)]
public class GreaterThanAttribute : ValidationAttribute, IClientModelValidator
{
public const string RuleName = "greaterthan";
public const string ParameterName = "othervalue";
public object OtherPropertyValue { get; set; }
public string OtherPropertyName { get; }
public GreaterThanAttribute(string otherPropertyName)
{
OtherPropertyName = otherPropertyName;
ErrorMessage = $"The value should be greater than filed {OtherPropertyName}";
}
public void AddValidation(ClientModelValidationContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
MergeAttribute(context.Attributes, "data-val", "true");
MergeAttribute(context.Attributes, $"data-val-{RuleName}", ErrorMessageString);
MergeAttribute(context.Attributes, $"data-val-{RuleName}-{ParameterName}", OtherPropertyValue?.ToString());
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
// server side validation, no difficulties here
}
private static void MergeAttribute(IDictionary<string, string> attributes, string key, string value)
{
if (attributes.ContainsKey(key))
return;
attributes.Add(key, value);
}
}
我在视图中为此属性添加了醒目的验证器:
<script type="text/javascript">
$.validator.addMethod("@GreaterThanAttribute.RuleName", function (value, element, params) {
var parsedThisValue = Globalize.numberParser()(value);
var parsedOtherValue = Globalize.numberParser()(params);
return parsedThisValue > parsedOtherValue;
});
$.validator.unobtrusive.adapters.addSingleVal("@GreaterThanAttribute.RuleName", "@GreaterThanAttribute.ParameterName");
</script>
问题是,现在我需要手动将data-val-greaterthan-othervalue
属性添加到输入字段,例如:
@{
var propertyName = $"data-val-{GreaterThanAttribute.RuleName}-{GreaterThanAttribute.ParameterName}";
var propertyValue = Model.Child[0].PropertyB;
}
@Html.TextBoxFor(x => Model.Child[0].PropertyA, new Dictionary<string, object> {{propertyName, propertyValue}})
@Html.ValidationMessageFor(x => Model.Child[0].PropertyA)
我不喜欢那样。因此,我正在寻找一种使用现有ASP.NET机制添加此属性而又不会污染其视图的方法。 与我在答案Access model data in AddValidation method asp.net core custom validation中找到的最接近的方式。
现在,我正在尝试将实际值注入到我的自定义验证html属性提供程序中的属性中:
public class CustomValidationHtmlAttributeProvider : DefaultValidationHtmlAttributeProvider
{
private readonly IOptions<MvcViewOptions> _optionsAccessor;
private readonly IModelMetadataProvider _metadataProvider;
private readonly ClientValidatorCache _clientValidatorCache;
public CustomValidationHtmlAttributeProvider(IOptions<MvcViewOptions> optionsAccessor, IModelMetadataProvider metadataProvider, ClientValidatorCache clientValidatorCache)
: base(optionsAccessor, metadataProvider, clientValidatorCache)
{
_optionsAccessor = optionsAccessor;
_metadataProvider = metadataProvider;
_clientValidatorCache = clientValidatorCache;
}
public override void AddValidationAttributes(ViewContext viewContext, ModelExplorer modelExplorer, IDictionary<string, string> attributes)
{
// getting existing validation attribute
var greaterThanAttribute = modelExplorer.Metadata.ValidatorMetadata.FirstOrDefault(x =>
x.GetType() == typeof(GreaterThanAttribute)) as GreaterThanAttribute;
var otherPropertyName = greaterThanAttribute.OtherPropertyName;
// -------------
// how to get reference to parent object of the model here?
// -------------
var otherValue = ?????????????
greaterThanAttribute.OtherPropertyValue = otherValue;
base.AddValidationAttributes(viewContext, modelExplorer, attributes);
}
}
问题是我找不到一种方法来获取对正在验证的属性的父类的引用,以获取PropertyB
的值。我这里只有:
modelExplorer.Model
指向PropertyA的值,符合预期modelExplorer.Container
指向一个视图的整个模型的值,该视图很复杂,可能具有多个层次结构和列表。因此,基本上我需要立即获取Model.Child[0].PropertyA
的值,但我事先并不知道确切的路径。当然,我不知道当前Child的索引,等等。modelExplorer.Metadata
拥有整个容器的当前属性的所有元数据,但是我看不到将此元数据连接到实际Model和值的方法。所以问题是:考虑到我不了解容器的整个层次结构,如何在这里达到PropertyB
的值?也许还有其他完整的方法可以实现所需的验证属性?
答案 0 :(得分:0)
我终于找到了解决方案。
首先,我已经重载了错误版本的方法。正确的是
void AddAndTrackValidationAttributes(ViewContext viewContext, ModelExplorer modelExplorer, string expression, IDictionary<string, string> attributes)
,因为它为具有验证属性的当前属性提供了表达式字符串。
使用此表达式,我们可以假设,名称存储在otherPropertyName
中的目标属性存在于同一路径上(因为它位于同一类中)。例如,如果expression是Model.Child[0].PropertyA
,则可以使用表达式Model.Child[0].PropertyB
检索目标属性。
不幸的是,ExpressionMetadataProvider.FromStringExpression
函数不适用于使用索引器进行表达。但是,它仅适用于属性表达式。就我而言,唯一的方法是手动遍历对象层次结构:解析表达式并使用反射进入给定索引处的属性和元素。
整个代码:
public class CustomValidationHtmlAttributeProvider : DefaultValidationHtmlAttributeProvider
{
private readonly IOptions<MvcViewOptions> _optionsAccessor;
private readonly IModelMetadataProvider _metadataProvider;
private readonly ClientValidatorCache _clientValidatorCache;
public CustomValidationHtmlAttributeProvider(IOptions<MvcViewOptions> optionsAccessor, IModelMetadataProvider metadataProvider, ClientValidatorCache clientValidatorCache)
: base(optionsAccessor, metadataProvider, clientValidatorCache)
{
_optionsAccessor = optionsAccessor;
_metadataProvider = metadataProvider;
_clientValidatorCache = clientValidatorCache;
}
public override void AddValidationAttributes(ViewContext viewContext, ModelExplorer modelExplorer, string expression, IDictionary<string, string> attributes)
{
// getting existing validation attribute
var greaterThanAttribute = modelExplorer.Metadata.ValidatorMetadata.FirstOrDefault(x =>
x.GetType() == typeof(GreaterThanAttribute)) as GreaterThanAttribute;
var otherPropertyName = greaterThanAttribute.OtherPropertyName;
var targetExpression = GetTargetPropertyExpression(expression, otherPropertyName);
var otherValue = GetValueOnExpression(modelExplorer.Container.Model, targetExpression);
greaterThanAttribute.OtherPropertyValue = otherValue;
base.AddValidationAttributes(viewContext, modelExplorer, attributes);
}
private static object GetValueOnExpression(object container, string expression)
{
while (expression != "")
{
if (NextStatementIsIndexer(expression))
{
var index = GetIndex(expression);
switch (container)
{
case IDictionary dictionary:
container = dictionary[index];
break;
case IEnumerable<object> enumerable:
container = enumerable.ElementAt(int.Parse(index));
break;
default:
throw new Exception($"{container} is unknown collection type");
}
expression = ClearIndexerStatement(expression);
}
else
{
var propertyName = GetPropertyStatement(expression);
var propertyInfo = container.GetType().GetProperty(propertyName);
if (propertyInfo == null)
throw new Exception($"Can't find {propertyName} property in the container {container}");
container = propertyInfo.GetValue(container);
expression = ClearPropertyStatement(expression);
}
}
return container;
}
private static bool NextStatementIsIndexer(string expression)
=> expression[0] == '[';
private static string ClearPropertyStatement(string expression)
{
var statementEndPosition = expression.IndexOfAny(new [] {'.', '['});
if (statementEndPosition == -1) return "";
if (expression[statementEndPosition] == '.') statementEndPosition++;
return expression.Substring(statementEndPosition);
}
private static string GetPropertyStatement(string expression)
{
var statementEndPosition = expression.IndexOfAny(new [] {'.', '['});
if (statementEndPosition == -1) return expression;
return expression.Substring(0, statementEndPosition);
}
private static string ClearIndexerStatement(string expression)
{
var statementEndPosition = expression.IndexOf(']');
if (statementEndPosition == expression.Length - 1) return "";
if (expression[statementEndPosition + 1] == '.') statementEndPosition++;
return expression.Substring(statementEndPosition + 1);
}
private static string GetIndex(string expression)
{
var closeBracketPosition = expression.IndexOf(']');
return expression.Substring(1, closeBracketPosition - 1);
}
private static string GetTargetPropertyExpression(string sourceExpression, string targetProperty)
{
var memberAccessTokenPosition = sourceExpression.LastIndexOf('.');
if (memberAccessTokenPosition == -1) // expression is just a property name
return targetProperty;
var newExpression = sourceExpression.Substring(0, memberAccessTokenPosition + 1) + targetProperty;
return newExpression;
}
}