如何在 ValidationHtmlAttributeProvider 中的 AddValidationAttributes 期间获取 parent object?
How to get parent object during AddValidationAttributes in ValidationHtmlAttributeProvider?
我正在尝试在 ASP.NET 核心 MVC 网络应用程序中实现我的自定义验证属性,包括服务器端和客户端验证。现有数据注释的缺点是属性的静态性质,这使得无法将运行时数据传递给它。
我想创建 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 机制添加此属性而不会污染视图的方法。
我在答案 .
中找到的最接近的方法
现在我正在尝试将实际值注入我的自定义验证中的属性 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);
}
}
问题是我无法找到一种方法来获取对正在验证的 属性 的 parent class 的引用以获得 PropertyB
的值.我这里只有:
modelExplorer.Model
指向 PropertyA 的值,正如预期的那样
modelExplorer.Container
指向一个视图的整个模型的值,它很复杂,可能有多个级别的层次结构和列表。所以基本上我现在需要获取 Model.Child[0].PropertyA
的值,但我事先不知道确切的路径。当然,我不知道当前索引 Child 等等。
modelExplorer.Metadata
具有整个容器当前 属性 的所有元数据,但我看不到将此元数据连接到实际模型和值的方法。
所以问题是:考虑到我不知道容器的整个层次结构这一事实,如何在此处达到 PropertyB
的值?也许还有其他完整的方法来实现所需的验证属性?
我终于找到了解决办法。
首先,我重载了错误版本的方法。正确的是
void AddAndTrackValidationAttributes(ViewContext viewContext, ModelExplorer modelExplorer, string expression, IDictionary<string, string> attributes)
,因为它向当前 属性 提供带有验证属性的表达式字符串。
通过这个表达式,我们可以假设名称存储在 otherPropertyName
中的目标 属性 存在于相同的路径中(因为它位于相同的 class 中)。例如,如果表达式是 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;
}
}
我正在尝试在 ASP.NET 核心 MVC 网络应用程序中实现我的自定义验证属性,包括服务器端和客户端验证。现有数据注释的缺点是属性的静态性质,这使得无法将运行时数据传递给它。
我想创建 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 机制添加此属性而不会污染视图的方法。
我在答案
现在我正在尝试将实际值注入我的自定义验证中的属性 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);
}
}
问题是我无法找到一种方法来获取对正在验证的 属性 的 parent class 的引用以获得 PropertyB
的值.我这里只有:
modelExplorer.Model
指向 PropertyA 的值,正如预期的那样modelExplorer.Container
指向一个视图的整个模型的值,它很复杂,可能有多个级别的层次结构和列表。所以基本上我现在需要获取Model.Child[0].PropertyA
的值,但我事先不知道确切的路径。当然,我不知道当前索引 Child 等等。modelExplorer.Metadata
具有整个容器当前 属性 的所有元数据,但我看不到将此元数据连接到实际模型和值的方法。
所以问题是:考虑到我不知道容器的整个层次结构这一事实,如何在此处达到 PropertyB
的值?也许还有其他完整的方法来实现所需的验证属性?
我终于找到了解决办法。
首先,我重载了错误版本的方法。正确的是
void AddAndTrackValidationAttributes(ViewContext viewContext, ModelExplorer modelExplorer, string expression, IDictionary<string, string> attributes)
,因为它向当前 属性 提供带有验证属性的表达式字符串。
通过这个表达式,我们可以假设名称存储在 otherPropertyName
中的目标 属性 存在于相同的路径中(因为它位于相同的 class 中)。例如,如果表达式是 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;
}
}