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

Muh*_*ami 3 c# razorengine asp.net-core-mvc asp.net-core

我正在处理移动商店管理系统的订单页面。我想让用户通过选择列表选择一个公司,然后从另一个通过 AJAX 动态加载的选择列表中选择该公司的多个模型。

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

以下是级联选择的代码:

<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>
Run Code Online (Sandbox Code Playgroud)

级联代码:

$("#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>')
    });
  });
}
Run Code Online (Sandbox Code Playgroud)

一旦Save按钮被点击,我展示了使用局部视图当前选定的型号的产品:

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

问题一

当用户选择第一个公司及其两个模型,然后单击Save按钮时,局部视图将加载索引i=0,i=1。然后,用户选择另一家公司并选择他们的型号。同样,局部视图使用相同的索引呈现。如何使索引唯一?当用户单击Save按钮时会呈现此局部视图,该按钮仅呈现当前公司的选定模型。

@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>
Run Code Online (Sandbox Code Playgroud)

问题二

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

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

在此处输入图片说明

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

有关的

Jer*_*ney 5

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

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

然后,您可以OrderViewModel.Products使用 LINQ 在服务器端过滤集合以将结果限制为选定的产品:

var selectedProducts = c.Products.Where(p => p.IsSelected);
Run Code Online (Sandbox Code Playgroud)

要更深入地解释这种方法的工作原理,以及实现中的一些变化,请阅读下面我的完整答案。


详细解答

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

注意:我不会编写所有代码,因为这会导致我重新编写您已经编写的大量代码——并且会产生更长的答案。相反,我将提供将解决方案应用于现有代码所需的步骤清单。

背景

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

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

<input asp-for="@Model[i].Quantity" />
Run Code Online (Sandbox Code Playgroud)

然后输出类似于以下 HTML 的内容:

<input id="0__Quantity" name="[0].Quantity" value="1" />
Run Code Online (Sandbox Code Playgroud)

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

public async Task<IActionResult> ProductsAsync(List<Product> products) { … }
Run Code Online (Sandbox Code Playgroud)

当你调用asp-for一个集合时,它总是从 开始索引0。并且当它将表单数据绑定到绑定模型时,它总是会开始[0]并计数。

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

注意:当你这样做时,你需要确保你仍然坚持ASP.NET Core MVC 已经熟悉的约定之一,以确保发生数据绑定。不过,如果您真的想要,您也可以创建自己的绑定约定

问题一:设置索引

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

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

    注意:如果card该类不是专门用于产品,您可以改为product为视图中的每个tr元素分配一个唯一的类,_DisplayOrder并对其进行计数。

  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);
    },
  })
})
Run Code Online (Sandbox Code Playgroud)
[HttpPost]
public IActionResult GetProduct(int Phoneid, string[] modelId, int startingIndex = 0) { … }
Run Code Online (Sandbox Code Playgroud)
  1. startingIndex通过视图模型将其中继到您的“部分”视图;例如,
public class ProductListViewModel {
  public int StartingIndex { get; set; }
  public List<Product> Products { get; set; }
}
Run Code Online (Sandbox Code Playgroud)
  1. 使用该值来偏移写入输出的索引:
<input id="@(Model.StartingIndex+i)__Quantity" name="[@(Model.StartingIndex+i)].Quantity" value="@Model.Products[i].Quantity" />
Run Code Online (Sandbox Code Playgroud)

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

笔记

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

小心你的索引!

集合模型绑定约定中,您会注意到一个警告:

使用下标数字 (... [0] ... [1] ...) 的数据格式必须确保它们从零开始按顺序编号。如果下标编号有任何间隙,则间隙后的所有项目都将被忽略。例如,如果下标是 0 和 2 而不是 0 和 1,则忽略第二项。

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

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

如上所述,任何具有name属性的表单元素都会将其数据提交给服务器。因此,无论您是使用asp-for、使用 HTML 手动编写表单还是使用 JavaScript 动态构建它都无关紧要。如果有一个带有name属性的表单元素,并且它在form被提交的范围内,它将被包含在有效负载中。

调试表单数据

您可能已经熟悉这一点,但如果不熟悉:如果您使用浏览器的开发者控制台,您将能够在提交表单时看到此信息作为页面元数据的一部分。例如,在谷歌浏览器中:

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

你应该看到类似的东西:

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

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

绑定问题

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

重复索引

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

馆藏名称

当您List<Products>使用asp-for标签助手绑定到 a时,我相信它会使用[i].Property命名约定。这是一个问题,因为你OrderViewModel不是一个集合。相反,这些需要与Products绑定模型上的属性相关联。这可以通过前缀namewith来完成Products。如果您使用asp-for绑定到Products视图模型上的属性,这将自动完成- 正如ProductListViewModel上面所建议的那样。但是由于您需要name根据IndexOffset无论如何动态生成's ,您可以将它们硬编码为模板的一部分:

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

精选产品

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

var selectedProducts = c.Products.Where(p => p.IsSelected).ToList();
…
repo.SetProducts(selectedProducts);
Run Code Online (Sandbox Code Playgroud)