mvc razor view nested foreach model post null subobject - asp.net-mvc

In my code I cannot read a nested object value on form post.
Wrog way to edit List property in one Object:
#{
var contatore = 0;
foreach (var item in Model.Movimenti)
{
var movimento = item;
<tr>
<td align="left">
#*Imposto gli Hidden per tutte le proprietà che devo recuperare al post*#
#Html.HiddenFor(x => movimento.Prodotto.Descrizione, "Movimenti[" + contatore + "].Prodotto.Descrizione")
#Html.DisplayFor(x => movimento.Prodotto.Descrizione, "Movimenti[" + contatore + "].Prodotto.Descrizione")
</td>
<td>#Html.EditorFor(x => movimento.Aum, "CurrencyDisabled", "Movimenti[" + contatore + "].AUM")</td>
</tr>
contatore++;
}
}
This is the correct way to edit List property in one Object:
The code:
#using AI.Business.Models
#model Operazione
#{ ViewBag.Title = "Simulatore"; }
#using (Html.BeginForm("CreaOperazione", "Operativita", FormMethod.Post))
{
// Imposto gli Hidden per tutte le proprietà che devo recuperare al post
#Html.HiddenFor(x => x.RapportoModel.TipoRapportoId)
<table width="100%" class="display" id="Simulatore" cellspacing="0">
<thead>
<tr>
<th class="dt-head-left">Linea</th>
<th>AUM</th>
</tr>
</thead>
<tbody>
#Html.EditorFor(x => x.Movimenti)
</tbody>
</table>
<button id="btnSalva" name="btnSalva" type="submit" style="float: right;">Salva Operazione</button>
}
With the editor assuggested:
#model AI.Business.Models.Movimento
<tr>
<td align="left">
#Html.HiddenFor(x => x.Prodotto.Descrizione)
#Html.DisplayFor(x => x.Prodotto.Descrizione)</td>
<td>#Html.EditorFor(x => x.Aum, "CurrencyDisabled")</td>
And this is my object:
public class Movimento
{
public int Id { get; set; }
public ProdottoModel Prodotto { get; set; }
public decimal Aum { get; set; }
}
And the Object Prodotto:
public class ProdottoModel
{
[Key]
public int ID { get; set; }
public string Descrizione { get; set; }
}
In my Actionresult the property Descrizione is null:
[HttpPost]
public ActionResult CreaOperazione(Operazione operazione)
{
if (ModelState.IsValid)
{
// Do something
}
else
ImpostaErrore(ModelState);
return View("PaginaSimulatore", operazione);
}
Open the images:
At my first access to the page the property Prodotto.Descrizione is populated
When i raise the form post event this property was sent with a null value

I'm not sure how you're getting any of this to work, but it's a total fluke. HiddenFor, for example, has no parameter that let's you specify the name value for the field. Instead, where you're trying to do that, the parameter is actually for htmlAttributes, which expects either an anonymous object or IDictionary. The only reason you aren't getting errors is because string is technically an object, but it will never do anything in this context.
The same goes for the rest of your helper calls. With EditorFor, in particular, the second param where you're passing "CurrencyDisabled", is for specifying the editor template that should be used, and the third param is for additionalViewData, which just appends items to ViewData within the context of the editor template.
Long and short, none of this works how you think it does. Plainly and simply, if you need to work with a collection, you need to use for rather than foreach. The expression that you pass to the *For family of helpers is not just about identifying a property however you can get to it; it must be a bindable expression, i.e. something Razor can use to create a name for the form field that will line up to something on your model on post. In order for that to happen, the names must be something like Movimenti[N].Prodotto.Descrizione, and the only way to get that is to call the helper like:
#Html.HiddenFor(m => m.Movimenti[i].Prodotto.Descrizione)
Where i would be the iterator from your for loop.

Related

How to post back a list of object to the controller in MVC [duplicate]

This question already has answers here:
MVC Form not able to post List of objects
(3 answers)
Post an HTML Table to ADO.NET DataTable
(2 answers)
Closed 6 years ago.
I have an object model and I am passing it to the view so that user can input their comments for some of the object's properties as a part of a survey.
I am able to receive what the user has entered for an object if I am only rendered one single object to the view. However, when I want to render multiple (a list) of objects to the view then I receive a null list of objects when the user click on the submit form.
Please see my code below:
This is my object model
public class SurveyViewModel
{
public string Name { get; set; }
public double PV { get; set; }
public double QtyUsePerMonth { get; set; }
public double TotalPVPerMonth { get; set; }
}
This is my view where I render the list of object
#model IEnumerable<WebApplication4.Models.SurveyViewModel>
#{
ViewBag.Title = "Survey";
}
<h2>Survey</h2>
#using (Html.BeginForm(null, null, FormMethod.Post, new { id = "form1" }))
{
<table class="table">
<tr>
<th>
#Html.DisplayNameFor(model => model.Name)
</th>
<th>
#Html.DisplayNameFor(model => model.PV)
</th>
<th>
#Html.DisplayNameFor(model => model.QtyUsePerMonth)
</th>
<th>
#Html.DisplayNameFor(model => model.TotalPVPerMonth)
</th>
<th></th>
</tr>
#foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.Name)
#Html.HiddenFor(modelItem => item.Name)
</td>
<td>
#Html.DisplayFor(modelItem => item.PV)
#Html.HiddenFor(modelItem => item.PV)
</td>
<td>
#Html.TextBoxFor(modelItem => item.QtyUsePerMonth)
</td>
<td>
#Html.TextBoxFor(modelItem => item.TotalPVPerMonth)
</td>
</tr>
}
</table>
<input type="submit" value="Submit">
}
And this is my HttpPost method
[HttpPost]
public ActionResult Survey(List<Models.SurveyViewModel> model)
{
...
}
When the user click on the Submit button I got a null for model where I am expecting to see a list.
Please let me know what I am doing wrong.
Thank you for your help.
Normally, you can't post the list of objects to the controller in default model binder.
Best solution you can pass the object only based on Index to perform the CRUD operation in POST, and GET request you could get all list of objects. So in POST method it works if pass the object only.
If you want to post the list of object, you can achieve it by overriding the ModelBinder or using the FormCollection.
Here is an example, but to perform this way of operation we need to iterate and convert into the list. Because formcollection contains more number of items and not in the List type. The key of the property varies, because of html helper. Be aware when getting the value from formcollection.
[HttpPost]
public ActionResult Index(FormCollection model)
{
List<SurveyViewModel> obj = new List<SurveyViewModel>();
var name =Request.Form["item.Name"].Split(',').ToArray();
var pv =Request.Form["item.PV"].Split(',').ToArray();
//length must be same
for (var i = 0; i < name.Length; i++)
{
obj.Add(new SurveyViewModel()
{
Name = name[i],
PV = Convert.ToDouble(pv[i])
});
}
return View();
}

Input Tag Helper issue in ASP.NET

In ASP.NET view, shown below, I'm getting the error that is specific to line <input asp-for="StateName" />
Error:
'List<GrantsViewModel>' does not contain a definition for 'StateName'
NOTE: View is supposed to display different State Names in an HTML table column.
Controller:
public class DbTestController : Controller
{
private MyProjContext _context;
public DbTestController(MyProjContext context)
{
_context = context;
}
public IActionResult GrantNumbers()
{
var qryVM = from s in _context.StateNames
join g in _context.AnnualGrants on s.StateNumber equals g.StateNumber into sg
from r in sg.DefaultIfEmpty()
select new GrantsViewModel() { StateNumber = s.StateNumber,StateName= s.State, GrantNo= (r == null ? String.Empty : r.GrantNo), FiscalYear = (r == null ? 1900 : r.FiscalYear) };
return View(qryVM.ToList());
}
}
ViewModel:
public class GrantsViewModel
{
public int GrantNo_Id { get; set; }
public string StateNumber { get; set; }
public string StateName { get; set; }
public string GrantNo { get; set; }
public int FiscalYear { get; set; }
}
View:
#model List<MyProjet.Models.GrantsViewModel>
<form asp-controller="DbTest" asp-action="GrantNumbers" asp-route-returnurl="#ViewData["ReturnUrl"]" method="post">
<table class="table">
<thead>
<tr>
<th>
#Html.DisplayNameFor(model => model.First().StateName)
</th>
<th>
#Html.DisplayNameFor(model => model.First().GrantNo)
</th>
<th></th>
</tr>
</thead>
<tbody>
#foreach (var item in Model)
{
<tr>
<td>
<input asp-for="StateName" />
</td>
<td>
#Html.DisplayFor(modelItem => item.GrantNo)
</td>
<td></td>
</tr>
}
</tbody>
</table>
<button type="submit" class="btn btn-default">Save</button>
</form>
#Html.DisplayNameFor(model => model.StateName)
In this line you are trying to access the property StateName from the object referenced by model. Except model references an object of type List<T>, which does not have a property StateName.
To access StateName, you need to provide which element of the list you are accessing, such as the following (assuming you don't need to iterate, since you are just getting the column titles).
#Html.DisplayNameFor(model => model[0].StateName)
To reference the element correctly in the asp-for helper, use
<input asp-for="#item.StateName" />
You want to use FirstOrDefault() when referencing names of properties. This will still be able to get the display names using reflection even if your IEnumerable/List Model is empty.
#Html.DisplayNameFor(model => model.FirstOrDefault().StateName)
Instead of the StateName asp-tag, here is the Razor Helper for an input:
#Html.EditorFor(modelItem => item.StateName)
I'm a bit confused as to why you have this table in a form if you're wanting to just display your list of information. If you clarify, I can better answer based on your intent.
If you just want this data displayed, then use a DisplayFor() like you did for GrantNo column:
#Html.DisplayFor(modelItem => item.StateName)

Passing a List to View from Controller in .NET

I am just trying to pass a List and display it dynamically in a table in the View. I have a Homepage Model and Homepage controller and the variables are being set right, but I can't figure out how to pass it to the view.
My model looks like this:
public class HomePageModel
{
[Display(Name = "First Name")]
public string FirstName { get; set; }
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Display(Name = "ExtNum")]
public string ExtNum { get; set; }
[Display(Name = "PhoneDisplay")]
public List<PhoneDisplay> PhoneDisplay { get; set; }
}
and this is the controller:
public ActionResult Homepage(HomePageModel HpModel)
{
ViewBag.Welcome = "Welcome: ";
ViewBag.FirstName = HpModel.FirstName;
ViewBag.LastName = HpModel.LastName;
ViewBag.Extlbl = "Extension: ";
ViewBag.Ext = HpModel.ExtNum;
ViewBag.Todaylbl = "Today:";
ViewBag.Today = DateTime.Now;
DBOps ops = new DBOps();
HpModel.PhoneDisplay = ops.getDisplayInfo(HpModel.ExtNum);
return View(HpModel);
}
PhoneDisplay is a list that contains a line index, a description string and a 4 digit number. Each user will have at least 1 item in this list and maximum 6. I was able to pass the other parameters and display them in the view but I can't find a way to pass the list and display that dynamically.
EDIT
I made it this far but still can't find the list items.
#model AxlMVC.Models.HomePageModel
<table>
<caption style="font-weight:bold">Your Phone Information</caption>
<tr>
<th>Line Index</th>
<th>Display</th>
<th>Extension Number</th>
</tr>
#{
foreach (var item in Model.PhoneDisplay) //problems here
{
<tr>
<td>
#Html.Display(item.numplanindex)
</td>
<td>
#Html.Display(item.display)
</td>
<td>
#Html.Display(item.dnorpattern)
</td>
</tr>
}
}
</table>
EDIT
I debugged the cshtml file and the items in the foreach loop are being passed just fine too, but the table is not showing on the page and neither are the items all I can see is the caption and the headers for each column
Html.Display displays "data from the ViewData dictionary or from a model" as stated on MSDN. What it means is that it searches for the key in the ViewData dictionary with the value you pass in or a property in the Model with the specified name. E.g. Display("test") would search ViewData for the "test" key and the Model for the property named test. Since you are passing in property values that cannot work. Your options are:
Output the value directly, #item.numplanindex. This will output a string representation of the value.
Use Display, although this is not recommended. You could do Display("PhoneDisplay[1].numplanindex") to display numplanindex property of the second item in list.
Use DisplayFor, like DisplayFor(model => item.numplanindex). This is a strongly typed version of Display. It will either displays a string representation of the value or a template for the type, if you have one. You can also manage how the output is displayed via Data Annotations, e.g. DisplayFormatAttribute.
Use DisplayTextFor, like DisplayTextFor(model => item.numplanindex). This method outputs the string representation of the value.
Since you already have data annotations on the model, you could modify your view like this:
#model AxlMVC.Models.HomePageModel
<table>
<caption class="tableCaption">Your Phone Information</caption>
<tr>
<th>#Html.DisplayNameFor(model => model.PhoneDisplay[0].numplanindex)</th>
<th>#Html.DisplayNameFor(model => model.PhoneDisplay[0].display)</th>
<th>#Html.DisplayNameFor(model => model.PhoneDisplay[0].dnorpattern)</th>
</tr>
#{
foreach (var item in Model.PhoneDisplay)
{
<tr>
<td>#Html.DisplayTextFor(model => item.numplanindex)</td>
<td>#Html.DisplayTextFor(model => item.display)</td>
<td>#Html.DisplayTextFor(model => item.dnorpattern)</td>
</tr>
}
}
</table>
The line #Html.DisplayNameFor(model => model.PhoneDisplay[0].numplanindex) also works if PhoneDisplay contains no items. Only property metadata is collected, expression as such is not executed.

View Models and Check Boxes in View

I have a newbie question, which I have tried to understand for the past few days. Hopefully someone can be kind enough to help me understand the programming flow.
Assuming I have a model, the information is stored in the database:
public class Student
{
public int studentID { get; set; }
public string studentName { get; set; }
public strin studentGrade {get; set; }
}
public class StudentDBContext : DbContext
{
public DbSet<Student> Students { get; set; }
}
and I want to display it into the view, with additional checkbox so I can select which students to be promoted into the next grade. I read that one way to do it is by putting into view model:
public class StudentViewModel
{
public bool promoted { get; set; }
public Student stu { get; set; }
}
But I am stuck on is this the way to do it? and if yes, how do you put into the view where it will display all the students with a checkbox next to it. Afterwards, I want to update all the grade for the students whose checkboxes are ticked. For example:
Student A, Student B, Student D promoted from Grade 1 to Grade 2. So I want to display the students, tick Student A, B and D and submit to update the Grade.
Step by step example will be much appreciated.
Update 1:
Controller:
[HttpGet]
public ViewResult CheckBox()
{
var studentViewModels = db.Students.Select(m => new StudentViewModel()
{
stu = m
}).ToList();
return View(studentViewModels);
}
[HttpPost]
public ActionResult CheckBox(IList<studentViewModel> list)
{
foreach (var stuUpdate in list.Where(m => m.promoted))
{
var stuRow = db.Students.Find(stuUpdate.stu.studentID);
stuRow.studentName = stuRow.studentName + "1";
db.Entry(stuRow).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("CheckBox");
}
return RedirectToAction("CheckBox");
}
View:
#model IList<School.ViewModels.StudentViewModel>
#using (Html.BeginForm())
{
<table>
<tr>
<th>
</th>
<th>
student ID
</th>
<th>
student name
</th>
<th>
student grade
</th>
</tr>
#foreach (var item in Model) {
<tr>
<td>
#Html.CheckBoxFor(modelItem => item.promoted)
#Html.HiddenFor(modelItem => item.stu.studentID)
</td>
<td>
#Html.DisplayFor(modelItem => item.stu.studentID)
</td>
<td>
#Html.DisplayFor(modelItem => item.stu.studentName)
</td>
<td>
#Html.DisplayFor(modelItem => item.stu.studentGrade)
</td>
</tr>
}
</table>
<input type="submit" value="save" />
}
However currently hit by the following error:
Value cannot be null.
Parameter name: source
Source Error:
foreach (var stuUpdate in list.Where(m => m.promoted))
A very basic "step by step" (done in SO, so I probably did a few mistakes, but you've got the idea).
You have always a few ways to do these kind of things, so... just really take it as a sample, and find other examples to get other ideas.
well, first, in your controller, you will have a GET action (to see the list).
[HttpGet]
public ViewResult StudentList() {
//retrieve all students. With the Select, we create a new instance of StudentViewModel for each student.
//assuming StudentDbContext being a property of your controller, if it's not, you can instantiate a new one (in a using clause)
var studentViewModels = StudentDbContext.Students
.Select(m => new StudentViewModel() {
stu = m
//we don't say nothing about promoted :
//it will be there as "false" by default, which is probably what we want.
}).ToList();
//now we've got a list of StudentViewModel. This will be the model of our view
return View(studentViewModels);
}
Then we've got a view, StudentList.cshtml
in this view, we will display a table, with a line for each student : the studentId (hidden in this case), the name (display only), the grade (display only), and a checkbox.
We need a for loop (not a foreach) to get fine model binding.
#model IList<StudentViewModel>
#using (Html.BeginForm()) {
<table>
<tr>
<th>Student name</th>
<th>Student grade</th>
<th>Promote</th>
</tr>
#for (var i = 0; i < Model.Count(); i++) {
<tr>
<td>#Html.HiddenFor(m => Model[i].Student.studentID)
#Html.DisplayFor(m => Model[i].Student.studentName)
</td>
<td>#Html.DisplayFor(m => Model[i]Student.studentGrade)</td>
<td>#Html.CheckBoxFor(m => Model[i].promoted)</td>
</tr>
}
</table>
<input type="submit" value="save" />
}
This form will lead to another POST action (same name as the GET one, depending of what you have in your Html.BeginForm)
[HttpPost]
public ActionResult StudentList(IList<StudentViewModel> list) {
//we treat only the lines where checkbox has been checked
foreach (var studentViewModel in list.Where(m => m.promoted) {
var student = StudentDBContext.GetById(studentViewModel.Student.studentID);//get student entity instance from context
student.studentGrade ="<new value>";
StudentDBContext.SaveChanges();//save changes, you must have such a method somewhere.
}
return Action("StudentList");
}
Little detail :
Try to respect some really basic "usual" practices : for example in c#, Properties should begin by an uppercase letter (so StudentGrade, StudentName, Promoted, etc).

Binding input model in partial view help needed

I want to pass an input model from a partial view to a controller. I'm rather new to MVC so still trying to understand how the default model binder works.
Via AJAX (listBox) a controller passes back a partial view and inserts into table id=searchResults.
#model ViewModels.LocationViewModel
#using (Ajax.BeginForm("ProcessSearch", "SearchR", new AjaxOptions
{
HttpMethod = "GET",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "searchResults",
}))
{
<div>#Html.ListBoxFor(xxx)</div>
<input id="Search" type="submit" value="Search" />
}
Here is the controller and ViewModel that populates the partial view
public class OrderViewModel
{
public string Description { get; set; }
public string Status { get; set; }
}
public ActionResult ProcessSearch(SearchViewModel search)
{
select new OrderViewModel{
Status=f.STATUS,
Description=f.DESCRIPTION}).ToList();
return PartialView(model);
}
In the same main view I have this form that I want I want to bind to yet another view model. I simply don't understand how to implement the default binder from the model of the partial view. I apologize if I didn't explain this correctly. I hope it makes sense.
#using (Html.BeginForm("Testing", "SearchR", FormMethod.Post))
{
<div>#Html.DropDownListFor(yyy)</div>
<input id="Reshop" type="submit" value="Reshop" />
}
<table id="searchResults"></table>
public ActionResult Testing(RSOrderViewModel rOrder)
{
return Content("hey");
}
public class RSOrderViewModel
{
public string yyy { get; set; }
public IEnumerable<OrderViewModel> sovm { get; set; }
}
#model List<ViewModels.OrderViewModel>
#{ViewData.TemplateInfo.HtmlFieldPrefix = "sovm";
}
<table id="searchResults">
<tr>
<th>Order Id</th>
<th>Order Detail</tr>
#for (int i = 0; i < Model.Count; i++)
{
<tr>
<td>
#Html.DisplayFor(x => x[i].OrderId)
</td>
<td>
#Html.DisplayFor(x => x[i].OrderDetail)
</td>
</tr>
}
</table>
The table is outside the second form. So when you POST to the Testing action all that is sent to the controller is the value of the dropdown list. If you want to send the collection that's stored in this table you will have to either use AJAX or put the table inside the form:
#using (Html.BeginForm("Testing", "SearchR", FormMethod.Post))
{
<div>#Html.DropDownListFor(yyy)</div>
<table id="searchResults"></table>
<input id="Reshop" type="submit" value="Reshop" />
}
Now of course putting the table inside the form doesn't mean that it will send anything to the server when you submit the form. You need to put input fields (hidden if you don't want them to be visible to the user) that will contain the values that will be POSTed back. Also those input field names must follow the standard convention for binding to a list.
You haven't actually shown how does the partial view look like but here's an example of how it might look so that the convention is respected. For example let's suppose that you have 2 properties in your OrderViewModel that you want to be bound: OrderId and OrderDetail:
#model IEnumerable<OrderViewModel>
#{
// We set the name prefix for input fields to "sovm"
// because inside your RSOrderViewModel the collection
// property you want to bind to this table is called sovm
// and in order to respect the convention the input names
// must be in the following form "sovm[0].OrderId", "sovm[1].OrderId", ...
ViewData.TemplateInfo.HtmlFieldPrefix = "sovm";
}
<thead>
<tr>
<th>Order Id</th>
<th>Order detail</th>
</tr>
</thead>
<tbody>
#Html.EditorForModel()
</tbody>
and then you could have an editor template which will be rendered for each element of the model (~/Views/Shared/EditorTemplates/OrderViewModel.cshtml):
#model OrderViewModel
<tr>
<td>#Html.EditorFor(x => x.OrderId)</td>
<td>#Html.EditorFor(x => x.OrderDetail)</td>
</tr>
The name of the template is the name of the type used in the collection you want to bind to (IEnumerable<OrderViewModel> sovm { get; set; } => OrderViewModel.cshtml). It must also be placed inside the ~/Views/Shared/EditorTemplates folder if it can be reused between multiple controllers or if it is specific to the current controller inside the ~/Views/XXX/EditorTemplates folder where XXX is the current controller.

Resources