Reputation: 487
I Have the following in my razor view
@foreach (var group in Model.QuestionList.GroupBy(x => x.AreaName))
{
<h4>@group.Key</h4>
for (int i = 0; i < Model.QuestionList.Count(x => x.AreaName == group.Key); i++)
{
<div class="form-group">
<div class="row">
<div class="col-md-4">
@Html.DisplayFor(x => Model.QuestionList[i].Question)
</div>
<div class="col-md-2">
@Html.HiddenFor(x => Model.QuestionList[i].StnAssureQuestionId)
@Html.DropDownListFor(model => model.QuestionList[i].Score, new SelectList(Model.QuestionList[i].Scores, "ScoreId", "ScoreNum", 0), "Please Select", new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.QuestionList[i].Score, "", new { @class = "text-danger" })
</div>
<div class="col-md-4">
@Html.EditorFor(x => Model.QuestionList[i].Comments, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.QuestionList[i].Comments, "", new { @class = "text-danger" })
</div>
</div>
</div>
}
}
I want to be able to display all the objects in QuestionList but group them by AreaName, which is the group key.
The current code displays the title of the first group then the questions in that group but after that all it does is display the next group name followed by the same questions then again for all the group.
It's a no brainer I'm sure but I'm still not skilled enough to spot it.
Upvotes: 2
Views: 4552
Reputation:
You should not be using complex queries in your view, and while the JamieD77's answer will solve the issue of correctly displaying the items, it will fail to bind to your model when you submit.
If you inspect the html you generating you will see that for each group you have inputs such as
<input type="hidden" name="questionList[0].StnAssureQuestionId" ... />
<input type="hidden" name="questionList[1].StnAssureQuestionId" ... />
but the DefaultModelBinder
requires collection indexers to start at zero and be consecutive so when binding, it will correctly bind the inputs in the first group, but ignore those in all other groups because the indexers starts back at zero.
As always start with view models to represent waht you want to display/edit (What is ViewModel in MVC?). In this case I'm assuming the SelectList
options associated with Score
are common across all Questions
public class QuestionnaireVM
{
public IEnumerable<QuestionGroupVM> Groups { get; set; }
public SelectList Scores { get; set; }
}
public class QuestionGroupVM
{
public string Name { get; set; }
public IEnumerable<QuestionVM> Questions { get; set; }
}
public class QuestionVM
{
public int ID { get; set; }
public string Question { get; set; }
public int Score { get; set; }
public string Comments { get; set; }
}
While you could use nested loops in the view
for (int i = 0; i < Model.Groups.Count; i++)
{
<h4>Model.Groups[i].Name</h4>
for (int j = 0; j < Model.Groups[i].Count; j++)
{
@Html.DisplayFor(m => m.Groups[i].Questions[j].Question)
a better solution is to use EditorTemplates
which give you a reusable component (and you would not have to change the collection properties to IList<T>
). Note I have omitted <div>
elements to keep it simple.
In /Views/Shared/EditorTemplates/QuestionVM.cshtml
@model QuestionVM
@Html.DisplayFor(m => m.Question)
@Html.HiddenFor(m => m.ID)
@Html.DropDownListFor(m => m.Score, (SelectList)ViewData["scores"], "Please Select", new { @class = "form-control" })
@Html.ValidationMessageFor(m => m.Score, "", new { @class = "text-danger" })
@Html.EditorFor(m => m.Comments, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(m => m.Comments, "", new { @class = "text-danger" })
In /Views/Shared/EditorTemplates/QuestionGroupVM.cshtml
@model QuestionGroupVM
<h4>@Html.DisplayFor(m => m.Name)</h4>
@Html.EditorFor(m => m.Questions, new { scores = ViewData["scores"] })
and the main view would be
@model QuestionnaireVM
@using (Html.BeginForm())
{
@Html.EditorFor(m => m.Groups, new { scores = Model.Scores })
<input type="submit" value="Save" />
}
Then in the get method, project your data model to the view model, for example
QuestionnaireVM model = new QuestionnaireVM
{
Groups = db.Questions.GroupBy(x => x.AreaName).Select(x => new QuestionGroupVM
{
Name = x.Key,
Questions = x.Select(y => new QuestionVM
{
ID = y.StnAssureQuestionId,
Question = y.Question,
Score = y.Score,
Comments = y.Comments
}
},
Scores = new SelectList(.....)
};
return View(model);
and the signature of the POST method would be
public ActionResult Edit(QuestionnaireVM model)
Side note: You do not currently have an input for the group name property which means if you needed to return the view because ModelState
was invalid, you would need to run the query again, so consider adding @Html.HiddenFor(m => m.Name)
to the QuestionGroupVM.cshtml
template (and of course if you do retur the view, you also need to reassign the SelectList
property.
Upvotes: 3
Reputation: 13949
You might be able to get by with something like this, but I'd take other's advice about creating a specific view model for this view also.
@foreach (var group in Model.QuestionList.GroupBy(x => x.AreaName))
{
var questionList = group.ToList();
<h4>@group.Key</h4>
for (int i = 0; i < questionList.Count(); i++)
{
<div class="form-group">
<div class="row">
<div class="col-md-4">
@Html.DisplayFor(x => questionList[i].Question)
</div>
<div class="col-md-2">
@Html.HiddenFor(x => questionList[i].StnAssureQuestionId)
@Html.DropDownListFor(model => questionList[i].Score, new SelectList(questionList[i].Scores, "ScoreId", "ScoreNum", questionList[i].Score), "Please Select", new { @class = "form-control" })
@Html.ValidationMessageFor(model => questionList[i].Score, "", new { @class = "text-danger" })
</div>
<div class="col-md-4">
@Html.EditorFor(x => questionList[i].Comments, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => questionList[i].Comments, "", new { @class = "text-danger" })
</div>
</div>
</div>
}
}
Upvotes: 6
Reputation: 344
Please consider that a View should be completely agnostic to the business logic implemented by the service layer. View is just a dummy presentation mechanism which grabs data served by a Controller through a ViewModel and displays the data. It is the basic of the MVC architecture and my strong recommendation is that following the architecture is itself a very good reason to go the right way.
That being said the view you have is getting messy. Consider reconstructing it as something like this:
public class QuestionDataViewModel
{
public List<QuestionData> Data { get; set; }
}
public class QuestionData
{
public string AreaName { get; set; }
public List<Question> QuestionList { get; set; }
}
public class Question
{
public int StnAssureQuestionId { get; set; }
public int Score { get; set; }
public IEnumerable<SelectListItem> Scores { get; set; }
public List<Comment> Comments { get; set; }
}
Construct this server side and just render through simple razor foreach loops. This will not only benefit you with cleaner code but will also help avoid the model binding pain you are about to run into when you post the form back with your current implementation.
Upvotes: 1
Reputation: 12491
When you displaying your questions you should work with your group
object (grouped collection) but not with the initial collection.
I mean you should change your
Model.QuestionList[i]
To
group.Select(x => x.QuestionList)[i]
Anyway your solution really messy It's better to do such grouping on server side.
Upvotes: 2