rossisdead
rossisdead

Reputation: 2098

Dynamic validation in ASP MVC

For a project at work I'm trying to create a process that lets a user dynamically create a form that other users could then fill out the values for. I'm having trouble figuring out how to go about getting this to play nice with the built in model binding and validation with ASP MVC 3, though.

Our view model is set up something like this. Please note that I've over simplified the example code:

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; }
}

And our view looks something like:

@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" />
}

I was hoping that we'd be able to create a custom ModelValidatorProvider or ModelMetadataProvider class that would be able to analyze a FieldValue instance, determine if its Field.IsRequired property is true, and then add a RequiredFieldValidator to that specific instance's validators. I'm having no luck with this, though. It seems that with ModelValidatorProvider(and ModelMetadataProvider) you can't access the parent container's value(ie: GetValidators() will be called for FieldValue.Value, but there's no way from there to get the FieldValue object).

Things I've tried:

Is what I'm trying to do even possible in MVC? Or is there something I'm missing entirely?

Upvotes: 4

Views: 6377

Answers (1)

Darin Dimitrov
Darin Dimitrov

Reputation: 1038720

One possibility is to use a custom validation attribute.

But before getting into the implementation I would like to point out a potential flaw in your scenario. The IsRequired property is part of your model. This means that when the form is submitted its value must be known so that we conditionally apply the required rule to the corresponding property. But for this value to be known when the form is submitted this means that it must be either part of the form (as a hidden or standard input field) or must be retrieved from somewhere (datastore, ...). The problem with the first approach is obvious => hidden field means that the user can set whatever value he likes, so it's no longer a real validation because it is the user that decides which field is required.

This warning being said, let's suppose that you trust your users and decide to take the hidden field approach for storing the IsRequired value. Let's see how a sample implementation:

Model:

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; }
}

Controller:

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);
    }
}

View:

@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;
    }
}

Associated unobtrusive adapter:

(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);

Upvotes: 2

Related Questions