从动态加载的视图中将项目添加到列表中,并将其传递给 asp.net 核心中的控制器

Add item into List from View Loaded Dynamically and pass it to Controller in asp.net core

我正在处理移动商店管理系统的订单页面。我想允许用户通过 select 列表 select 一家公司,然后从另一个 select 列表中 select 该公司的多个模型,该列表通过 [=62] 动态加载=].

级联模型的代码正在运行,但我无法将 selected 模型发送到服务器,因为它正在通过 JavaScript 在 DOM 中添加它们。

下面是级联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>

级联代码:

$("#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>')
    });
  });
}

单击 保存 按钮后,我将使用局部视图显示当前 selected 型号的产品:

$('#saveBtn').click(function () {   
  $.ajax({
    url: '/Order/GetProduct?Phoneid=' + $("#CompanyId").val() + "&modelId=" + $('#modelId').val(),
    type: 'Post',
    success: function (data) {
      $('#products').append(data);
    },
  })
})

问题 1

当用户 select 找到第一个公司和他们的两个模型,然后单击 保存 按钮时,部分视图加载索引 i=0,i=1 .然后,用户 select 是另一家公司,select 是他们的模型。同样,局部视图使用相同的索引呈现。如何使索引唯一?当用户单击 保存 按钮时,将呈现此部分视图,该按钮仅呈现当前公司的 selected 模型。

@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>

问题 2

如何将通过局部视图呈现的所有项目发送到服务器?我只想将这些 selected 产品连同每个型号的数量和价格发送到服务器。这意味着在 OrderViewModel.

的产品列表中绑定这些项目

您可以在下图中找到我的 OrderViewModelProducts 模型:

你能告诉我如何将 Razor 项目绑定到列表中以 post 到控制器吗?如果您能给我一些建议,我将不胜感激。

相关

对于第一个问题,你可以尝试不同的东西。当您执行 ajax 调用时,您会得到一个模型列表。对于这些模型中的每一个,将选定的公司 ID 添加为 属性。所以你不必担心索引是独一无二的。

关于第二个问题,应该是比较容易做的事情。但是,需要更多信息。 1. 当按下保存按钮时,你是在做完整的 post 备份吗?或者它也是一个 AJAX 电话? 2. 为什么您不想选择 AJAX 调用来进行更新?因此,您可以根据响应将用户重定向到结果页面等。

如果你可以在新项目中创建一个小样本,并上传到github,post这里的信息。我应该可以看一下,更好地理解。我一定能帮上忙。

也尝试阅读此线程,它可能会有所帮助

how to persist partial view model data during postback in asp.net mvc

TL;DR: 您可以设置自己的 name 属性,而不是依赖 asp-for 标签助手。这使您可以灵活地以您想要的任何数字开始索引。理想情况下,您会将现有产品的数量传递给 GetProduct() 并开始对其进行索引。此外,您还需要在 name 属性前加上 Products 前缀,从而确保这些表单元素正确绑定到 OrderViewModel.Products collection on post back。

<input name="Products[@(startIndex+i)].Quantity" value="@Model[i].Quantity" />

然后您可以使用 LINQ 过滤 server-side 上的 OrderViewModel.Products collection 以将结果限制为所选产品:

var selectedProducts = c.Products.Where(p => p.IsSelected);

有关此方法如何工作的更多 in-depth 解释,以及实施中的一些变化,请阅读下面我的完整答案。


详细解答

这里发生了很多事情,所以这将是一个冗长的答案。我将首先提供一些关于 ASP.NET Core MVC 如何连接视图模型、视图和绑定模型之间的关键背景,因为这将更好地理解如何根据您的需要调整此行为。然后我将提供解决您每个问题的策略。

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.

背景

重要的是要注意,虽然 ASP.NET 核心 MVC 试图通过约定(例如 asp-for 标签助手)标准化和简化从视图模型到视图再到绑定模型的工作流程,但这些都是相互独立。

因此,当您使用 collection 调用 asp-for 时,例如

<input asp-for="@Model[i].Quantity" />

然后输出如下内容HTML:

<input id="0__Quantity" name="[0].Quantity" value="1" />

然后,当您提交时,服务器会查看您的表单数据,并使用 a set of conventions 将该数据映射回您的绑定模型。所以这可能映射到:

public async Task<IActionResult> ProductsAsync(List<Product> products) { … }

当您在 collection 上调用 asp-for 时,它将始终从 0 开始索引。当它将表单数据绑定到绑定模型时,它将始终从 [0] 开始并向上计数。

但是如果您需要更改此行为,您没有理由需要使用asp-for。如果您需要灵活地生成属性,您可以自己设置 id and/or name 属性。

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.

问题 1:设置索引

鉴于上述背景,如果您想为第二个模型自定义从调用 GetProducts() 返回的起始索引,您需要执行以下操作:

  1. 在调用 GetProduct() 之前,确定您已经拥有的产品数量,例如计算分配了 card class 的元素数量(即 $(".card").length)。

    Note: If the card class is not exclusively used for products, you can instead assign a unique class like product to each tr element in your _DisplayOrder view and count that.

  2. 在您对 GetProduct() 的调用中包括此计数,例如 &startingIndex= 参数:

$('#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) { … }
  1. 通过视图模型将此 startingIndex 转发到您的“部分”视图;例如,
public class ProductListViewModel {
  public int StartingIndex { get; set; }
  public List<Product> Products { get; set; }
}
  1. 使用该值来偏移写入输出的索引:
<input id="@(Model.StartingIndex+i)__Quantity" name="[@(Model.StartingIndex+i)].Quantity" value="@Model.Products[i].Quantity" />

这不像 asp-for 那样整洁,因为您需要连接很多类似的信息,但它为您提供了灵活性,以确保您的 name 值在页面上是唯一的, 无论你调用多少次 GetProduct().

备注

  • 如果您不想创建新的视图模型,您可以可以通过您的ViewData字典中继startingIndex。不过,我更喜欢拥有一个包含我需要的所有数据的视图模型。
  • 当使用 asp-for 标签助手时,它会自动生成 id 属性,但如果您从未通过例如引用它JavaScript可以省略。
  • 浏览器只会将具有 name 属性的表单元素的值提交给服务器。因此,如果您有 client-side 需要的输入元素,但绑定模型 不需要 ,出于某种原因,您可以省略 name 属性.
  • 除了 {Index}__{Property} 之外,您还可以遵循其他约定。但是,除非您真的 想要深入了解模型绑定,否则最好坚持使用 existing collection conventions.
  • 之一

小心你的索引!

Model Binding conventions for collections 中,您会注意到一个警告:

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.

因此,在分配这些时,您需要确保它们是连续的,没有任何间隙。如果您使用现有的计数 (.length),例如$(".card")$(".product") 元素在您的页面上播种 startingIndex 值,但是,这应该不是问题。

问题 2:将这些值发送到服务器

如上所述,任何具有 name 属性的表单元素都会将其数据提交到服务器。因此,使用 asp-for、使用 HTML 手动编写表单或使用 JavaScript 动态构建表单并不重要。如果有一个带有 name 属性的表单元素,并且它在提交的 form 中,它将包含在有效负载中。

调试表单数据

您可能已经很熟悉 wth this,但如果不是:如果您使用浏览器的开发人员控制台,则在提交表单时,您将能够看到此信息作为页面元数据的一部分。例如,在 Google Chrome:

  • 转到开发者工具 (Ctrl+Shift+I )
  • 转到网络选项卡
  • 提交您的表格
  • 点击页面名称(通常是第一个条目)
  • 转到 Headers 选项卡
  • 向下滚动到 表单数据 部分(或 查询字符串参数 对于 GET 请求)

您应该会看到如下内容:

  • [0].isSelected
  • [0].Phoneid 4
  • [0].modelId 10
  • [0].数量 5
  • [0].价格 10.50
  • [1].isSelected
  • […]…

如果您在 Chrome 中看到这些数据,但没有在您的 ASP.NET 核心 MVC 控制器操作中看到这些数据,那么这些字段的命名约定与您的绑定之间存在脱节模型——因此,ASP.NET Core MVC 不知道如何映射两者。

绑定问题

这里可能存在两个问题,它们都可能会干扰您的数据绑定。

重复索引

由于您当前正在提交重复索引,可能 会导致数据冲突。例如,如果 [0].Quantity 有两个值,那么它会将它们作为数组检索——并且可能无法将任何一个值绑定到例如您的 Products 绑定模型上的 int Quantity 属性。不过,我还没有对此进行测试,所以我不完全确定 ASP.NET Core MVC 如何处理这种情况。

Collection 姓名

当您使用 asp-for 标签助手绑定到 List<Products> 时,我 相信 它将使用 [i].Property 命名约定。这是个问题,因为您的 OrderViewModel 不是 collection。相反,这些需要与绑定模型上的 Products 属性 相关联。这可以通过在 name 前加上 Products 来完成。如果您使用 asp-for 绑定到视图模型上的 Products 属性,这将自动完成——正如上面 ProductListViewModel 所建议的那样。但是,由于您无论如何都需要基于 IndexOffset 动态生成 name,因此您可以 hard-code 这些作为模板的一部分:

<input id="Products_@(Model.StartingIndex+i)__Quantity" name="Products[@(Model.StartingIndex+i)].Quantity" value="@Model.Products[i].Quantity" />

已选产品

不过还是有问题!这将包括您的 所有 产品 — 即使用户没有以其他方式选择它们。有很多策略可以处理这个问题,从在客户端动态过滤它们,到创建一个首先验证 Products_[i]__isSelected 属性的自定义模型绑定器。不过,最简单的方法是简单地允许它们全部绑定到您的绑定模型,然后在使用例如任何处理之前过滤它们。林克:

var selectedProducts = c.Products.Where(p => p.IsSelected).ToList();
…
repo.SetProducts(selectedProducts);