I'm trying to dip my toe into frontend development with ASP.NET Core 5 MVC - but I'm not really having much success so far... I'm more of a backend developer - give me a SQL Server database, and a ASP.NET Core Web API, and I'm happy - but this frontend wizardry is not quite my strong suit ...
OK, so I'm trying something extremely simple - a Razor page to edit a list of something, add a numerical value to each of those somethings, and then storing them.
I have a model/DTO class like:
public class Customer
{
public int CustomerId { get; set; }
public string Name { get; set; }
public string City { get; set; }
}
Of course, in reality, it has many more properties - but those aren't relevant here. What is: name and city are given, and I need to add the CustomerId
to it (in our case, the CustomerId
is handled by SAP - so we need to manually add those after the customer has been created there).
I have a really simple Razor page which basically shows the list of customers, allows me to enter the CustomerId
in the last column, and then I have a POST
method on the same Razor page that gets hit when the user clicks on the Update
(submit) button.
Here's my view:
@page
@model IndexModel
<h3 class="display-4">Customers</h3>
@if (!string.IsNullOrWhiteSpace(@Model.Message))
{
<div class="container">
<span class="@Model.MessageClass mt-4 mb-5">@Model.Message</span>
</div>
}
<div class="container">
<form method="post">
<table class="table table-striped table-bordered table-hover table-sm">
<thead class="thead-dark">
<tr>
<th scope="col-md-2">Name</th>
<th scope="col-md-2">City</th>
<th scope="col-md-1">CustomerId</th>
</tr>
</thead>
<tbody>
@for (int index = 0; index < Model.Customers.Count; index++)
{
<tr>
<td>@Model.Customers[index].Name</td>
<td>@Model.Customers[index].City</td>
<td>
<input type="number" asp-for="Customers[index].CustomerId" id="Customers[@index]" />
</td>
</tr>
}
</tbody>
</table>
<input type="submit" value="Update" class="btn btn-primary" />
</form>
</div>
and this is my "code-behind" for this Razor page:
public class IndexModel : PageModel
{
public string Message { get; set; }
public string MessageClass { get; set; }
public List<Customer> Customers { get; set; }
public async Task OnGetAsync()
{
Customers = CustomerProvider.GetCustomers();
}
public async Task OnPostAsync()
{
Customers = CustomerProvider.GetCustomers();
if (await TryUpdateModelAsync(Customers))
{
Message = "Customers successfully updated";
MessageClass = "alert-success";
}
else
{
Message = "Customer update did not work";
MessageClass = "alert-danger";
}
}
}
Nothing too fancy here - I basically get a list of customers from somewhere (a database, in reality), I display that list, I can enter customer id values into the grid, and I was expecting to get back the updated Customers
in my OnPostAsync
method.
Now the code runs fine, shows the list of customers, I can enter values into the CustomerId
column, and I can click on "Update". The OnPostAsync
method gets called, I fetch the customers again, and I was expecting the await TryUpdateModelAsync
to update my customers with the CustomerId
values that I have entered.
But it does not do that - after my GetCustomers
call, I have my four test customers - as expected - but after the TryUpdateModelAsync
, that list is empty... The call to TryUpdateModelAsync
works - it returns true
- but the list of customers isn't updated with the information entered on screen - quite the contrary, the list is wiped out ...
I also tried to use
public async Task OnPostAsync([FromForm] List<Customer> updatedCustomers)
hoping that the MVC data binding would return back the list of updated customers - but this updatedCustomers
is null
and doesn't send back the entered data...
But when I look at HttpContext.Request.Form
- I do see the values that were entered:
but somehow, those aren't handled properly and not applied to the Customers
list...
Any ideas? I must be missing something really silly somewhere - but I just cannot find it...
You can bind the values in two ways:
[BindProperty]
Add a property to your model and annotate it with [BindProperty]
. When you build inputs with asp-for="@Model.MyList[i].AProp"
it will be bound to form values when submitted.
Note: You still need to render read-only properties in HTML with hidden inputs (<input type="hidden" />
) for those values to be available when the form is submitted, otherwise you'll get sentinel/null values.
Assuming you have a Razor template as follows:
<form method="POST">
@Html.AntiForgeryToken()
<table>
<thead>
<tr>
<th>Customer Id</th>
<th>Name</th>
</tr>
</thead>
<tbody>
@for (var i = 0; i < Model.Customers.Count; i++)
{
<tr>
<td>
<input type="text"
readonly
value="@Model.Customers[i].CustomerId"
asp-for="@Model.Customers[i].CustomerId"/> <!-- asp-for attributes are indexed for each item -->
</td>
<td>
<input type="text"
value="@Model.Customers[i].Name"
asp-for="@Model.Customers[i].Name"/>
</td>
</tr>
}
</tbody>
</table>
<button type="submit">Save</button>
</form>
you need a model with a bound property:
public record Customer(string CustomerId, string Name);
public class HomeModel : PageModel
{
[BindProperty]
public List<Customer> Customers { get; set; }
// ...
}
When rendered, you should have inputs with indexed name
attributes:
<input type="text" value="Bob" id="Customers_1__Name" name="Customers[1].Name"/>
When you submit the form, Model.Customers
will be populated with the values.
[FromForm]
attributeYou can also accept a parameter of the same name as input names. This means if the input names are like Customers[1].Name
, the parameter name must be customers
(case insensitive) (not updatedCustomers
like you have). Or you can specify a different name using [FromForm(Name = "customers")] updatedCustomers
.
// this works
public ActionResult OnPost([FromForm]List<Customer> customers)
{
return Page();
}
// this also works
public ActionResult OnPost([FromForm(Name = "customers")]List<Customer> updatedCustomers)
{
return Page();
}
If the model has a bound property ([BindProperty]
), it will also be populated with the form values in addition to the parameter.