字段由同一属性多次注释

jwa*_*zko 7 .net validation asp.net-mvc data-annotations

对于我的ASP.NET MVC应用程序,我创建了自定义验证属性,并指出可以为单个字段或属性指定多个实例:

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)]
public sealed class SomeAttribute: ValidationAttribute
Run Code Online (Sandbox Code Playgroud)

我为这样的属性创建了验证器:

public class SomeValidator : DataAnnotationsModelValidator<SomeAttribute>
Run Code Online (Sandbox Code Playgroud)

并在Application_StartGlobal.asax 中将其连接起来

DataAnnotationsModelValidatorProvider.RegisterAdapter(
    typeof (SomeAttribute), typeof (SomeValidator));
Run Code Online (Sandbox Code Playgroud)

最后,如果我以所需的方式使用我的属性:

[SomeAttribute(...)]  //first
[SomeAttribute(...)]  //second
public string SomeField { get; set; }
Run Code Online (Sandbox Code Playgroud)

当框架执行验证时,只调用第一个属性实例.第二个似乎死了.我注意到在每个请求期间只创建了一个验证器实例(对于第一个注释).

如何解决这个问题并解雇所有属性?

jwa*_*zko 7

让我回答一下.从MSDN(http://msdn.microsoft.com/en-us/library/system.attribute.typeid.aspx,http://msdn.microsoft.com/en-us/library/6w3a7b50.aspx):

在AttributeUsageAttribute.AllowMultiple设置为true的情况下定义自定义属性时,必须覆盖Attribute.TypeId属性以使其唯一.如果属性的所有实例都是唯一的,则覆盖Attribute.TypeId以返回属性的对象标识.如果只有属性的某些实例是唯一的,则返回Attribute.TypeId中的值,该值将在这些情况下返回相等性.例如,某些属性具有构造函数参数,该参数充当唯一键.对于这些属性,从Attribute.TypeId属性返回构造函数参数的值.

实现时,该标识符仅是属性的类型.但是,意图是使用唯一标识符来标识相同类型的两个属性.

总结一下:

TypeId被记录为"用于标识相同类型的两个属性的唯一标识符".默认情况下,TypeId只是属性的类型,因此当遇到两个相同类型的属性时,它们被MVC验证框架视为"相同".

实施:

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)]
public sealed class SomeAttribute: ValidationAttribute
{
    private string Parameter { get; set; }
    public override object TypeId
    {
        get { return string.Format("{0}[{1}]", GetType().FullName, Parameter); } 
    }

    public SomeAttribute(string parameter)
    {
        Parameter = parameter;
    }                
Run Code Online (Sandbox Code Playgroud)

这种TypeId创建方式选择如下:

  • 返回新对象 - 它太多了,实例总是不同的,
  • 基于字符串参数返回哈希码 - 可能导致冲突(无限多个字符串无法被内射映射到任何有限集中 - 字符串的最佳唯一标识符是字符串本身,请参见此处).

完成后,服务器端验证案例工作.当这个想法需要与不引人注目的客户端验证相结合时,问题就开始了.假设您已按以下方式定义了自定义验证器:

public class SomeAttributeValidator : DataAnnotationsModelValidator<SomeAttribute>
{
    public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
    {
        var rule = new ModelClientValidationRule {ValidationType = "someattribute"};
        rule.ValidationParameters.Add(...)
        yield return rule;
    }
Run Code Online (Sandbox Code Playgroud)

有这个:

public class Model
{
    [SomeAttribute("first")]
    [SomeAttribute("second")]
    public string SomeField { get; set; }
Run Code Online (Sandbox Code Playgroud)

导致以下错误:

不显眼的客户端验证规则中的验证类型名称必须是唯一的.以下验证类型不止一次出现:someattribute

如上所述,解决方案是拥有独特的验证类型.我们必须区分每个已注册的属性实例,它已用于注释字段,并为其提供相应的验证类型:

public class SomeAttributeValidator : DataAnnotationsModelValidator<SomeAttribute>
{
    private string AnnotatedField { get; set; }
    public SomeAttributeValidator(
        ModelMetadata metadata, ControllerContext context, SomeAttribute attribute)
        : base(metadata, context, attribute)
    {
        AnnotatedField = string.Format("{0}.{1}", 
            metadata.ContainerType.FullName, metadata.PropertyName);
    }

    public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
    {
        var count = Storage.Get<int>(AnnotatedField) + 1;
        Storage.Set(AnnotatedField, count);

        var suffix = char.ConvertFromUtf32(96 + count); // gets a lowercase letter 
        var rule = new ModelClientValidationRule
        {
            ValidationType = string.Format("someattribute{0}", suffix)
        };
Run Code Online (Sandbox Code Playgroud)

(验证类型必须只包含小写字母 - 使用上面的解决方案,如果你有超过26个属性 - 整个字母表用尽,预计会有异常)

其中Storage是简单的实用程序,它存储当前http请求的数据:

internal class Storage
{
    private static IDictionary Items
    {
        get
        {
            if (HttpContext.Current == null)
                throw new ApplicationException("HttpContext not available.");
            return HttpContext.Current.Items;
        }
    }

    public static T Get<T>(string key)
    {
        if (Items[key] == null)
            return default(T);
        return (T)Items[key];
    }

    public static void Set<T>(string key, T value)
    {
        Items[key] = value;
    }
}
Run Code Online (Sandbox Code Playgroud)

最后,JavaScript部分:

$.each('abcdefghijklmnopqrstuvwxyz'.split(''), function(idx, val) {
    var adapter = 'someattribute' + val;
    $.validator.unobtrusive.adapters.add(adapter, [...], function(options) {
        options.rules[adapter] = {
            ...
        };
        if (options.message) {
            options.messages[adapter] = options.message;
        }
    });
});

$.each('abcdefghijklmnopqrstuvwxyz'.split(''), function(idx, val) {
    var method = 'someattribute' + val;
    $.validator.addMethod(method, function(value, element, params) {
        ...
    }, '');
});
Run Code Online (Sandbox Code Playgroud)

要获得完整的解决方案,请查看此来源.