ASP MVC 中的动态验证

4
在工作中,我正在尝试创建一个流程,让用户动态创建一个表单,其他用户可以填写值。但我在如何使其与ASP MVC 3的内置模型绑定和验证兼容方面遇到了麻烦。
我们的视图模型设置类似于以下示例代码(请注意,我已经过度简化了示例代码):
public class Form 
{
    public FieldValue[] FieldValues { get; set; }
}

public class Field
{
    public bool IsRequired { get; set; }
}

public class FieldValue 
{
    public Field Field { get; set; }
    public string Value { get; set; }
}

我们的视图大致如下所示:

@model Form
@using (Html.BeginForm("Create", "Form", FormMethod.Post))
{
    @for(var i = 0; i < Model.Fields.Count(); i++)
    {
        @Html.TextBoxFor(_ => @Model.Fields[i].Value) 
    }
    <input type="submit" value="Save" name="Submit" />
}

我希望我们能创建一个自定义的ModelValidatorProvider或ModelMetadataProvider类,能够分析FieldValue实例,确定其Field.IsRequired属性是否为true,然后将RequiredFieldValidator添加到该特定实例的验证器中。但我一直没有成功。似乎在ModelValidatorProvider(和ModelMetadataProvider)中无法访问父容器的值(即:GetValidators()将被调用以获取FieldValue.Value,但从那里无法获取FieldValue对象)。
我尝试过的事情:
- 在ModelValidatorProvider中,我尝试使用ControllerContext.Controller.ViewData.Model,但如果有嵌套类型,则无法正常工作。如果我正在尝试找出验证器Form.FieldValues[3],我不知道要使用哪个FieldValue。 - 我尝试使用自定义ModelMetadata,尝试使用内部modelAccessor的Target属性来获取父级,但如果有嵌套类型,则这也无法正常工作。在MVC的某个内部位置,像我的示例中的表达式将导致Target成为模型的类型(Form),而不是FieldValue。因此,我遇到了与上述相同的问题,即我不知道要与哪个FieldValue实例进行比较。 - 一个类级别的验证属性,我可以将其放在FieldValue类本身上,但这只在服务器验证期间调用。我也需要客户端验证。
在MVC中我正在尝试的事情是否可能?还是我完全错了什么?

在MVC中肯定是可能的,但是相当复杂。 MVC团队的Brad Wilson有一个视频,在其中描述了如何围绕字典构建动态验证系统。http://channel9.msdn.com/Series/mvcConf/mvcConf-2011-Brad-Wilson-Advanced-MVC-3。 - Kyle Nunery
你如何将结果绑定到数组?你使用自定义模型绑定器吗? - Guillermo Gutiérrez
1个回答

2
一种可能性是使用自定义验证属性。
但在实现之前,我想指出您场景中的一个潜在缺陷。IsRequired属性是您模型的一部分。这意味着当表单提交时,必须知道其值,以便我们有条件地将所需规则应用于相应的属性。但是,为了在表单提交时知道此值,这意味着它必须是表单的一部分(作为隐藏或标准输入字段)或必须从某个地方检索(数据存储,...)。第一种方法的问题是显而易见的=>隐藏字段意味着用户可以设置任何他喜欢的值,因此它不再是真正的验证,因为决定哪个字段是必需的是用户。
说完这个警告,假设您信任用户并决定采用隐藏字段方法来存储IsRequired值。让我们看一个示例实现:
模型:
public class Form
{
    public FieldValue[] Fields { get; set; }
}

public class FieldValue
{
    public Field Field { get; set; }

    [ConditionalRequired("Field")]
    public string Value { get; set; }
}

public class Field
{
    public bool IsRequired { get; set; }
}

控制器:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        var model = new Form
        {
            Fields = new[]
            {
                new FieldValue { Field = new Field { IsRequired = true }, Value = "" },
                new FieldValue { Field = new Field { IsRequired = true }, Value = "" },
                new FieldValue { Field = new Field { IsRequired = false }, Value = "value 3" },
            }
        };
        return View(model);
    }

    [HttpPost]
    public ActionResult Index(Form model)
    {
        return View(model);
    }
}

查看:

@model Form
@using (Html.BeginForm())
{
    @Html.EditorFor(x => x.Fields)
    <input type="submit" value="Save" name="Submit" />
}

ConditionalRequiredAttribute:

public class ConditionalRequiredAttribute : ValidationAttribute, IClientValidatable
{
    private RequiredAttribute _innerAttribute = new RequiredAttribute();

    private readonly string _fieldProperty;

    public ConditionalRequiredAttribute(string fieldProperty)
    {
        _fieldProperty = fieldProperty;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var containerType = validationContext.ObjectInstance.GetType();
        var field = containerType.GetProperty(_fieldProperty);
        if (field == null)
        {
            return new ValidationResult(string.Format("Unknown property {0}", _fieldProperty));
        }

        var fieldValue = (Field)field.GetValue(validationContext.ObjectInstance, null);
        if (fieldValue == null)
        {
            return new ValidationResult(string.Format("The property {0} was null", _fieldProperty));
        }

        if (fieldValue.IsRequired && !_innerAttribute.IsValid(value))
        {
            return new ValidationResult(this.ErrorMessage, new[] { validationContext.MemberName });
        }

        return ValidationResult.Success;
    }


    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule()
        {
            ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
            ValidationType = "conditionalrequired",
        };

        rule.ValidationParameters.Add("iserquiredproperty", _fieldProperty + ".IsRequired");

        yield return rule;
    }
}

关联的不显眼适配器:

(function ($) {
    $.validator.unobtrusive.adapters.add('conditionalrequired', ['iserquiredproperty'], function (options) {
        options.rules['conditionalrequired'] = options.params;
        if (options.message) {
            options.messages['conditionalrequired'] = options.message;
        }
    });

    $.validator.addMethod('conditionalrequired', function (value, element, parameters) {
        var name = $(element).attr('name'),
        prefix = name.substr(0, name.lastIndexOf('.') + 1),
        isRequiredFiledName = prefix + parameters.iserquiredproperty,
        requiredElement = $(':hidden[name="' + isRequiredFiledName + '"]'),
        isRequired = requiredElement.val().toLowerCase() === 'true';

        if (!isRequired) {
            return true;
        }

        return value && value !== '';
    });

})(jQuery);

感谢回答,Darin。虽然我不确定这是否解决了问题。但是我想使用已经存在的验证器属性,而不必重新实现它们。感谢指出安全问题。我们已经解决了这个问题(验证字段是否属于表单等),只是它不是我的示例代码的一部分。 - rossisdead
1
哦,我明白了。那么FluentValidation.NET呢?这是我在ASP.NET MVC应用程序中执行验证所使用的框架。你的情况使用它实现相当简单。 - Darin Dimitrov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接