4

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:

  • In the ModelValidatorProvider, I've tried using ControllerContext.Controller.ViewData.Model, but that doesn't work if you have nested types. If I'm trying to figure out the validators Form.FieldValues[3], I have no idea which FieldValue to use.

  • I tried using a custom ModelMetadata that tries to use the internal modelAccessor's Target property to get the parent, but this also doesn't work if you have a nested type. Somewhere internal to MVC, an expression like the one in my example will result in the Target being the Model's type(Form), not FieldValue. So I get the same problem as above where I have no idea what instance of FieldValue to compare against.

  • A class-level validation attribute that I could put on the FieldValue class itself, but this only gets called during server validation. I need client-side validation, too.

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

2
  • It's definitely possible in MVC but it's quite complicated. There is a video by Brad Wilson, of the MVC team, where he describes building a dynamic validation system around a Dictionary channel9.msdn.com/Series/mvcConf/…. Commented May 9, 2012 at 19:56
  • How do you bind the results to an array? Do you use a custom model binder? Commented Nov 9, 2012 at 22:39

1 Answer 1

2

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);
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for answering Darin. I'm not sure this solves the problem, though. I want to use the validator attributes that already exist withotu having to reimplement them all. Thanks for pointing out the security issue, though. We already have this covered(verifying that a field belongs to the form and what not), it's just not part of my sample code.
Oh, I see. How about FluentValidation.NET then? It's the framework I use for performing validation in my ASP.NET MVC applications. Your scenario is pretty trivial to implement with it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.