Indy-Jones
Indy-Jones

Reputation: 728

MVC 5 Dynamic Data Form - List of Model Not Posting to Controller

Ok this is driving me nuts. Sometimes I find with MVC I get a bit lost in the woods and start looking down the wrong path and all goes to hell. So I'm reaching out to see if a fresh pair of eyes will point me in the correct direction.

Basically the structure is allowing an admin to create their own questions which in itself would be pretty simple, however in this case it's more than a "single" answer box. Example:

What are the name(s) and address(es) of your business locations?

Name: _______ Address: ____ City: ___ State: __ Zip: _

So if the client only had one location it would be an answer with 5 text boxes (name, address, city, state, zip)...but since the questions are created dynamically it could be 3 text boxes, 1, 2, etc.

So the problem I'm having is when the model is posted back to the controller the Questions part of the view model is fine, but the answers/choices part of it is always returning null. Please take a look and see if there's the giant on/off switch I clearly have neglected to include.

Here's the models and view model:

public class Questions
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ID { get; set; }
    public int FormID { get; set; }

    [Display(Name = "Question Order")]
    [Range(0, Int32.MaxValue, ErrorMessage = "Must Use an Integer")]
    public int QuestionOrder { get; set; }

    [Required]
    [DataType(DataType.MultilineText)]
    public string Question { get; set; }

    [Display(Name = "Help Text")]
    public string HelpText { get; set; }
    public bool IsActive { get; set; }

    [Display(Name = "Question May Have More Than One Entry")]
    public bool CanHaveMoreThanOne { get; set; }

    public int QuestionTypeID { get; set; }

}

public class Choices
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ID { get; set; }
    public int QuestionID { get; set; }
    public string FieldName { get; set; }
    public string QuestionLabel { get; set; }
    public bool IsRequired { get; set; }
    public string RegEx { get; set; }
    public string TextIfSelected { get; set; }
    public string QuestionToSkipToIfSelected { get; set; }
    public bool IsActive { get; set; }

    [NotMapped]
    public string AnswerText { get; set; }
    [NotMapped]
    public bool AnswerBool { get; set; }
    [NotMapped]
    public int AnswerNumeric { get; set; }
    [NotMapped]
    public DateTime AnswerDate { get; set; }

}

public class ViewQuestion
{

    public Questions Question { get; set; }
    public IEnumerable<Choices> Answers { get; set; }

}

Here's the view:

@using InterviewMaster.Models
@model ViewQuestion

@using (Html.BeginForm("AnswerQuestion","Interview", FormMethod.Post))
{
@Html.AntiForgeryToken()

<div class="form-horizontal">
    @Html.HiddenFor(m => m.Question.CanHaveMoreThanOne)
    @Html.HiddenFor(m => m.Question.FormID)
    @Html.HiddenFor(m => m.Question.HelpText)
    @Html.HiddenFor(m => m.Question.ID)
    @Html.HiddenFor(m => m.Question.Question)
    @Html.HiddenFor(m => m.Question.QuestionOrder)
    @Html.HiddenFor(m => m.Question.QuestionTypeID)

    <h3>@Model.Question.Question</h3>
    <hr />

    @if (Model.Question.QuestionTypeID == 1) 
    {

        foreach(var o in Model.Answers) 
        {

            Html.RenderPartial("Partial1", o);   

        }
    }

    else if (Model.Question.QuestionTypeID == 2) 
    {
         //TO-DO: Make other types of answers such as radio buttons, checkboxes, etc.
    }

    <input type="submit" value="Create" class="btn btn-default" />
</div>
}

Here's the partial I'm using

@model InterviewMaster.Models.Choices


@using(Html.BeginCollectionItem("Choices"))
{
    @Html.HiddenFor(model => model.ID)
    @Html.HiddenFor(model => model.QuestionID)
    @Html.HiddenFor(model => model.FieldName)
    @Html.HiddenFor(model => model.QuestionLabel)
    @Html.HiddenFor(model => model.IsRequired)
    @Html.HiddenFor(model => model.RegEx)
    @Html.HiddenFor(model => model.TextIfSelected)
    @Html.HiddenFor(model => model.QuestionToSkipToIfSelected)
    @Html.HiddenFor(model => model.IsActive)

    @Html.HiddenFor(model => model.AnswerBool)
    @Html.HiddenFor(model => model.AnswerDate)
    @Html.HiddenFor(model => model.AnswerNumeric)

    <div class="form-group">
    <label>@Html.ValueFor(model => model.QuestionLabel)</label>
    @Html.TextBoxFor(model => model.AnswerText)
    </div>

}

and last but not least, the controller receiving this post:

    // POST: /Interview/AnswerQuestion
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult AnswerQuestion(ViewQuestion dto)
    {
        foreach(var answer in dto.Answers)
        {
            Console.Write("Answer:" + answer.AnswerText);
        }

        return RedirectToAction("ContinueInterview", new { OrderID = 1, OrderDetailID = 1, FormID = 1, QuestionID = 1 });
    }

Upvotes: 2

Views: 5589

Answers (1)

Indy-Jones
Indy-Jones

Reputation: 728

Ok for those of you who may need this someday, here's what the problem was.

This...

 @using(Html.BeginCollectionItem("Choices"))

should have been this...

 @using(Html.BeginCollectionItem("dto.Answers"))

In the example on Steven Sandersons' site (http://blog.stevensanderson.com/2010/01/28/editing-a-variable-length-list-aspnet-mvc-2-style/) it didn't really go into a complex example such as mine (not his fault...mine)

I thought it was whatever the model was you were editing/viewing. Instead, it's what you're sending back to the controller. Big duh on my part.

Hopefully this helps someone in the future. This took me about 8 hours of driving myself crazy....arrrrgggghhh!

Upvotes: 4

Related Questions