2

我为我的一个 MVC 模型创建了自定义 TypeDescriptionProvider。我用它来动态分配ValidationAttribute。

我使用一个属性的值来决定将哪些属性添加到其他属性。在我使用 DataAnnotationsValidationRunner 的 Web 服务中,验证工作正常。

跑步者来源:这里

internal static class DataAnnotationsValidationRunner
{
    public static IEnumerable<ErrorInfo> GetErrors(object instance)
    {
        return from prop in TypeDescriptor.GetProperties(instance).Cast<PropertyDescriptor>()
               from attribute in prop.Attributes.OfType<ValidationAttribute>()
               where !attribute.IsValid(prop.GetValue(instance))
               select new ErrorInfo(prop.Name, attribute.FormatErrorMessage(string.Empty), instance);
    }
}

要获取属性值,我使用以下代码(在 MyCustomTypeDescriptor 中)

public override PropertyDescriptorCollection GetProperties()
    {
        var originalProperties = base.GetProperties();
        var newProperties = new List<PropertyDescriptor>();
        var myProperty = originalProperties.Find("CountryCodeID", false)

        var myId = (int)countryProperty.GetValue(base.GetPropertyOwner(myProperty));

        foreach (PropertyDescriptor pd in originalProperties)
        {
            AttributeCollection runtimeAttributes = pd.Attributes;

            // add new attributes based on myId value
            ....
        }

        return new PropertyDescriptorCollection(newProperties.ToArray());
    }

在 MVC 视图中将此模型与此描述符一起使用时,出现以下异常:

值不能为空。参数名称:primary 说明:在执行当前Web请求期间发生了未处理的异常。请查看堆栈跟踪以获取有关错误及其源自代码的位置的更多信息。

异常详细信息:System.ArgumentNullException:值不能为空。参数名称:primary

在 TypeDescriptor 中获取属性值的正确方法是什么?我通过模型类型的提供者使用这个描述符,而不是实例(例如 global.asax)。

编辑:我找到了解决方法。在 MyTypeDescriptorProvider 的 GetTypeDescriptor 方法中,我使用实例参数并将其传递给 MyCustomTypeDescriptor 的构造函数。但是,MVC 验证不起作用。我虽然它自动使用这些动态数据(类似于上面提到的跑步者)。

编辑 2:使用 workaroud 我几乎总是看到实例为空。所以不可能在那里获得价值并将其交给 TypeDescriptor 的构造函数......

谢谢!

4

1 回答 1

2

最后,我能够使用 customTypeDescriptor 生成客户端验证和绑定期间验证模型所需的 HTML 标记。

第一个 MyCustomTypeDescriptor.cs:

/// <summary>
/// CustomTypeDescriptor that provides validation in both MVC Web and WCF services.
/// </summary>
public class MyCustomTypeDescriptionProvider : TypeDescriptionProvider
{
    public MyCustomTypeDescriptionProvider(TypeDescriptionProvider parent)
        :base(parent)
    {

    }

    public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
    {
        return new MyCustomTypeDescriptor(base.GetTypeDescriptor(objectType, instance));
    }
}

public class MyCustomTypeDescriptor : CustomTypeDescriptor
{
    public MyCustomTypeDescriptor(ICustomTypeDescriptor parent)
        : base(parent)
    { }

    public override PropertyDescriptorCollection GetProperties()
    {
        var originalProperties = base.GetProperties();

        if (this.IsRequired(originalProperties))
        {
            var newProperties = new List<PropertyDescriptor>();

            foreach (PropertyDescriptor property in originalProperties)
            {
                var attrs = property.Attributes;
                var newAttrs = new Attribute[attrs.Count + 1];
                attrs.CopyTo(newAttrs, 0);
                newAttrs[attrs.Count] = new RequiredAttribute();
                newProperties.Add(TypeDescriptor.CreateProperty(property.ComponentType, property, newAttrs));
            }

            return new PropertyDescriptorCollection(newProperties.ToArray());
        }
        else
        {
            return originalProperties;
        }
    }

    /// <summary>
    /// IsRequired just simulates more complex validation rule (dependant on another value in model)
    /// </summary>
    /// <param name="originalProperties"></param>
    /// <returns></returns>
    private bool IsRequired(PropertyDescriptorCollection originalProperties)
    {
        if (originalProperties == null || originalProperties.Count == 0)
        {
            throw new ArgumentNullException();
        }

        var dependantProperty = originalProperties.Find("DependantValue", false);

        if (dependantProperty == null)
        {
            throw new InvalidOperationException();
        }

        var value = (int)dependantProperty.GetValue(base.GetPropertyOwner(dependantProperty));

        return value > 0;
    }
}

然后绑定这个描述符(每个实例!)我使用 MyModelValidatorProvider:

/// <summary>
/// validator provider is used only for unobtrusive validation
/// </summary>
public class MyModelValidatorProvider : DataAnnotationsModelValidatorProvider
{
    protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
    {
        var isPropertyValidation = metadata.ContainerType != null && !String.IsNullOrEmpty(metadata.PropertyName);
        var model = context.Controller.ViewData.Model as TestCustomizedModel;            

        if (isPropertyValidation && model != null)
        {
            TypeDescriptor.AddProvider(new MyCustomTypeDescriptionProvider(TypeDescriptor.GetProvider(model)), model);

            AttributeCollection newAttributes;

            newAttributes = TypeDescriptor.GetProperties(model).Find(metadata.PropertyName, false).Attributes;

            var attrArray = new Attribute[newAttributes.Count];

            newAttributes.CopyTo(attrArray, 0);

            attributes = attrArray;
        }

        return base.GetValidators(metadata, context, attributes);
    }
}

这很好用,但是,在 ModelBinding 期间,没有设置 ViewData,因此 ValidatorProvider 不会挂钩。作为对此的解决方案,我使用了 MyModelBinder:

/// <summary>
/// Model binder that attaches CustomTypeDescriptor and validates model. 
/// </summary>
public class MyModelBinder : DefaultModelBinder
{
    protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        base.OnModelUpdated(controllerContext, bindingContext);

        TypeDescriptor.AddProvider(new MyCustomTypeDescriptionProvider(TypeDescriptor.GetProvider(bindingContext.Model)), bindingContext.Model);

        var errors = DataAnnotationRunner.GetErrors(bindingContext.Model);

        if (errors != null)
        {
            foreach (var error in errors)
            {
                bindingContext.ModelState.AddModelError(error.MemberNames.FirstOrDefault() ?? string.Empty, error.ErrorMessage);
            }
        }
    }
}

现在我可以使用 MyCustomTypeDescriptor 和 DataAnnotationRunner 来验证所有 MVC Web、MVC 其他类而不是控制器、html 帮助程序(不显眼的验证)以及其他项目,如 WCF 服务......

这一切都很好,只是感觉不对劲。如果我能够以某种方式将 MyCustomTypeDescriptor 直接连接到 MVC,那就太好了,但是正如这个链接所声称的那样,这似乎是不可能的。

如何在 ASP.NET MVC 中提供我自己的 ICustomTypeDescriptor?

欢迎任何有助于使该解决方案更优雅的改进。谢谢你。

于 2013-07-22T13:01:27.093 回答