I am working on Mobile Store Management System's Order page. I want to allow users to select a company through a select list, and then select multiple models of that company from another select list which is loaded dynamically through AJAX.
The code for the cascading models is working, but I am unable to send the selected models to the server because it is adding them in the DOM through JavaScript.
The following is the code for the cascading selection:
<div class="form-group row">
<label class="control-label col-6">Company Name</label>
<div class="col-12">
<select id="CompanyId" class="custom-select mr-sm-2"
asp-items="@(new SelectList(
@ViewBag.Companies,"Phoneid","Com_name"))">
<option value="">Please Select</option>
</select>
</div>
<span class="text-danger"></span>
</div>
<div class="form-group row">
<label class="control-label col-6"></label>
<div class="col-12">
<select id="modelId" multiple class="custom-select mr-sm-2"
asp-items="@(new SelectList(string.Empty,"modelId","model_name","--Select--"))">
<option value="">Please Select</option>
</select>
</div>
<span class="text-danger"></span>
</div>
<div>
<input type="button" id="saveBtn" value="Save" />
</div>
Cascading Code:
$("#CompanyId").change(async function()
{
await $.getJSON("/Order/GetAllModels",{ PhoneId: $("#CompanyId").val()},
function(data)
{
$("#modelId").empty();
$.each(data, function (index, row) {
$("#modelId").append("<option value='" + row.modelId + "'>" +
row.model_name + '</option>')
});
});
}
Once the Save button is clicked, I am displaying the product for the currently selected models using a partial view:
$('#saveBtn').click(function () {
$.ajax({
url: '/Order/GetProduct?Phoneid=' + $("#CompanyId").val() + "&modelId=" + $('#modelId').val(),
type: 'Post',
success: function (data) {
$('#products').append(data);
},
})
})
When the user selects the first company and their two models, and then clicks the Save button, the partial view loads with indexes i=0,i=1
. Then, the user selects another company and selects their models. Again, the partial view renders with same indexes. How can I make the indexes unique? This partial view is rendered when the user clicks the Save button, which renders only the current company's selected models.
@model List<Mobile_Store_MS.ViewModel.Orders.Products>
<table class="table">
<tbody>
@for (int i = 0; i < Model.Count; i++)
{
<tr class="card d-flex">
<td>
<input asp-for="@Model[i].isSelected" />
</td>
<td>
<input hidden asp-for="@Model[i].Phoneid" /> <input hidden asp-for="@Model[i].modelId" />
@Html.DisplayFor(modelItem => Model[i].com_Name) @Html.DisplayFor(modelItem => Model[i].model_name)
</td>
<td>
<input asp-for="@Model[i].Quantity" />
</td>
<td>
<input class="disabled" readonly asp-for="@Model[i].price" />
</td>
</tr>
}
</tbody>
</table>
How can I send all of the items rendered through the partial view to the server? I just want to send these selected products along with the quantity and price for each model to the server. This means binding these items in the product list of the OrderViewModel
.
You can find my OrderViewModel
and Products
model in the following diagram:
Can you tell me how to bind Razor items into a list to post to the controller? I would be very grateful if you give me some suggestions.
TL;DR: Instead of relying on the asp-for
tag helper, you can set your own name
attribute. This gives you the flexibility to start the index at whatever number you want. Ideally, you will pass the number of existing products to GetProduct()
and start indexing off of that. In addition, you also need to prefix your name
attribute with Products
, thus ensuring those form elements are properly bound to your OrderViewModel.Products
collection on post back.
<input name="Products[@(startIndex+i)].Quantity" value="@Model[i].Quantity" />
You can then filter the OrderViewModel.Products
collection on the server-side using LINQ to limit the results to selected products:
var selectedProducts = c.Products.Where(p => p.IsSelected);
For a more in-depth explanation of how this approach works, as well as some of the variations in the implementation, read my full answer below.
There's a lot going on here, so this is going to be a lengthy answer. I'm going to start by providing some critical background on how ASP.NET Core MVC connects the dots between your view model, your view, and your binding model, as that will better understand how to adapt this behavior to your needs. Then I'm going to provide a strategy for solving each of your problems.
Note: I'm not going to write all of the code, as that would result in me reinventing a lot of code you've already written—and would make for an even longer answer. Instead, I'm going to provide a checklist of steps needed to apply the solution to your existing code.
It's important to note that while ASP.NET Core MVC attempts to standardize and simplify the workflow from view model to view to binding model through conventions (such as the asp-for
tag helper) these are each independent of one another.
So when you call asp-for
on a collection using e.g.,
<input asp-for="@Model[i].Quantity" />
It then outputs the something like the following HTML:
<input id="0__Quantity" name="[0].Quantity" value="1" />
And then, when you submit that, the server looks at your form data, and uses a set of conventions to map that data back to your binding model. So this might map to:
public async Task<IActionResult> ProductsAsync(List<Product> products) { … }
When you call asp-for
on a collection, it will always start the index at 0
. And when it bind the form data to a binding model, it will always start at [0]
and count up.
But there's no reason you need to use asp-for
if you need to change this behavior. You can instead set the id
and/or name
attributes yourself if you need flexibility over how they are generated.
Note: When you do this, you'll want to make sure you're still sticking to one of the conventions that ASP.NET Core MVC is already familiar with to ensure data binding occurs. Though, if you really want to, you can instead create your own binding conventions.
Given the above background, if you want to customize the starting index returned from your call to GetProducts()
for your second model, you‘ll want to do something like the following:
Before calling GetProduct()
, determine how many products you already have by e.g. counting the number of elements with the card
class assigned (i.e., $(".card").length
).
Note: If the
card
class is not exclusively used for products, you can instead assign a unique class likeproduct
to eachtr
element in your_DisplayOrder
view and count that.
Include this count in your call to GetProduct()
as e.g., a &startingIndex=
parameter:
$('#saveBtn').click(function () {
$.ajax({
url: '/Order/GetProduct?Phoneid=' + $("#CompanyId").val() + "&modelId=" + $('#modelId').val() + "&startingIndex=" + $("#card").length,
type: 'Post',
success: function (data) {
$('#products').append(data);
},
})
})
[HttpPost]
public IActionResult GetProduct(int Phoneid, string[] modelId, int startingIndex = 0) { … }
startingIndex
to your "partial" view via a view model; e.g.,public class ProductListViewModel {
public int StartingIndex { get; set; }
public List<Product> Products { get; set; }
}
<input id="@(Model.StartingIndex+i)__Quantity" name="[@(Model.StartingIndex+i)].Quantity" value="@Model.Products[i].Quantity" />
That's not as tidy as asp-for
since you're needing to wire up a lot of similar information, but it offers you the flexibility to ensure that your name
values are unique on the page, no matter how many times you call GetProduct()
.
startingIndex
via your ViewData
dictionary instead. I prefer having a view model that includes all of the data I need, though.asp-for
tag helper, it automatically generates the id
attribute, but if you're not ever referencing it via e.g. JavaScript you can omit it.name
attribute. So if you have input elements that are needed on the client-side but aren't needed in the binding model, for some reason, you can omit the name
attribute.{Index}__{Property}
that you can follow. But unless you really want to get into the weeds of model binding, you're best off sticking to one of the existing collection conventions.In the Model Binding conventions for collections, you'll notice a warning:
Data formats that use subscript numbers (... [0] ... [1] ...) must ensure that they are numbered sequentially starting at zero. If there are any gaps in subscript numbering, all items after the gap are ignored. For example, if the subscripts are 0 and 2 instead of 0 and 1, the second item is ignored.
As such, when assigning these, you need to make sure that they're sequential without any gaps. If you're using the count (.length
) of existing e.g. $(".card")
or $(".product")
elements on your page to seed the startingIndex
value, however, then that shouldn't be a problem.
As mentioned above, any form element with a name
attribute will have its data submitted to the server. So it doesn't really matter if you're using asp-for
, writing out your form manually using HTML, or constructing it dynamically using JavaScript. If there's a form element with a name
attribute, and it's within the form
being submitted, it will get included in the payload.
You're likely already familiar with this, but if not: If you use your browser's developer console, you'll be able to see this information as part of the page metadata when you submit your form. For instance, in Google Chrome:
GET
request)You should see something like:
If you're seeing these in Chrome, but not seeing these data reflected in your ASP.NET Core MVC controller action, then there's a disconnect between the naming conventions of these fields and your binding model—and, thus, ASP.NET Core MVC doesn't know how to map the two.
There are two likely issues here, both of which might be interfering with your data binding.
Duplicate Indexes
Since you are currently submitting duplicate indexes, that could be causing collisions with the data. E.g., if there are two values for [0].Quantity
, then it will retrieve those as an array—and may fail to bind either value to e.g. the int Quantity
property on your Products
binding model. I haven't tested this, though, so I'm not entirely sure how ASP.NET Core MVC deals with this situation.
Collection Name
When you bind to a List<Products>
with the asp-for
tag helper, I believe it will use the [i].Property
naming convention. That's a problem because your OrderViewModel
is not a collection. Instead, these needs to be associated with the Products
property on your binding model. That can be done by prefixing the name
with Products
. This will be done automatically if you use asp-for
to bind to a Products
property on your view model—as proposed on the ProductListViewModel
above. But since you need to dynamically generate the name
's based on the IndexOffset
anyway, you can just hard-code these as part of your template:
<input id="Products_@(Model.StartingIndex+i)__Quantity" name="Products[@(Model.StartingIndex+i)].Quantity" value="@Model.Products[i].Quantity" />
There's still a problem, though! This is going to include all of your products—even if they weren't otherwise selected by the user. There are a number of strategies for dealing with this, from dynamically filtering them on the client, to creating a custom model binder that first validates the Products_[i]__isSelected
attribute. The easiest, though, is to simply allow all of them to be bound to your binding model, and then filter them prior to any processing using e.g. LINQ:
var selectedProducts = c.Products.Where(p => p.IsSelected).ToList();
…
repo.SetProducts(selectedProducts);