带有由 Ajax 和局部视图动态修改的嵌套集合的 CreateModel

问题描述

我有一个非常基本的案例,我有一个解决方案,但我认为这种方式不是最干净/高效/便携的。

当然,为了便于阅读,我简化了模型,实际情况要复杂得多。

型号:

public partial class Referal
{
    public Referal()
    {
        Childrens = new List<Children>();
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public List<Children> Childrens { get; set; }
}

public partial class Children
{
    public int Id { get; set; }
    public string Firstname { get; set; }
    public bool Vaccinated { get; set; }
    public int ReferalId { get; set; }
    
    public virtual Referal Referal { get; set; }

}

我认为 ModelBuilder 等的附加代码片段并不相关。

所以我有一个带有 Referal 类型的 CreateModel 的 Razor 页面,我预加载一个空子级,以便在页面上包含 Referal 的必填字段和第一个子级的必填字段:

public class CreateModel : PageModel
{
    [BindProperty]
    public Referal Input { get; set; }

    public async Task OnGetAsync()
    {
        Input = new() { Childrens = new() { new() } };
    }

    public async Task<PartialViewResult> OnPostPartialChildrenAsync(int? id)
    {
        if (id == null)
        {
            Input.Childrens.Add(new());
        }
        
        return Partial("_AddChildren",this);
    }
}

创建 Razor 页面(再次简化了严格的相关部分):

@page "{handler?}/{id?}"
@model CreateModel

<section class="extended">
<form method="post" id="souscription">

    <div class="form-group">
        <label asp-for="Input.Name" class="control-label">
        </label><input asp-for="Input.Name" class="form-control" />
    </div>

    <div id="nestedChildrens">
        @Html.EditorFor(Model => Model.Input.Childrens)
    </div>
    <button type="button" class="cgicon-register intermediary" data-nested="childrens">Add a child</button>

    <button type="submit" class="cgicon-send mainbutton">Create Now</button>

</form>
</section>

如您所见,子组件由 EditorTemplate 加载。

@model Models.Children

<div class="children">
<div class="form-group-header">
    Add child
    <button type="button" data-nested="childrens" data-removal="@Model.NId"></button>
</div>
<div class="form-group">
    <label asp-for="Firstname" class="control-label">
    </label><input asp-for="Firstname" class="form-control" />
</div>
<div class="form-group form-check">
    <label class="form-check-label">
        <input class="form-check-input" asp-for="Vaccinated" /> Got his fix already
    </label>
</div>
</div>

所以现在的想法是,用户可以点击“添加一个孩子”;数据将通过处理程序“PartialChildren”发布在创建页面上,如果需要添加子项,并返回 _AddChildren 部分视图。对于孩子添加,没问题:创建了“孩子”块,然后发布时我的 CreateModel 包含两个孩子,这是规则。

用户应该能够在保存数据之前删除创建的子项之一。我已经尝试了很多服务器端代码来做到这一点,在 OnPostPartialChildrenAsync 函数中,例如 Input.Childrens.removeAt()ModelState.Clear(),但没有任何效果

我无法摆脱的典型行为是我删除一个孩子,我的 post 函数删除了我的 createModel 的孩子并将其发送回来;孩子不见了。然后我再次发布(验证器,添加一个新孩子,无论如何),删除的孩子再次弹出(因为发布一次,仍然在 modelState 或 somewhere )。

我尝试禁用链接到已删除子块的所有字段,但是如果用户删除一个字段,因为表单不包含任何 Input.Childrens[0] 表单元素,我的所有子元素都将丢失。

唯一有效的方法是在创建 Razor 页面上使用这段 Javascript :

<script>
    manageChildren(document);
    function manageChildren(childContainer) {
        [].forEach.call(childContainer.querySelectorAll("button[data-nested='childrens']"),function (button,index) {
            button.addEventListener("click",function () {
                let id = "";
                if (!$(this).hasClass("cgicon-register")) {
                    var brotherHood = childContainer.querySelectorAll(".children").length;
                    if(brotherHood<=1) return false;
                    [].forEach.call(childContainer.querySelectorAll("[name^='Input.Childrens[" + index+ "]'"),function (input) {
                        input.setAttribute("disabled","true");
                    });
                    for (i = id + 1; i < brotherHood; i++) {
                        [].forEach.call(childContainer.querySelectorAll("[name^='Input.Childrens[" + i + "]'"),function (input) {
                            input.setAttribute("name",input.getAttribute("name").replace("Input.Childrens[" + i + "]","Input.Childrens[" + (i - 1) + "]"));
                        });
                    }
                    id = index;                         
                }
                $.ajax({
                    async: true,data: $("#souscription").serialize(),type: "POST",url: "/Create/PartialChildren/" + id,success: function (nestedList) {
                        $("#nestedChildrens").html(nestedList);
                        manageChildren(document.getElementById("nestedChildrens"));
                    }
                });
            });
        });
    }
</script>

所以我删除了所有名为 [Input.Childrens[index]* 的字段,然后重命名每个具有更高索引的字段,索引小于 1。

即使它运行良好,即使没有数据丢失并且用户可以真正安全地使用它,我也不太喜欢用客户端脚本来取代输入模型机制。此外,实际上不可能将这段代码移植到其他实体的其他场景中。

我很想知道我是否错过了一种神奇的方式,让 C# 在保存之前删除我的 createModel 的特定子项(因此所有输入子项的 id = 0 ),而无需此 js 客户端处理。>

感谢您的阅读和帮助。

解决方法

发布表单时有两种方法可以绑定集合。一个,这就是你目前正在做的,是使用一个从 0 开始并为集合中的每个元素递增 1 的顺序索引。

另一种是使用显式索引,它可以是不需要连续的任何值。它甚至不需要是数字。这种方法需要为每个项目添加一个名为 [property].Index 的隐藏字段,表示项目的索引。

<input type="hidden" name="Input.Children.Index" value="A" />
<input type="text" name="Input.Children[A].FirstName" />

然后,如果您从集合中删除元素,则不需要重新索引剩余的元素。

查看更多信息:https://www.learnrazorpages.com/razor-pages/model-binding#binding-complex-collections