Replicated nested repeater behaviour in MVC - asp.net-mvc

Today I decided to give MVC a go, and although I really like the idea, I found it fairly difficult to transition from ASP.NET and grasp some basic concepts, like using foreach instead of nested repeaters.
It took me good few hours to come up with this solution, but it doesn't seem quite right. Could someone please explain what's wrong with this code, and what the right way to do it is. Here is my solution:
Essentially it's a survey that consists of several questions, each of which has several answers. I have tables in db, which are represented as strongly typed entities. The controller looks like this:
public ActionResult Details(int id)
{
return View(new Models.Entities().Questions.Where(r => r.PROMId == id));
}
and corresponding view like this:
<% foreach (var question in Model) { %>
<h3>Question <%: Array.IndexOf(Model.ToArray(), question) + 1 %></h3>
<p><%: question.QuestionPart1 %></p>
<p><%: question.QuestionPart2 %></p>
<% var answers = new Surveys_MVC.Models.Entities().Answers.Where(r => r.QuestionId == question.QuestionId); %>
<% foreach (var answer in answers) { %>
<input type="radio" /><%: answer.Text %>
<% } %>
<% } %>
All feedback appreciated.

As far as using for loops for the nested repeater behavior, I think that's the best way to do this in MVC. But I would suggest you use dedicated ViewModels.
ViewModel:
public class RadioQuestionListViewModel
{
public IEnumerable<RadioQuestionViewModel> Questions {get;set;}
}
public class RadioQuestionViewModel
{
public int QuestionNumber {get;set;}
public string InputName {get;set;}
public string QuestionPart1 {get;set;}
public string QuestionPart2 {get;set;}
public IEnumerable<RadioAnswerViewModel> PossibleAnswers {get;set;}
}
public class RadioAnswerViewModel
{
public int AnswerId {get;set;}
public string Text {get;set;}
}
Controller:
public ActionResult Details(int id)
{
var model = GetRadioQuestionListModelById(id);
return View(model);
}
View:
<% foreach (var question in Model) { %>
<h3>Question <%: question.QuestionNumber %></h3>
<p><%: question.QuestionPart1 %></p>
<p><%: question.QuestionPart2 %></p>
<% foreach (var answer in question.PossibleAnswers) { %>
<%: Html.RadioButton(question.InputName, answer.AnswerId) %>
<%: answer.Text %>
<% } %>
<% } %>
This approach has a few advantages:
It prevents your view code from depending on your data access classes. The view code should only be responsible for deciding how the desired view model gets rendered to HTML.
It keeps non-display-related logic out of your view code. If you later decide to page your questions, and are now showing questions 11-20 instead of 1-whatever, you can use the exact same view, because the controller took care of figuring out the question numbers to display.
It makes it easier to avoid doing a Array.IndexOf(Model.ToArray(), question) and a database roundtrip inside a for loop, which can become pretty costly if you have more than a few questions on the page.
And of course your radio buttons need to have a input name and value associated with them, or you'll have no way to retrieve this information when the form is submitted. By making the controller decide how the input name gets generated, you make it more obvious how the Details method corresponds to your SaveAnswers method.
Here's a possible implementation of GetRadioQuestionListModelById:
public RadioQuestionListViewModel GetRadioQuestionListModelById(int id)
{
// Make sure my context gets disposed as soon as I'm done with it.
using(var context = new Models.Entities())
{
// Pull all the questions and answers out in a single round-trip
var questions = context.Questions
.Where(r => r.PROMId == id)
.Select(r => new RadioQuestionViewModel
{
QuestionPart1 = r.q.QuestionPart1,
QuestionPart2 = r.q.QuestionPart2,
PossibleAnswers = r.a.Select(
a => new RadioAnswerViewModel
{
AnswerId = a.AnswerId,
Text = a.Text
})
})
.ToList();
}
// Populate question number and name
for(int i = 0; i < questions.Count; i++)
{
var q = questions[i];
q.QuestionNumber = i;
q.InputName = "Question_" + i;
}
return new RadioQuestionListViewModel{Questions = questions};
}

I don't know if it is better, but you can create a helper to do this for you:
public static void Repeater<T>(this HtmlHelper html, IEnumerable<T> items, string cssClass, string altCssClass, string cssLast, Action<T, string> render)
{
if (items == null)
return;
var i = 0;
foreach (var item in items)
{
i++;
if (i == items.Count())
render(item, cssLast);
else
render(item, (i % 2 == 0) ? cssClass : altCssClass);
}
}
Then you can call it like so:
<%Html.Repeater(Model, "css", "altCss", "lastCss", (question, css) => { %>
<h3>Question <%: Array.IndexOf(Model.ToArray(), question) + 1 %></h3>
<p><%: question.QuestionPart1 %></p>
<p><%: question.QuestionPart2 %></p>
<% var answers = new Surveys_MVC.Models.Entities().Answers.Where(r => r.QuestionId == question.QuestionId); %>
<% foreach (var answer in answers) { %>
<input type="radio" /><%: answer.Text %>
<% } %>
<% }); %>
This has a lot of power and the above is just a general example. You can read more here http://haacked.com/archive/2008/05/03/code-based-repeater-for-asp.net-mvc.aspx

Related

MVC Binding to checkbox

I have found so many questions about this, but none of them go over or seem to go over my scenario. I have a model:
public class CheckBoxModel
{
public int Id{ get; set; }
public bool IsSelected { get; set; }
}
In then try and bind my IsSelected bool to a checkbox like this:
<%= Html.CheckBox("IsSelectedCheck",Model.IsSelected)%>
I have lots of items on a page, they all have a checkbox next to them, all i am trying to do is pass back to the controller all the id's of the items and which ones have been selected.
At the moment, the value of IsSelected is always false. Should Html.CheckBox set the value of Model.IsSelected each time the user toggles the checkbox.
Thanks
Try like this:
<%= Html.CheckBoxFor(x => x.IsSelected) %>
Also if you want to pass along the id don't forget to do so:
<%= Html.HiddenFor(x => x.Id) %>
And if you had a collection of those:
public class MyViewModel
{
public CheckBoxModel[] CheckBoxes { get; set; }
}
you could:
<% for (var i = 0; i < Model.CheckBoxes.Length; i++) { %>
<div>
<%= Html.HiddenFor(x => x.CheckBoxes[i].Id) %>
<%= Html.CheckBoxFor(x => x.CheckBoxes[i].IsSelected) %>
</div>
<% } %>
which will successfully bind to:
[HttpPost]
public ActionResult MyAction(MyViewModel model)
{
// model.CheckBoxes will contain everything you need here
...
}
An alternative to Darin's fantastic answer
I definitely recommend following Darin's approach for returning classes which will be most of the time. This alternative is a 'quick' and dirty hack if all you need is the checked Ids:
<% foreach (var cb in Model.CheckBoxes) { %>
<div>
<input type="checkbox"
value="<%= cb.Id %>"
<%= cb.IsSelected ? "checked=\"checked\"" : "" %>
name="ids" />
</div>
<% } %>
Will bind to the int[] ids parameter in the following action:
[HttpPost]
public ActionResult MyAction(int[] ids)
{
// ids contains only those ids that were selected
...
}
The benefit is cleaner html as there is no hidden input.
The cost is writing more code in the view.
In MVC 4.0 (Razor 2.0) you can use the following syntax in your view:
<input type="checkbox" value="#cb.Id" checked="#cb.IsSelected" name="ids" />

MVC design question

I'm building a questionnaire. The questionnaire have multiple sections, each section has multiple questions, and each question can have one to many answers. Each question can be a different type (radio buttons, checkbox, text...).
I put my tables in the model and loop through the sections table to display sections, loop through questions to display questions, loop through answerOptions to populate answers:
<fieldset>
<legend>Fields</legend>
<%foreach (var s in Model.Sections)
{ %>
<h3><%=s.SCTN_TXT %></h3>
<% var QuestsInSect = Model.GetQuestionsBySectionID(s.SCTN_ID);%>
<%foreach (var q in QuestsInSect){%>
<h4><%=q.QSTN_TXT %><%=q.QSTN_ID.ToString() %></h4>
<% var answers = Model.GetAnswerOptionByQuestionID(q.QSTN_ID); %>
<%if (q.QSTN_TYP_ID>= 3)
{%>
<%:Html.TextBox(q.QSTN_ID.ToString())%>
<%}
else if (q.QSTN_TYP_ID == 1)
{ %>
<%var answerOptions = Model.GetDropDownListAnswerOptionByQuestionID(q.QSTN_ID);%>
<%:Html.DropDownList(q.QSTN_ID.ToString(), answerOptions)%>
<%}
else
{ %>
<% foreach (var ao in answers)
{ %>
<br />
<%:Html.CheckBox(q.QSTN_ID.ToString())%>
<%=ao.ANS_VAL%>
<% }
}
}
} %>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
In my controller I loop through collection.Allkeys to figure out the answer for each question:
[HttpPost]
public ActionResult Create(FormCollection collection)
{
try
{
// TODO: Add insert logic here
List<ASSMNT_RESP> arList = new List<ASSMNT_RESP>();
foreach (string key in collection.AllKeys)
{
QSTN q = _model.GetQuestionByQuestionID(int.Parse(key));
IEnumerable<ANS_OPTN> aos = _model.GetAnswerOptionByQuestionID(int.Parse(key));
ASSMNT_RESP ar = new ASSMNT_RESP();
ar.QSTN_ID = int.Parse(key);
ar.ASSMNT_ID = 1;
if (q.QSTN_TYP_ID == 1)//dropdown
{
//do something
}
else if (q.QSTN_TYP_ID == 2)//checkboxlist
{
//do something
}
else
{
//do something
}
//_model.AddAssessmentResponse(ar);
System.Diagnostics.Trace.WriteLine(key + "---"+ collection[key]);
}
//_model.Save();
return RedirectToAction("Index");
}
catch
{
return View();
}
}
It works, but I just don't think it's a very good design. It seems like I have too much logic in the view. I would like to move the logic in the view and controller to the model. Can you recommend an easier/cleaner way to do this?
Thanks.
I'm not an expert on MVC, but I think you'd get a lot of benefit out of using a strongly-typed view based on a custom model class that you build. The properties exposed by such a model can be nested (i.e., the top-level model can consist of properties each of which are also custom classes). Conceptually, something like:
public class MyTopLevelModel
{
public MySubModel1 SubModel1 { get; set; }
public MySubModel2 SubModel2 { get; set; }
}
public class MySubModel1
{
public string AProperty { get; set; }
public int AnotherProperty { get; set; }
}
You can include collections in the class definitions, too. And then you can decorate the individual properties with with attributes specifying whether a particular property is required, a range of permissible values, etc.
It's a big subject, though, and this only scratches the surface. FWIW, I've gotten a LOT out of Steven Sanderson's Pro ASP.NET MVC2 Framework book.
Make your viewmodel more explicit.
public class ViewModel
{
public IList<SectionViewModel> Sections {get;set;}
}
public class SectionViewModel
{
public IList<QuestionViewModel> Questions {get;set;}
}
public class QuestionViewModel
{
public IList<AnswerViewModel> Answers {get;set;}
}
In your view you can then do something like this (I'm using razor):
#foreach(var section in Model.Sections)
{
<h3>#section.Name</h3>
foreach(var question in section.Questions)
{
<h4>#question.Name</h4>
foreach(var question in section.Questions)
{
#Html.EditorFor(x=> question.Answers)
}
}
}
Then create an EditorTemplate for your AnswerViewModel.

Can 2 Linq queries be passed to 1 View?

I have a view that is outputting a list of questions(yesno and multiple choice). For the Multiple Choice questions, I want to also pass the possible multiple choice answers but I can not figure out how to pass 2 queries. Is there a way to do this or is it better handled another way?
Controller Code:
public ActionResult Test(int id)
{
var listofquestions = from m in db.vQuestionnaireQuestions
where m.Questionnaire_ID.Equals(id)
select m;
return View("Test", listofquestions.ToList());
}
View Code:
<% foreach (var item in Model)
{ %>
<br />
<%: item.Question_Text %>
<%if (item.Question_Type_ID == 1) //Yes-No Question
{ %>
//Yes-No Stuffs
<% }
else if (item.Question_Type_ID == 2) //Multiple Choice
{ %>
//Can I access a Linq query again here?
//I have Question_ID to use, but I don't think
//I can have 2 Models
<% }
else //All Else
{ %>
//All Else Stuffs
<% }
} %>
EDIT
I've created a view model class
View Model Class Code:
public IEnumerable<vQuestionnaireQuestion> FindAllQuestionnaireQuestionsTest()
{
return db.vQuestionnaireQuestions;
}
public vQuestionnaireQuestion GetQuestionnaireQuestionsTest(int id)
{
return db.vQuestionnaireQuestions.FirstOrDefault(q => q.Questionnaire_ID == id);
}
public IEnumerable<Multiple_Choice_Answers> FindAllMultipleChoiceAnswersTest()
{
return db.Multiple_Choice_Answers;
}
public Multiple_Choice_Answers GetMultipleChoiceAnswersTest(int id)
{
return db.Multiple_Choice_Answers.FirstOrDefault(q => q.Question_ID == id);
}
and added it to the inherits of my view:
<%# Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<IEnumerable<QuestionnaireApp.Models.Questionnaire>>" %>
The model information does not seem to be making it as now all my item.fieldname's are coming back as not having a definition. Am I over complicating this?
Try wrapping the results in a view model class and pass that to View
class SomeClassName {
IEnumerable<Question> ListOfQuestions;
IEnumerable<Answer> ListOfAnswers;
}
Many people create a class specifically for the purpose fo passing data between controller and view. That way you can define whatever properties you need, and an instance of your class can easily have one or more LINQ result sets in it.

How ASP.NET MVC: How can I bind a property of type List<T>?

Let's say I've the bellow model
public class UserInformation
{
public List<UserInRole> RolesForUser { get; set; }
//Other properties omitted...
}
public class UserInRole
{
public string RoleName { get; set; }
public bool InRole { get; set; }
}
On my page I have something like
<%using(Html.BeginForm()){%>
.../...
<%for(int i =0; i<Model.InRoles.Cout; i++%>
<p><%: Html.CheckBox(Model.Roles[i].RoleName, Model.Roles[i].InRole)%></p>
<%}%>
The idea is to be able to check/uncheck the checkbox so that when the form is posted to the action, the action acts appropriately by adding/removing the user from each role.
The problem is when form is posted to the action method, the Roles property (which is a list UserInRole object) doesn't reflect the change made by the user. ModelBinder works properly on the all other properties but 'Roles property'
I wonder how I can do that. I suspect that the name/id given to the checkbox is not appropriate. But, I'm just stack. Maybe I should do it differently.
Thanks for helping
You should see Phil Haack's post on model binding to a list.
Essentially what you need to is simply submit a bunch of form fields each having the same name.
<%# Page Inherits="ViewPage<UserInformation>" %>
<% for (int i = 0; i < 3; i++) { %>
<%: Html.EditorFor(m => m.RolesForUser[i].RoleName) %>
<%: Html.EditorFor(m => m.RolesForUser[i].InRole) %>
<% } %>
I think the problem is how your submitting your form data. For model binding to work it needs the key name with its associated value. The below code based on your code should bind correctly:
<%using(Html.BeginForm()){%>
.../...
<%for(int i =0; i<Model.RolesForUser.Count; i++%>
<p>
<%: Html.Hidden("UserInformation.RolesForUser[" + i + "].RoleName", Model.RolesForUser[i].RoleName) %>
<%: Html.CheckBox("UserInformation.RolesForUser[" + i + "].InRole", Model.RolesForUser[i].InRole) %>
<%: Model.RolesForUser[i].RoleName %>
</p>
<%}%>

How to show nested data using MVC Views and PartialViews

The problem I am working on is very similar to the way StackOverflow is displaying Question, its comments, Posts and comments related to the Posts. The hierarchy is the same.
How is this accomplished in ASP.Net MVC?
So far I have this: (I have named the files similar to SO site to make my question more readable)
Views/Questions/Details.aspx
public class QuestionsController : Controller
{
public ActionResult Details(int? questionId)
{
Question question= _db.Question .First(i => i.QuestionId== questionId);
return View(question);
}
}
This loads the details and display the question.
I have a user control called QuestionComment, which should display the comments for the question but I am not sure how to go about wiring it up. I have been using the Dinners solution as a guide.
Create ViewModel for displaying Question with Comments. Something like this:
public class QuestionViewModel
{
public Question Question { get; set; }
public IEnumerable<Comment> Comments { get; set; }
}
your controller become:
public class QuestionsController : Controller
{
public ActionResult Details(int? questionId)
{
var question = _db.Question.First(x => x.QuestionId == questionId);
var comments = _db.Comment.Where(x => x.QuestionId == questionId).ToList();
var model = new QuestionViewModel {
Question = question,
Comments = comments
};
return View("Details", model);
}
}
your "Details" View:
<%# Page Inherits="System.Web.Mvc.ViewPage<QuestionViewModel>" %>
<% Html.Renderpartial("QuestionControl", model.Question); %>
<% Html.Renderpartial("CommentsControl", model.Comments); %>
"QuestionControl" partial View:
<%# Control Inherits="System.Web.Mvc.ViewUserControl<Question>" %>
<h3><%= Model.Title %></h3>
...
"CommentsControl" partial View:
<%# Control Inherits="System.Web.Mvc.ViewUserControl<IEnumerable<Comment>>" %>
<ul>
<% foreach (var comment in Model) { %>
<li>
<%= comment.Content %>
</li>
<% } %>
</ul>
...
In your view write something like this;
<% foreach (var question in Model.Questions) { %>
<%=question.bodyText%>
<%}%>
Hope this helps, if not post a comment and I'll be less cryptic.

Resources