I am trying to bind a view to a model which contains a list in a list. Naturally I would prefer to use out of the box model binding. Having spent some time on it yesterday I found a workaround which is really a hack and I would like to correct this. The basic structure of my models are as follows:
public class MyMPIModel
{
public List<ScoreInfo> ScoreInfo { get; set; }
}
public class ScoreInfo
{
public int ScorePrefId { get; set; }
public List<Category> Categories { get; set; }
}
public class Category
{
public int Id;
public string Name;
public bool Checked;
}
The view InterestCategories.cshtml contains the following form:
#using (Html.BeginForm())
{
for (var i = 0; i < Model.ScoreInfo.Count; i++)
{
#Html.EditorFor(x => x.ScoreInfo[i])
}
}
The editor template ScoreInfo.cshtml:
#Html.HiddenFor(x => x.ScorePrefId)
<div class="preferences-block">
#for (var i = 0; i < Model.Categories.Count; i++)
{
#Html.EditorFor(x => x.Categories[i])
}
</div>
Finally the editor template Category.cshtml:
#Html.HiddenFor(x => x.Id)
#Html.HiddenFor(x => x.Name)
<label>
#Html.CheckBoxFor(x => x.Checked, new { #class = "check"})
<span>#Model.Name</span>
</label>
Inspecting the form using firebug I can see that all the hidden fields have been populated. Also when I submit the form, Fiddler shows the correct data. Here is a sample:
ScoreInfo[0].Categories[1].Id 2
ScoreInfo[0].Categories[1].Name Managing Money
ScoreInfo[0].Categories[1].Checked false
However, when I post to the controller, set a breakpoint and inspect the model, the list of ScoreInfo objects have been populated but the lists of Category objects inside the ScoreInfo object have not.
I have the following POST action in my controller:
[HttpPost]
public ActionResult InterestCategories(MyMPIModel model, FormCollection form)
{
...
// model would not bind forcing me to populate via form collection
for (var i = 0; i < model.ScoreInfo.Count; i++)
{
...
for (var j = 0; j < scoreInfo.Categories.Count; j++)
{
var category = scoreInfo.Categories[j];
var prefix = "ScoreInfo[" + i + "].Categories[" + j + "]";
category.Name = form[prefix + ".Name"];
var sId = form[prefix + ".Id"];
if (sId != null) category.Id = Int32.Parse(sId);
var sChecked = form[prefix + ".Checked"];
if (sChecked != null) category.Checked = sChecked.Contains("true");
}
}
}
You have to use Properties instead of Fields in your Category class:
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public bool Checked { get; set; }
}
Related
I am trying to display a checklist that gets data from MySQL Database and displays it in a view and updates the value of the variable (IsChecked) of each element in the table by whether we have checked the amenity or not (i am displaying some amenities). The model of the view is Hotel_5.ViewModel.BookingRoom, where BookingRoom is a custom model i created where i use multiple models. I get the exception at Model.AmenitiesList.Count(). The model is null.
This is my view
<div class="form-group">
#for (var i = 0; i < Model.AmenitiesList.Count(); i++)
{
#Html.CheckBoxFor(m => m.AmenitiesList[i].IsChecked, new { #class = "form-control" });
<label>#Model.AmenitiesList[i].amenityType</label>
//If you need to hide any values and get them in your post
#Html.HiddenFor(m => m.AmenitiesList[i].AmenityId)
#Html.HiddenFor(m => m.AmenitiesList[i].AmenityPrice)
}
</div>
This is my ViewModel
public class BookingRoom
{
public Bookings bookings { get; set; }
public Rooms rooms { get; set; }
public List<Amenities> AmenitiesList { get; set; } = new List<Amenities>();
}
This is my Amenities Model
public class Amenities
{
[Key]
public int AmenityId { get; set; }
public double AmenityPrice { get; set; }
public AmenityType amenityType { get; set; }
public bool IsChecked { get; set; }
}
public enum AmenityType
{
tv,
wi_fi,
hair_dryer,
help
}
When Querying you should Include its AmenitiesList too, otherwise it will be null:
In Controller:
var bookingRoom = context.BookingRooms.Include(b => b.AmenitiesList).FirstOrDefault(b => b.Id == someId);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
please note that what I queried might not be what you want, it is just to demonstrate how to use Include() and also you should add using Microsoft.EntityFrameworkCore.
How do I get my editor template field to apply .input-validation-error css class for custom validation on a collection field property?
Normally I'd add a decorator directly on the view model property but in this case, the validation attribute is on the parent view model for the collection.
I tried to apply the answer similar to what was given here:
https://stackoverflow.com/a/8573879/181473
Here is an example with custom validation attribute on the collection property:
public class ParentViewModel
{
public ParentViewModel()
{
Quantities = new List<Quantity>();
}
[Required]
public string Title { get; set; }
[CustomValidation(typeof(ParentViewModel), "ValidateQuantities")]
public List<Quantity> Quantities { get; set; }
public static ValidationResult ValidateQuantities(List<Quantity> quantities , ValidationContext context)
{
ValidationResult result = null;
ParentViewModel vm = (ParentViewModel)context.ObjectInstance;
if (quantities.Sum(m => m.Amount) != 21)
{
var memberNames = new List<string>();
for (int i = 0; i < quantities.Count; i++)
{
string memberName = context.MemberName + "[" + i.ToString() + "]." + nameof(Quantity.Amount);
memberNames.Add(memberName);
}
// Not working, adding member name to result does not work:
result = new ValidationResult("Amounts must add up to 21.", memberNames);
}
return result;
}
}
public class Quantity
{
public int? Id { get; set; }
[Required]
public int? Amount { get; set; }
}
Here is my Controller:
public class FormController : Controller
{
[HttpGet]
public ActionResult Index()
{
var viewModel = new ParentViewModel();
viewModel.Quantities = new List<Quantity>()
{
new Quantity(){ Id = 1000 },
new Quantity(){ Id = 1001 },
new Quantity(){ Id = 1002 },
};
return View(viewModel);
}
[HttpPost]
public ActionResult Index(ParentViewModel viewModel)
{
ModelState.Clear();
TryValidateModel(viewModel);
for (int i = 0; i < viewModel.Quantities.Count; i++)
{
string prefix = nameof(viewModel.Quantities) + "[" + i.ToString() + "]";
TryValidateModel(viewModel.Quantities[i], prefix);
}
return View(viewModel);
}
}
This is the Form razor template:
#* Views/Form/Index.cshtml *#
#model Mvc5App.Controllers.ParentViewModel
<style>
.input-validation-error {
background: red;
}
</style>
<br>
#using (Html.BeginForm("Index", "Form", FormMethod.Post))
{
#Html.ValidationSummary();
#Html.LabelFor(m => m.Title)
#Html.TextBoxFor(m => m.Title)
<br>
for (int i = 0; i < Model.Quantities.Count; i++)
{
#Html.EditorFor(m => m.Quantities[i], "Form/EditorTemplates/Quantity");
<br>
}
<button type="submit" name="SubmitAction" value="SubmitAction" class="btn btn-primary">Submit</button>
}
And this is the editor template:
#* Views/Form/EditorTemplates/Quantity.cshtml *#
#model Mvc5App.Controllers.Quantity
#Html.HiddenFor(m => m.Id)
#Html.LabelFor(m => m.Amount)
#Html.TextBoxFor(m => m.Amount)
When I call TryValidateModel(viewModel); in the POST route the "ParentViewModel.Title" field gets the class applied that changes the background color to red.
But all the "ParentViewModel.Quantities" Amount fields don't get the .input-validation-error css class which I guess means ModelState isn't aware that these fields have an issue.
How can I get all the amount fields to turn red if that custom validation result fails validation?
I can't find or figure out how to take a list of items (cupcakes) and display them in razor with a quantity field.
What is happening is I am not able to get the values for each cupcake quantity in the list. Can you do textbox arrays in Razor?
VIEW
<div class="form-group">
<label>Cupcakes</label>
#foreach (var cupcake in Model.CupcakeList)
{
#Html.TextBox("CupcakeQuantities", cupcake.Id) #cupcake.Name <br/>
}
</div>
MODEL
public List<Cupcake> CupcakeList { get; set; }
public List<int> CupcakeQuantities { get; set; }
CONTROLLER
public ActionResult Create()
{
var model = new PartyBookingModel()
{
CupcakeList = db.Cupcakes.ToList(),
CupcakeQuantities = new List<int>()
};
return View(model);
}
CUPCAKE (ENTITY)
public class Cupcake
{
public int Id { get; set; }
public string Name { get; set; }
public decimal PerDozen { get; set; }
}
You have to use an index, rather than foreach for it to work.
#for (int i = 0; i < Model.CupcakeList.Count; i++)
{
#Html.TextBoxFor(x=>Model.CupcakeQuantities[i]) #Model.CupcakeList[i].Name <br/>
}
This will create sequentially named+number entries that will be recombined back into the model on post back.
I realise this may seem like "why doesn't foreach work?", but with foreach there is not enough reflected information available to TextBoxFor (as it is just a single object), whereas the array index is extracted by reflection from the Model.CupcakeQuantities[i] expression.
The receiving controller method should take the same as the model passed to the view:
e.g.
[HttpPost]
public ActionResult(PartyBookingModel model)
Try it this way:
view:
#for (int i = 0; i < Model.Count; i++)
{
#Html.HiddenFor(x=>Model[i].Id) #Model[i].Name
#Html.TextBoxFor(x => Model[i].Quantity) <br/>
}
model:
public class CupcakeViewModel
{
public int Id {get;set;}
public string Name {get;set;}
public int Quantity {get;set;}
}
controller:
public ActionResult Create()
{
var model = db.Cupcakes.Select(c => new CupcakeViewModel {
Id = c.Id,
Name = c.Name,
Quantity = 0
})
.ToList();
return View(model);
}
[HttpPost]
public ActionResult Create(CupcakeViewModel[] cakes)
{
//Save choosen cakes
}
My Model
public class MyFormField
{
public List<MyClass> propName { get; set; }
public string Value { get; set; }
}
public class MyClass
{
public string Name { get; set; }
}
Am passing List of MyFormField to view
In View
#foreach (var item in Model.propName)
{
#Html.TextBoxFor(m => m.Name)
}
i need to add a checkBox before the textBox like below
#foreach (var item in Model.propName)
{
#Html.checkbox("")
#Html.TextBoxFor(m => m.Name)
}
and each checkbox should have a unique name attribute as a corresponding textbox. How can i Change my model to acheive this ?
For business need i need a checkbox for each corresponding Textbox or what better way i can achieve in MVC ?
Add to your model:
public class MyClass
{
public string Name { get; set; }
public boolean Checked {get;set;}
}
Then add:
#Html.CheckBoxFor(model => item.Checked)
#Html.TextBoxFor(model => item.Name)
That way, each checkbox is assoicated with the MyClass in the list
public class MyClass
{
public string Name { get; set; }
public boolean Checked {get;set;}
}
Doing this you would have unique id for each row.
#for (int i = 0; i < Model.propName.Count; i++)
{
#Html.CheckBoxFor(m => m.propName[i].Checked, new { class = "propName_" + i })
#Html.TextBoxFor(m => m.propName[i].Name, new { class = "propName_" + i })
}
I have nested foreach loops in my view, and am having problems with the radio buttons.
#foreach (var header in Model.surveyHeaders)
{
<h1>#header.Name</h1>
foreach (var subHeader in Model.surveySubHeaders.Where(x => x.SurveryHeaderID == header.ID))
{
<h2>#subHeader.Name</h2>
foreach (var question in Model.surveyQuestions.Where(x => x.SurveySubHeaderID == subHeader.ID))
{
#Html.RadioButtonFor(x => x.surveyResults.Where(y => y.SurveyQuestionID == question.ID).First().Value, 1);
#Html.RadioButtonFor(x => x.surveyResults.Where(y => y.SurveyQuestionID == question.ID).First().Value, 2);
#Html.RadioButtonFor(x => x.surveyResults.Where(y => y.SurveyQuestionID == question.ID).First().Value, 3);
#question.Question
}
}
}
The radio button name is always 'Value', so the radio buttons are not grouped. What can I do to achieve the grouping that is desired?
Ah collections in MVC, the joy! In order to make sure all fields are named accordingly in order to be correctly model bound on post, you need to use for loops, that will set the indexes correctly.
Before doing so, you're going to have to tweak your model structure to save your headaches. You need to re-arrange your logic so that you have a hierarchical object model in which you can iterate more cleaner (this way we're getting away from logic in the view too!)
Your survey header class, can't you put a list of subheaders on it? Then your subheader class, can you put the list of questions for that subheader? That way you can do:
#for (var i = 0; i < Model.SurveyHeaders.Count; i++)
{
<h1>#Model.SurveyHeaders[i].Name</h1>
for (var j = 0; j < Model.SurveyHeaders[i].SubHeaders.Count; j++)
{
<h2>#Model.SurveyHeaders[i].SubHeaders[j].Name</h2>
for (var k = 0; k < Model.SurveyHeaders[i].SubHeaders[j].Questions.Count; k++)
{
#Html.RadioButtonFor(x => x.SurveyHeaders[i].SubHeaders[j].Questions[k].Value , 1);
#Html.RadioButtonFor(x => x.SurveyHeaders[i].SubHeaders[j].Questions[k].Value , 2);
#Html.RadioButtonFor(x => x.SurveyHeaders[i].SubHeaders[j].Questions[k].Value , 3);
#Model.SurveyHeaders[i].SubHeaders[j].Questions[k].Question
}
}
}
This assumes your new model structure is something like (with the following classes):
public class MyModel
{
public List<SurveyHeader> SurveyHeaders { get; set; }
}
public class SurveyHeader
{
public string Name { get; set; }
public List<SubHeader> SubHeaders { get; set; }
}
public class SubHeader
{
public string Name { get; set; }
public List<Question> Questions { get; set; }
}
public class Question
{
public int Value { get; set; }
public string Question { get; set; }
}
Also one other tip, for future reference, you use the following LINQ in your code:
x => x.surveyResults.Where(y => y.SurveyQuestionID == question.ID).First().Value
You can simplify it because First can actually take a lamba, like so (although you should use FirstOrDefault and null check it in future just for safety):
x => x.surveyResults.First(y => y.SurveyQuestionID == question.ID).Value
You could use the overload of the RadioButtonFor extension that takes in HtmlAttributes and set the name of each of the RadioButtons.