Search code examples
asp.net-mvcmodelbindingradio-button

How can I bind a dynamic collection of questions with a dynamic collection of answers to a view model?


I have an app that is a multiple choice survey. The questions are loaded from a database and the answer choices are also loaded from a database. I want to bind the response to the model, but I'm not sure how to achieve this. Here is my ViewModel and View:

ViewModels:

 public class SurveyViewModel : SurveyViewModelBase
     {
         public IEnumerable<SectionViewModel> Sections { get; set; }
         public DemographicQuestionsModel DemographicQuestions { get; set; }
         public string Title { get; set; }
     }
 public class SectionViewModel
     {
        public string SectionTitle { get; set; }
        public IEnumerable<QuestionAnswerViewModel> QAs { get; set; }
     }
 public class QuestionAnswerViewModel
     {
         public QuestionViewModel Question { get; set; }
         public AnswerViewModel SelectedAnswer { get; set; }
         public IEnumerable<QuestionAnswerViewModel> ChildQuestions { get; set; }
     }
 public class QuestionViewModel
     {
         public Guid Id { get; set; }
         public string Text { get; set; }
         public IEnumerable<AnswerViewModel> PossibleAnswers { get; set; }
         public int QuestionNumber { get; set; }
     }
  public class AnswerViewModel
     {
         public Guid Id { get; set; }
         public string AnswerText { get; set; }
     }

View:

 @using VSC.ViewModels
 @model VSC.ViewModels.SurveyViewModel
 @{
     SectionViewModel section = Model.Sections.FirstOrDefault();
 }
 <div class="row">
     <div class="sectionTitle">@(section.SectionTitle)</div>
 </div>
 <form action="/VSC/Survey">
     @foreach (var qa in section.QAs)
     {
         if (qa.Question.QuestionNumber == 0)
         {
             <div class="row question">
                 @qa.Question.Text
             </div>
         }
         else
         {
             if (qa.ChildQuestions != null)
             {
                 <div class="row question child-question @(qa.Question.QuestionNumber==null?"parent-question":"")">
                 </div>
             }
             else
             {
                 <div class="row question @(qa.Question.QuestionNumber==null?"parent-question":"")">
                     @qa.Question.QuestionNumber  @qa.Question.Text
                 </div>
             }
             <div class="row answers">
                 @foreach (var answer in qa.Question.PossibleAnswers)
                 {
                     <div class="answer">
                         @{
    
                             <div class="answer">
                                 <label>
                                     @answer.AnswerText
                                     @Html.RadioButtonFor(m=>qa.SelectedAnswer, answer.Id)
                                 </label>
                             </div>
                             <div class="answer">
                                 <label>
    
                                 </label>
                             </div>
                         }
                     </div>
                 }
             </div>
         }
    
     }
     <input type="submit" value"Submit" />
 </form>

Naturally, this produces radio buttons with all the same names, so when I click on one for a given question, the previous questions radio button unchecks and the new question is now the only one with a selection.

Any help is appreciated.


Solution

  • You will need to specify the index in each ienumerable's input field. It is much better if you use for loop, but with your current code it could be done like this;

    See the code below where to create index variables, then manually create a radio button with the indexes.

    @foreach (var qa in section.QAs)
    {
       // create the QA index
       int indexQA = 0;
    
       if (qa.Question.QuestionNumber == 0)
       {
          ...
       }
       else
       {
          ...
          <div class="row answers">
             @foreach (var answer in qa.Question.PossibleAnswers)
             {
                // create possible answers index
                int indexPA = 0;
     
                <div class="answer">
                @{
                   <div class="answer">
    
                      // create label for, with index
                      <label for="QA[@indexQA]PA[@indexPA]">
                         @answer.AnswerText
                      </label>
    
                      // create radio button, with index
                      <input type="radio" id="QA[@indexQA]PA[@indexPA]" name="Sections[0].QAs[@indexQA].Question.SelectedAnswer.Id" data-index="QA[@indexQA]" class="possible-answer" value="@answer.Id">
    
                   </div>
                   <div class="answer">
                      <label>
                      </label>
                   </div>
                 }
                 </div>
    
                 // increment possible answers index
                 indexPA++;
              }
           </div>
        }
        
        // increment QA index
        indexQA++;
    }
    

    Since the radio buttons have different name attributes, they will not be grouped with each other, so we need to make a script that unchecks the others.

    @section scripts{
       <script>
          $(document).ready(function(){
             $(".possible-answer").change(function(){
                var dataValue1 = $(this).data("index");
                
                // loop through all possible answers and check their data-index
                $(".possible-answer").each(function(){
                   var dataValue2 = $(this).data("index");
                   
                   // if they have the same index, they belong to the same group, uncheck
                   if(dataValue1 == dataValue2)
                   {
                      $(this).prop("checked", false);
                   }
                });
                
                // check the current clicked one
                $(this).prop("checked", true);
             });
          });
       </script>
    }