Search code examples
asp.net-mvc-2asp.net-mvc-2-validation

How to validate and post data from dynamic form in ASP.NET MVC 2


I have an existing ASP.NET MVC 2 application that I've been asked to extend. I am adding a new feature to the site where I generate an employee assessment form based on a dynamic list of questions retrieved from our HR system. I have everything working with the exception of validation and posting the responses back to the site. Here's some details:

  1. I retrieve a list of "Questions" from our back-end system via a web service call.
  2. Each "Question" contains the text to display as well as the following settings:
    • The question Type (corresponds to textbox, textarea, radio button list or checkbox list)
    • If comments are allowed
    • If an answer is required
    • When applicable, the list of possible responses

To generate the form, I use a for-each loop over the list of Questions. I use the value of the QuestionType property to determine which partial view to render (one for each of the types). For example, if QuestionType == SingleChoice, that partial renders the choices as a radio button list. If comments are allowed for the question, I also render an additional textarea field to hold the user's comments.

As I said, rendering the form is working fine but now I need to:

A. Enforce when an answer is required. I'm using DataAnnotations for validation everywhere else in the solution but since I'm not working against a static model, I don't see how I can do that.

B. Post the results back to the site. For each question, there can be text entered into a textbox or textarea, a selected value for a radio button list or multiple selected values for a checkbox list. Plus, each question could also have additional text sent back in the form of a comment.

All of the examples that I've seen working with dynamic "lists" are only concerned with posting a single value for each field and it is always the same type (e.g. a list of textboxes). With the variations I have to support, plus the need to send back the entered/selected value(s) and a comment for each question, I'm stumped.

Any guidance is appreciated.


Solution

  • I've just finished completing exactly the same task.

    I chose to write a custom model binder for my dynamic form object. The model binder pulled out a bunch of prefixed form keys for hidden fields which contained some delimited meta data about the question (i.e IsRequired, QuestionType, QuestionId etc etc)

    I'm using MVC3 but I think this should all work in MVC2.

    I created a ModelBinder like:

    public class DynamicFormModelBinder : IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            // Create the object to be bound to (I had a kind of form object
            // with a simple list of answer objects
            DynamicForm form = new DynamicForm(new List<Answer>());
    
            HttpRequestBase request = controllerContext.HttpContext.Request;
    
            var keys = request.Form.AllKeys.Where(k => k.StartsWith("MyFormsKeyPrefix_Meta_"));
            foreach (var key in keys)
            {
                // Loop over each question's meta data. Metadata will always be present 
                // even if the user hasn't selected an answer as it's a hidden field
    
                // TODO: Split the meta data and pull out IsRequired, QuestionType etc
    
                // TODO: Get all the posted form values for the question (these values 
                //       will come from textboxes, dropdowns, checkboxes etc)
                //       Use a prefix like: MyFormsKeyPrefix_Answer_{QuestionId}
                //       textboxes & dropdowns will only ever have one value 
                //       but checkboxes could have multiple
    
                // TODO: If it's a mandatory question then ensure there is at least
                //       one posted value that is not an empty string
    
                // If there is a validation error then add it to the model state
                bindingContext.ModelState.AddModelError(key, "Field is required");
    
                foreach(var answerHtmlName in answerHtmlNames)
                {
                    // TODO: Loop over each posted answer and create some kind of nice
                    //       Answer object which holds the QuestionId, AnswerId, AnswerOptionId 
                    //       and Value etc.
    
    
                    // Add the answer to the forms answers list
                    form.Answers.Add(answer);
                }
            }
    
            return form;
        }
    
    }
    

    I register the ModelBinder in Global.asax using the following:

    ModelBinders.Binders.Add(typeof(DynamicForm), new DynamicFormModelBinder());
    

    So, the action method that recieves the form post looks something like:

    public ActionResult ProcessForm(DynamicForm form) {
        if(ModelState.IsValid) 
        {
            DynamicFormService.Process(form);
    
            return RedirectToAction("TheHttpGetAction");
        }
        return TheHttpGetAction();
    }