1

I have an application to create and edit surveys. Each survey contains a set of questions and associated responses (answers). When a survey is created, the collection of questions is generated from a separate Questions table. Each year, a new survey is created for each user with the same set of questions so that responses can be compared over time.

When a survey is created, the answers to each question are saved, but a user may not have given a response to each question and now I have to build an view to edit existing responses.

Models

public class Survey
{
    public int ID { get; set; }
    public int AreaID { get; set; }
    public Status Status { get; set; }
    public DateTime AssessmentDate { get; set; }
    public virtual Area Area { get; set; }
    public virtual ICollection<Answer> Answers { get; set; }
}
public class Question
{
    public int ID { get; set; }
    public string QuestionText { get; set; }
    public virtual ICollection<Answer> Answers { get; set; }
}
public class Answer
{
    public int ID { get; set; }
    public int? Response { get; set; }
    public int QuestionID { get; set; }
    public int SurveyID { get; set; }
    public virtual Question Question { get; set; }
    public virtual Survey Survey{ get; set; }
}

Here is my viewmodel that I use to create my view for the edit screen

public class SurveyResponseViewModel
{
    public Assessment Assessment { get; set; }
    public IEnumerable<Question> Questions { get; set; }
}

and the code in the GET method is

public ActionResult Edit(int? id) 
{ 
    if (id == null) 
    { 
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest); 
    } 
    Survey survey = db.Surveys.Find(id); 
    var viewModel = new SurveyResponseViewModel 
    { 
        Survey = survey, 
        Areas = new SelectList(db.Areas, "ID", "SubLevel").ToList(), 
        Questions = db.Questions.Where(q => q.isActive) 
    }; 
    if (survey == null) 
    { 
        return HttpNotFound(); 
    } 
    return View(viewModel); 
}

This populates my view model with all questions, but each question contains a collection of answers. How do I display and edit only the answer to each question associated with this survey in the view?

@foreach (var question in Model.Questions)
{
    // Display the question
    @Html.Raw(question.QuestionText)
    // How to create an input for the associated response??
    <input  type="text" name="????" placeholder="Enter a number..." value="????" />
}

Note that the response is int? and can have a value between 0 and 5 (or null if the user has not yet given the response). Ideally I would like this to be rendered as a radio buttons to select the possible values.

totalitarian
  • 3,606
  • 6
  • 32
  • 55
  • First you cannot use a `foreach` loop to generate form controls for a collection (refer [this answer](http://stackoverflow.com/questions/30094047/html-table-to-ado-net-datatable/30094943#30094943)). Second, (I assume) a question will only have one answer so you view model for a `Question` should have one `Answer`, not a collection of `Answer` –  Nov 08 '16 at 21:38
  • @StephenMuecke a survey has a set list of questions, but as there can be many instances of survey, a question can have an answer in in each survey. For example question 1 in survey 1 may have answer of 3 while in a different survey the same question may have an answer of 7 – totalitarian Nov 08 '16 at 21:47
  • In that case, why not use another foreach loop? – Pratik Gaikwad Nov 08 '16 at 21:49
  • @PratikGaikwad to loop what? – totalitarian Nov 08 '16 at 21:51
  • Yes, but your editing the answer for a question (there is only one answer per question per survey). You need a `QuestionViewModel` with properties for `QuestionText` and `AnswerText` (not a collection of answers) and the `SurveyResponseViewModel` will contain properties for the `Assessment` (not `Assessment` itself) and `List` –  Nov 08 '16 at 21:51
  • @totalitarian, if you're saying that multiple answers are possible for a single question, why not just use another foreach loop for the answers too? – Pratik Gaikwad Nov 08 '16 at 21:52
  • @StephenMuecke Thanks, that seems logical but how can I populate that QuestionViewModel with both the question and the answer? – totalitarian Nov 08 '16 at 21:53
  • Cant answer that without knowing the exact database structure, but assuming your `Answer` data model includes a FK property for the `QuestionID`, then you should be able to get the answer associated with each question using a `db.Answers.FirstOrDefault(x => x.QuestionID == questionID);` –  Nov 08 '16 at 21:57
  • @StephenMuecke I'm not sure what that does. Surely a I need a list of questions with corresponding answers.. – totalitarian Nov 08 '16 at 22:03
  • But a question has only one answer per survey/user. You need to get all the questions associated with the survey/user, and then for each question get the (single) answer that has been added for that question by the user –  Nov 08 '16 at 22:06
  • @StephenMuecke I see, but doesn't that mean I have to hit the database for each question. Isn't there a way to do it with one database call? – totalitarian Nov 08 '16 at 22:07
  • If you navigation properties are all set up correctly, then your `ICollection Answers` will be loaded when you get the survey so you can just query that property to find the associated answer you want to edit in the view (I don't know the db structure and if you need to find it by `QuestionID`, and/or `SurveyID`) –  Nov 08 '16 at 22:14
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/127675/discussion-between-totalitarian-and-stephen-muecke). – totalitarian Nov 08 '16 at 22:18

1 Answers1

1

First you will need to create a view model representing a question and its answer

public class QuestionVM
{
    public int QuestionID { get; set; }
    public string QuestionText { get; set; }
    public int? AnswerID { get; set; }
    [Range(0, 5)]
    public int? Response { get; set; }
}

and the main view model will be (note that view models should not contain properties that are data models when editing)

public class SurveyResponseVM
{
    public int ID { get; set; } // this will be bound by the route value
    [Required(ErrorMessage = "Please select an area")]
    [Display(Name = "Area")]
    public int? SelectedArea { get; set; }
    public IEnumerable<SelectListItem> AreaList { get; set; }
    .... // other properties of assessment that you need for the view
    public List<QuestionVM> Questions { get; set; }
}

so that in the view you can use

@model SurveyResponseVM
....
@using (Html.BeginForm())
{
    @Html.DropDownListFor(m => m.SelectedArea, Model.AreaList)
    ....
    for(int i = 0; i < Model.Questions.Count; i++)
    {
        <h3>@Model.Questions[i].QuestionText</h3>
        @Html.HiddenFor(m => m.Questions[i].AnswerID)
        for (int j = 0; j < 6; j++)
        {
            <label>
                @Html.RadioButtonFor(m => m.Questions[i].Resonse, j, new { id = "" })
                <span>@j</span>
            </label>
        }
    }
    <input type="submit" value="Save" />
}

which will post back to

public ActionResult Edit(SurveyResponseVM model)

For getting the values in the GET method, your 2nd query (db.Questions.Where(q => q.isActive) seems unnecessary as you already have the collection of Answer associated with the Survey (and each Answer contains the Question property. Your code can be

Survey survey = db.Surveys.Find(id);
if (survey == null) // check for null here
{ 
    return HttpNotFound(); 
} 
IEnumerable<Area> areas = db.Areas;
SurveyResponseVM model = new SurveyResponseVM()
{
    ID = survey.ID,
    SelectedArea = survey.AreaID,
    .... // other properties of Survey as required for the view
    AreaList = new SelectList(areas , "ID", "SubLevel"),
    Questions = survey.Answers.Select(x => new QuestionVM() 
    {
        QuestionText = x.Question.QuestionText, 
        AnswerID = x.ID, 
        Response = x.Response 
    }) 
};
return View(model);
  • Thanks again. I've incorporated this into my application and everything running smoothly :) – totalitarian Nov 09 '16 at 21:07
  • 1
    That's really up to you. Putting the code for mapping the data model to a view model in a separate service, (or using tools such as [automapper](http://automapper.org/)) makes your controller code cleaner and its my preference. I typically use something like `MyViewModel model = Map(dataModel); `ConfigureViewModel(model);` where `Map()` return the view model based the data model, and `ConfigureViewModel()` creates the `SelectList`'s –  Nov 10 '16 at 23:55