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>

      

+3


source to share


4 answers


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.

+4


source


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).

0


source


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>

      

0


source


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]

-1


source







All Articles