如何在ValidationHtmlAttributeProvider的AddValidationAttributes中获取父对象?

问题描述 投票:0回答:1

我正在尝试在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的值?也许还有完成所需验证属性的其他完整方法?

c# asp.net-mvc asp.net-core unobtrusive-validation
1个回答
0
投票

我终于找到了解决方案。

首先,我已重载了错误版本的方法。正确的是void AddAndTrackValidationAttributes(ViewContext viewContext, ModelExplorer modelExplorer, string expression, IDictionary<string, string> attributes),因为它为具有验证属性的当前属性提供了表达式字符串。

使用此表达式,我们可以假定名称存储在otherPropertyName中的目标属性存在于同一路径上(因为它位于同一类中)。例如,如果表达式为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;
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.