ASP.NET MVC 5 group of radio buttons

2019-01-01 15:23发布

问题:

I am starting my first ASP.NET MVC project, so I have one simple question. I have following code:

foreach(var question in Model.GeneralQuestions)
{
    <div class = \"well\">
        <h3>
            <strong>@question.QuestionString</strong>
        </h3>
        @foreach (var answer in question.PossibleAnswers)
        {
            @Html.RadioButtonFor(model => question.QuestionString, answer.Answer)
            @Html.Label(answer.Answer)
            <br />
        }
    </div>
}

All questions in Model.GeneralQuestions are unique, so radio buttons should be divided into groups by name attribute (for each question one group of radio buttons). But this code produces only one group, so when I answer second question first one becomes deselected. What do I need to change?

EDIT
My model looks like:

public class StudentViewModel
{
    public Student Student { get; set; }
    public List<Question> GeneralQuestions { get; set; }
    public List<SubjectQuestions> SubjectQuestions { get; set; }
}
public class Student
{
    public int StudentID { get; set; }
    public string Index { get; set; }
    public string Name { get; set; }
    public string Surname { get; set; }

    public virtual ICollection<Subject> Subjects { get; set; }
}
public class Question
{
    public int QuestionID { get; set; }
    public string QuestionString { get; set; }
    public bool IsAssociatedWithSubject { get; set; }

    public virtual ICollection<PossibleAnswer> PossibleAnswers { get; set; }
    public virtual ICollection<Results> Results { get; set; }
}
public class SubjectQuestions
{
    public Subject Subject { get; set; }
    public List<Question> Questions { get; set; }
}
public class Results
{
    public int ResultsID { get; set; }
    public int QuestionID { get; set; }
    public int? SubjectID { get; set; }
    public int PossibleAnswerID { get; set; }

    public virtual Question Question { get; set; }
    public virtual PossibleAnswer PossibleAnswer { get; set; }
    public virtual Subject Subject { get; set; }
}

In one instance of StudentViewModel I save one student and all questions that he should answer (both general and related to subjects he is studying) and pass it to view. In view I put all questions in single form and they are all type of radio. So, can anyone help me with grouping of radio buttons and posting back this form correctly?

回答1:

There are a number of problems with your code including generating duplicate id\'s (invalid html), generating duplicate name attributes (which is why you\'re creating only one group, but more importantly this will prevent you from binding to the model when you post back) and you\'re not actually binding to a valid property anyway.

You will need to create view models to represent what you want to display and edit and generate the radio buttons in a for loop (or using an EditorTemplate) so they are correctly named with indexers.

View models

public class QuestionVM
{
  public int ID { get; set; } // for binding
  public string Text { get; set; }
  [Required]
  public int? SelectedAnswer { get; set; } // for binding
  public IEnumerable<AnswerVM> PossibleAnswers { get; set; }
}

public class SubjectVM
{
  public int? ID { get; set; }
  [DisplayFormat(NullDisplayText = \"General\")]
  public string Name { get; set; }
  public List<QuestionVM> Questions { get; set; }
}

public class AnswerVM
{
  public int ID { get; set; }
  public string Text { get; set; }
}

public class StudentVM
{
  public int ID { get; set; }
  public string Name { get; set; }
  // plus any other properties of student that you want to display in the view
  public List<SubjectVM> Subjects { get; set; }
}

View

@model YourAssembly.StudentVM
@using(Html.BeginForm())
{
  @Html.HiddenFor(m => m.ID)
  @Html.DisplayFor(m => m.Name)
  for(int i = 0; i < Model.Subjects.Count; i++)
  {
    @Html.HiddenFor(m => m.Subjects[i].ID)
    @Html.DisplayFor(m => m.Subjects[i].Name) // will display \"General\" if no name
    for (int j = 0; j < Model.Subjects[i].Questions.Count; j++)
    {
      @Html.HiddenFor(m => m.Subjects[i].Questions[j].ID)
      @Html.DisplayFor(m => m.Subjects[i].Questions[j].Text)
      foreach(var answer in Model.Subjects[i].Questions[j].PossibleAnswers )
      {
        <div>
          @Html.RadioButtonFor(m => m.Subjects[i].Questions[j].SelectedAnswer, answer.ID, new { id = answer.ID})
          <label for=\"@answer.ID\">@answer.Text</label>
        </div>
      }
      @Html.ValidationMessageFor(m => m.Subjects[i].Questions[j].SelectedAnswer)
    }
  }
  <input type=\"submit\" value=\"save\" />
}

Controller

public ActionResult Edit(int ID)
{
  StudentVM model = new StudentVM();
  // populate your view model with values from the database
  return View(model);
}

[HttpPost]
public ActionResult Edit(StudentVM model)
{
  // save and redirect
}

Note I am a little confused by the database structure implied by your models (for example why do you need separate models for Question and SubjectQuestion when a null value for SubjectID identifies it as a \"General\" question). I suggest you start by just hard-coding some values in the GET method to see how it works and posts back.

StudentVM model = new StudentVM();
model.ID = 1;
model.Name = \"bambiinela\";
model.Subjects = new List<SubjectVM>()
{
  new SubjectVM()
  {
    Questions = new List<QuestionVM>()
    {
      new QuestionVM()
      {
        ID = 1,
        Text = \"Question 1\",
        SelectedAnswer = ?, // set this if you want to preselect an option
        PossibleAnswers = new List<AnswerVM>()
        {
          new AnswerVM()
          {
            ID = 1,
            Text = \"Answer A\"
          },
          new AnswerVM()
          {
            ID = 1,
            Text = \"Answer B\"
          }
        }
      },
      new QuestionVM()
      {
        ID = 2,
        Text = \"Question 2\",
        PossibleAnswers = new List<AnswerVM>()
        {
          // similar to above
        }
      }
    }
  },
  new SubjectVM()
  {
    ID = 1,
    Name = \"Math\",
    Questions = new List<QuestionVM>()
    {
      // similar to above
    }
  }
};

When you post, the model is populated with the ID of the selected answer for each question in each subject. Note the use of DisplayFor() for some properties. These won\'t post back so you would need to repopulate these properties if you return the view (e.g. ModelState is not valid). Alternatively you can generate a read-only textbox or add a hidden input for those properties. I also suggest you inspect the HTML that is generated, in particular the name attributes which will look something like

<input type=\"radio\" name=\"Subjects[0].Questions[0].SelectedAnswer\" ...

to give you an understanding of how collections are bound to your model on post back



回答2:

The trick is to use an expression (first parameter to Html.RadioButtonFor) which contains a value that changes per group of radio-buttons. In your case, it would be an index in the list of questions.

Here is some sample code:

 @for (int i = 0; i < Model.GeneralQuestions.Count; i++)
 {
     var question = Model.GeneralQuestions[i];
     @Html.Label(question.QuestionString)
     <br />
     foreach (var answer in question.PossibleAnswers)
     {
         @Html.RadioButtonFor(model => 
           Model.GeneralQuestions[i].SelectedAnswerId, answer.Id)
         @Html.Label(answer.Answer)
         <br />
     }
 }

This produces the following HTML:

<label for=\"Q1\">Q1</label>
<br />
<input id=\"GeneralQuestions_0__SelectedAnswerId\" 
  name=\"GeneralQuestions[0].SelectedAnswerId\" type=\"radio\" value=\"1\" />
<label for=\"A01\">A01</label>
<br />
<input id=\"GeneralQuestions_0__SelectedAnswerId\" 
  name=\"GeneralQuestions[0].SelectedAnswerId\" type=\"radio\" value=\"2\" />
<label for=\"A02\">A02</label>
<br />
<label for=\"Q2\">Q2</label>
<br />
<input id=\"GeneralQuestions_1__SelectedAnswerId\" 
  name=\"GeneralQuestions[1].SelectedAnswerId\" type=\"radio\" value=\"11\" />
<label for=\"A11\">A11</label>
<br />
<input id=\"GeneralQuestions_1__SelectedAnswerId\" 
  name=\"GeneralQuestions[1].SelectedAnswerId\" type=\"radio\" value=\"12\" />
<label for=\"A12\">A12</label>
<br />

And for sake of completeness, here is a reduced version of the models used:

public class StudentViewModel
{
    public List<Question> GeneralQuestions { get; set; }
}

public class Question
{
    public int QuestionId { get; set; }
    public string QuestionString { get; set; }
    public ICollection<PossibleAnswer> PossibleAnswers { get; set; }
    public int SelectedAnswerId { get; set; }
}

public class PossibleAnswer
{
    public int Id { get; set; }
    public string Answer { get; set; }
}

and here is the code from the action method:

return View(new StudentViewModel
{
    GeneralQuestions =
        new List<Question>
        {
            new Question
            {
                QuestionString = \"Q1\",
                PossibleAnswers =
                    new[]
                    {
                        new PossibleAnswer {Id = 1, Answer = \"A01\"},
                        new PossibleAnswer {Id = 2, Answer = \"A02\"}
                    }
            },
            new Question
            {
                QuestionString = \"Q2\",
                PossibleAnswers =
                    new[]
                    {
                        new PossibleAnswer {Id = 11, Answer = \"A11\"},
                        new PossibleAnswer {Id = 12, Answer = \"A12\"}
                    }
            },
        }
});