24

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?

bambi
  • 1,159
  • 2
  • 14
  • 31
  • 1
    Just a quick response but have you seen this article? http://stackoverflow.com/a/22178728/1765853 – macoms01 Jan 20 '15 at 21:22
  • So long as `QuestionString` is unique, this should be creating a group for each question although binding to `QuestionString` seems stange - shouldn't you be binding to something like `SelectedAnswer`? Can you show some of the html this is generating –  Jan 20 '15 at 21:27
  • Your outer loop should also be a `for` loop so that your controls are property named with indexers can bind this on post back. –  Jan 20 '15 at 21:30
  • @StephenMuecke Why do you think the answer that was deleted is not right? Previously, generated html was like this: ``. And after the change it became like this: ``. So it seems fine to me. – bambi Jan 20 '15 at 21:44
  • @bambiinela, There are numerous issue with you code including invalid html (duplicate id's), incorrect usage of the outer `foreach` loop which will prevent binding to a collection on post back and binding to an incorrect property. The deleted answer does not solve your problems. You need to post your model and I can give you an answer. –  Jan 20 '15 at 21:49
  • @StephenMuecke, I have just edited my question. So can you help me now? Please have in mind that I am just beginner in web application development. – bambi Jan 20 '15 at 22:21
  • Sure, but its not clear from your model what to bind the selected answer to. Each question has a collection of possible answers, but you need a property to bind the selected answer to e.g. `public string SelectedAnswer { get; set; }` (also I'm not sure what the `Results` property is - is that somehow related to this - can you post the model for it as well?) –  Jan 20 '15 at 22:33
  • Yes, the model seems to be bad. Since I have general questions and questions that are related to subjects (e.g. Mathematics and Physics both have same questions), I didn't want to duplicate questions in database so I didn't put property like SelectedAnswer {get; set;} in Question class. I will add now Results class to question. – bambi Jan 20 '15 at 22:44
  • @StephenMuecke, property PossibleAnswer in Results has meaning of SelectedAnswer you mentioned. – bambi Jan 20 '15 at 22:51
  • Your models as they currently stand are not going to allow you to do what you want (at least easily). I'll post an answer shortly using a view model with some explanations of your other issues (but I suspect because of your database structure, mapping to and from you data models will be more complex than it needs to be) –  Jan 20 '15 at 23:18

2 Answers2

29

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

niico
  • 11,206
  • 23
  • 78
  • 161
  • Thank you! All advices were helpful and the code is correct. Now I have separated model for MVC and entites for saving in database since my entites in database should look a bit different from MVC model. – bambi Jan 21 '15 at 15:02
  • is there any way for posting back question texts, subject names etc. in StudentVM object? All I get are IDs of answered questions in post method (`[HttpPost] public ActionResult Student(StudentVM model) {}`), other fields seem to be empty. – bambi Jan 22 '15 at 00:28
  • As I noted, you can create hidden inputs for those properties in additional to `@Html.DisplayFor()` - e.g. `@Html.HiddenFor(m => m.Name)` or you can replace the `DisplayFor` with `@Html.TextBoxFor(m => m.Name, new { @readonly = "readonly", @class="readonly")` and then use css to style it. However creating a whole lot of unnecessary hidden inputs can degrade performance (sending it to the client and then back again) and opens you to overposting attacks. Often its better to just get the data from the database again but only you can assess whats the best option. –  Jan 22 '15 at 00:51
  • I am confused as to why the name is generated as you have listed above, as a string representation of the object hierarchy with only the counters changing. Are these names then used behind the scenes in the binding mechanism? – GilShalit Nov 15 '18 at 12:51
  • @GilShalit, Yes they are. Perhaps the explanation in [Post an HTML Table to ADO.NET DataTable](http://stackoverflow.com/questions/30094047/html-table-to-ado-net-datatable/30094943#30094943) will help you to understand it –  Nov 15 '18 at 20:33
6

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"}
                    }
            },
        }
});
sudheeshix
  • 1,541
  • 2
  • 17
  • 28