Model collections lost on form submission
Using the post query below the model returns null for both collections, but it correctly returns the boolean attribute. My guess was that the collections loaded into the model at the time of the pull request would persist in the post request. What am I missing?
EDIT: Basically I am trying to update the invoice list based on the users' select list and checkbox.
Controller:
[HttpGet]
[AllowAnonymous]
public async Task<ActionResult> Index(bool displayFalse = true)
{
InvoiceViewModel invoiceView = new InvoiceViewModel();
var companies = new SelectList(await DbContext.Company.ToListAsync(), "CompanyID", "Name").ToList();
var invoices = await DbContext.Invoice.Where(s => s.Paid.Equals(displayFalse)).ToListAsync();
return View(new InvoiceViewModel { Companies = companies,Invoices = invoices, SelectedCompanyID = 0, DisplayPaid = displayFalse});
}
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Index(InvoiceViewModel model)
{
model.Invoices = await DbContext.Invoice.Where(s => s.CompanyID.Equals(model.SelectedCompanyID) && s.Paid.Equals(model.DisplayPaid)).ToListAsync();
return View(model);
}
Model:
public class InvoiceViewModel
{
public int SelectedCompanyID { get; set; }
public bool DisplayPaid { get; set; }
public ICollection<SelectListItem> Companies { get; set; }
public ICollection<Invoice> Invoices{ get; set; }
}
View:
@model InvoiceIT.Models.InvoiceViewModel
<form asp-controller="Billing" asp-action="Index" method="post" class="form-horizontal" role="form">
<label for="companyFilter">Filter Company</label>
<select asp-for="SelectedCompanyID" asp-items="Model.Companies" name="companyFilter" class="form-control"></select>
<div class="checkbox">
<label>
<input type="checkbox" asp-for="DisplayPaid" />Display Paid
<input type="submit" value="Filter" class="btn btn-default" />
</label>
</div>
<br />
</form>
<table class="table">
<tr>
<th>
@Html.DisplayNameFor(model => model.Invoices.FirstOrDefault().InvoiceID)
</th>
<th>
@Html.DisplayNameFor(model => model.Invoices.FirstOrDefault().CompanyID)
</th>
<th>
@Html.DisplayNameFor(model => model.Invoices.FirstOrDefault().Description)
</th>
<th>
@Html.DisplayNameFor(model => model.Invoices.FirstOrDefault().InvoiceDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Invoices.FirstOrDefault().DueDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Invoices.FirstOrDefault().Paid)
</th>
<th></th>
</tr>
@foreach (var item in Model.Invoices)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.InvoiceID)
</td>
<td>
@Html.DisplayFor(modelItem => item.CompanyID)
</td>
<td>
@Html.DisplayFor(modelItem => item.Description)
</td>
<td>
@Html.DisplayFor(modelItem => item.InvoiceDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.DueDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Paid)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id = item.InvoiceID }) |
@Html.ActionLink("Details", "Index", "InvoiceItem", new { id = item.InvoiceID }) |
@Html.ActionLink("Delete", "Delete", new { id = item.InvoiceID })
</td>
</tr>
}
</table>
source to share
The form only returns the name / value pairs of its controls (input, textbox, selection). Since only 2 controls you create are for properties SelectedCompanyID
and DisplayPaid
your model, then only those properties will be bound when published.
From your comments, what you really want to do is update the invoice table based on the selected company and checkbox values.
From a performance perspective, the approach is to use ajax to update only the invoice table based on the value of your controls.
Create a new controller method that returns a partial view of the table rows
public PartialViewResult Invoices(int CompanyID, bool DisplayPaid)
{
// Get the filtered collection
IEnumerable<Invoice> model = DbContext.Invoice.Where(....
return PartialView("_Invoices", model);
}
Note that you can make the CompanyID parameter null and adjust the query if you want to display unfiltered results first
And a partial view _Invoices.cshtml
@model IEnumerable<yourAssembly.Invoice>
@foreach(var item in Model)
{
<tr>
<td>@Html.DisplayFor(m => item.InvoiceID)</td>
.... other table cells
</tr>
}
In the main view
@model yourAssembly.InvoiceViewModel
@Html.BeginForm()) // form may not be necessary if you don't have validation attributes
{
@Html.DropDownListFor(m => m.SelectedCompanyID, Model.Companies)
@Html.CheckboxFor(m => m.DisplayPaid)
<button id="filter" type="button">Filter results</button>
}
<table>
<thead>
....
</thead>
<tbody id="invoices">
// If you want to initially display some rows
@Html.Action("Invoices", new { CompanyID = someValue, DisplayPaid = someValue })
</tbody>
</table>
<script>
var url = '@Url.Action("Invoices")';
var table = $('#invoices');
$('#filter').click(function() {
var companyID = $('#SelectedCompanyID').val();
var isDisplayPaid = $('#DisplayPaid').is(':checked');
$.get(url, { CompanyID: companyID, DisplayPaid: isDisplayPaid }, function (html) {
table.append(html);
});
});
</script>
An alternative would be to post the form as yours, but instead of returning the view, use
return RedirectToAction("Invoice", new { companyID = model.SelectedCompanyID, DisplayPaid = model.DisplayPaid });
and change the GET method to accept an additional parameter.
Side note: use TagHelpers
to generate
select asp-for="SelectedCompanyID" asp-items="Model.Companies" name="companyFilter" class="form-control"></select>
I'm not familiar with them to be sure, but if it name="companyFilter"
works (and overrides the default name that it would name="SelectedCompanyID"
) then you generate an attribute name
that doesn't match your model property and SelectedCompanyID
will result in 0
(the default for int
) in the POST method.
source to share
Adding ToList()
to a statement that fills companies
in converts SelectList
to List<T>
that form will not be recognized as SelectList
. Also, by using a dynamic keyword var
, you mask this problem. Try this instead:
SelectList companies = new SelectList(await DbContext.Company.ToListAsync(), "CompanyID", "Name");
In general, try to avoid using var
it unless the type is truly dynamic (unknown prior to execution).
source to share
You are putting your model data from the form, so it won't be submitted!
<form asp-controller="Billing" asp-action="Index" method="post" class="form-horizontal" role="form">
<label for="companyFilter">Filter Company</label>
<select asp-for="SelectedCompanyID" asp-items="Model.Companies" name="companyFilter" class="form-control"></select>
<div class="checkbox">
<label>
<input type="checkbox" asp-for="DisplayPaid" />Display Paid
<input type="submit" value="Filter" class="btn btn-default" />
</label>
</div>
<br />
<table class="table">
<tr>
<th>
@Html.DisplayNameFor(model => model.Invoices.FirstOrDefault().InvoiceID)
</th>
<th>
@Html.DisplayNameFor(model => model.Invoices.FirstOrDefault().CompanyID)
</th>
<th>
@Html.DisplayNameFor(model => model.Invoices.FirstOrDefault().Description)
</th>
<th>
@Html.DisplayNameFor(model => model.Invoices.FirstOrDefault().InvoiceDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Invoices.FirstOrDefault().DueDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Invoices.FirstOrDefault().Paid)
</th>
<th></th>
</tr>
@foreach (var item in Model.Invoices)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.InvoiceID)
</td>
<td>
@Html.DisplayFor(modelItem => item.CompanyID)
</td>
<td>
@Html.DisplayFor(modelItem => item.Description)
</td>
<td>
@Html.DisplayFor(modelItem => item.InvoiceDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.DueDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Paid)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id = item.InvoiceID }) |
@Html.ActionLink("Details", "Index", "InvoiceItem", new { id = item.InvoiceID }) |
@Html.ActionLink("Delete", "Delete", new { id = item.InvoiceID })
</td>
</tr>
}
</table>
</form>
source to share
Using a for loop to create with companies will display and store company values
for(c = 0 ; c < Model.Companies.Count(); c++)
{
<input type='hidden' name='@Html.NameFor(Model.Companies[c].Propery1)' id='@Html.IdFor(Model.Comapnies[c].Propery1)' value='somevalue'>someText />
<input type='hidden' name='@Html.NameFor(Model.Companies[c].Propery2)' id='@Html.IdFor(Model.Comapnies[c].Propery2)' value='somevalue'>someText />
}
this ensures that the list is rendered back as the linker by default expects the list to be in the format ListProperty [index]
source to share