使用来自 select 元素 ASP.NET Core MVC 的属性保存 parent 项和 child 项

Saving parent item and child items with properties from select element ASP.NET Core MVC

在我正在处理的 ASP.NET 核心 MVC 网络应用程序中,我遇到了一个问题,我将通过以下示例进行说明。

假设用户必须保存图书及其标题和页数,以及一位或多位作者。对于每个作者,用户应该从下拉列表中选择他的姓名和角色。

模型有四个 类:

public class Author
{
    public int AuthorID { get; set; }
    public string AuthorName { get; set; }

    public Author(int id, string name)
    {
        AuthorID = id;
        AuthorName = name;
    }
}

public class AuthorRole
{
    public int AuthorRoleID { get; set; }
    public string AuthorRoleName { get; set; }

    public AuthorRole(int id, string name)
    {
        AuthorRoleID = id;
        AuthorRoleName = name;
    }
}

public class BookAuthor
{
    public int? BookAuthorID { get; set; }
    public int? BookID { get; set; }
    [DisplayName("Name")]
    public int? AuthorID { get; set; }
    [DisplayName("Role")]
    public int? AuthorRoleID { get; set; }
}

public class BookViewModel
{
    public int? BookID { get; set; }
    [DisplayName("Title")]
    public string Title { get; set; }
    [DisplayName("Number of pages")]
    public int NumberOfPages { get; set; }
    public SelectList AuthorsList { get; set; }
    public SelectList AuthorRolesList { get; set; }

    [DisplayName("Authors")]
    public List<BookAuthor> BookAuthors { get; set; }
    public BookViewModel()
    {
        BookAuthors = new List<BookAuthor>();
    }
}

表单初始化动作

[HttpGet]
        public async Task<IActionResult> AddBook()
        {
            BookViewModel book = new BookViewModel();
            PopulateDropdowns(book);
            book.BookAuthors.Add(new BookAuthor());
            return View(book);
        }

        private void PopulateDropdowns(BookViewModel book)
        {
            List<Author> authors = new List<Author>();
            authors.Add(new Author(1, "Morgan Beaman"));
            authors.Add(new Author(2, "Cleveland Lemmer"));
            authors.Add(new Author(3, "Joanie Wann"));
            authors.Add(new Author(4, "Emil Shupp"));
            authors.Add(new Author(5, "Zenia Witts"));
            book.AuthorsList = new SelectList(authors, "AuthorID", "AuthorName");

            List<AuthorRole> authorRoles = new List<AuthorRole>();
            authorRoles.Add(new AuthorRole(1, "Author"));
            authorRoles.Add(new AuthorRole(2, "Editor"));
            authorRoles.Add(new AuthorRole(3, "Translator"));
            book.AuthorRolesList = new SelectList(authorRoles, "AuthorRoleID", "AuthorRoleName");
        }

returns 观点:

@{
    Layout = "_DetailsLayout";
}

@model Test.BookViewModel

<div class="container-fluid">
    <div class="row justify-content-center">
        <div class="col-sm-8 col-lg-6">
            <form asp-action="SaveBook" class="form-horizontal">
                <div asp-validation-summary="ModelOnly" class="text-danger"></div>
                <input type="hidden" asp-for="BookID" />
                <div class="form-group">
                    <label asp-for="Title" class="control-label"></label>
                    <input asp-for="Title" class="form-control" />
                    <span asp-validation-for="Title" class="text-danger"></span>
                </div>

                <div class="form-group">
                    <label asp-for="NumberOfPages" class="control-label"></label>
                    <input asp-for="NumberOfPages" class="form-control" />
                    <span asp-validation-for="NumberOfPages" class="text-danger"></span>
                </div>

                <label asp-for="BookAuthors" class="control-label"></label>
                @await Html.PartialAsync("_AuthorDetails", Model)
                <div id="AuthorDetailsWrapper"></div>

                <div class="clearfix">
                    <a class="btn btn-link text-success btn-sm float-right" data-ajax="true"
                       data-ajax-url="@Url.Action("AddAuthor", new { id = Model.BookID })"
                       data-ajax-method="GET" data-ajax-mode="after"
                       data-ajax-loading-duration="400"
                       data-ajax-update="#AuthorDetailsWrapper" href="">ADD AUTHOR</a>
                </div>
                <div class="row mt-5">
                    <div class="col">
                        <a asp-action="Index" class="btn btn-outline-secondary block-on-mobile mb-2">Back</a>
                    </div>
                    <div class="col">
                        <button type="submit" class="btn btn-success float-right block-on-mobile">Save</button>
                    </div>
                </div>
            </form>
        </div>
    </div>
</div>

如果图书有多位作者,用户应单击调用操作的“添加作者”

[HttpGet]
        public IActionResult AddAuthor(int? bookid)
        {
            BookViewModel book = new BookViewModel();
            BookAuthor newAuthor = new BookAuthor();
            if (bookid != null)
            {
                newAuthor.BookID = bookid;
            }
            PopulateDropdowns(book);
            book.BookAuthors.Add(newAuthor);
            return PartialView("_AuthorDetails", book);
        }

returns 观点:

@model Test.BookViewModel

@for (var i = 0; i < Model.BookAuthors.Count; i++)
{
    string guid = Guid.NewGuid().ToString();
    <div class="form-row">
        <input type="hidden" name="BookAuthors.Index" value="@guid" />
        <input asp-for="@Model.BookAuthors[i].BookID" type="hidden" data-guid="@guid" />
        <input asp-for="@Model.BookAuthors[i].BookAuthorID" type="hidden" data-guid="@guid" />
        <div class="form-group col">
            <label asp-for="@Model.BookAuthors[i].AuthorID"></label>
            <select asp-for="@Model.BookAuthors[i].AuthorID" asp-items="Model.AuthorsList" data-guid="@guid" class="dropdowns">
                <option></option>
            </select>
            <span asp-validation-for="@Model.BookAuthors[i].AuthorID" class="text-danger" data-guid="@guid"></span>
        </div>
        <div class="form-group col">
            <label asp-for="@Model.BookAuthors[i].AuthorRoleID"></label>
            <select asp-for="@Model.BookAuthors[i].AuthorRoleID" asp-items="Model.AuthorRolesList" data-guid="@guid" class="dropdowns">
                <option></option>
            </select>
            <span asp-validation-for="@Model.BookAuthors[i].AuthorRoleID" data-guid="@guid" class="text-danger"></span>
        </div>
    </div>
}

在提交表单时,应将模型传递给操作:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SaveBook(BookViewModel book)
{
    // validation and saving book to database
    }

所有属性 - 标题和页数,还有完整的作者列表,每个都有 AuthorID 和 AuthorRoleID。
问题是作者的属性(为下拉列表选择的 AuthorID 和 AuthorRoleID)未被传递,即具有空值。
例如,如果我这样填写表格 Filled form, with two authors 对于列表 BookAuthors 的两个元素,预置 AuthorID 和 AuthorRoleID 为空: Data not passed to action

我认为出现问题是因为 AuthorID(和 AuthorRoleID)的“asp-for”标签助手为 select 元素的“id”和“name”属性生成了错误的值 - 它使用 0作为索引,而不是指导。所以而不是

<select data-guid="283a0dea-9d64-432f-a03d-8c1a167b062a" class="dropdowns" id="BookAuthors_283a0dea-9d64-432f-a03d-8c1a167b062a__AuthorID" name="BookAuthors[283a0dea-9d64-432f-a03d-8c1a167b062a].AuthorID">
                <option></option>
            <option value="1">Morgan Beaman</option>
<option value="2">Cleveland Lemmer</option>
<option value="3">Joanie Wann</option>
<option value="4">Emil Shupp</option>
<option value="5">Zenia Witts</option>
</select>

元素看起来像这样

<select data-guid="283a0dea-9d64-432f-a03d-8c1a167b062a" class="dropdowns" id="BookAuthors_0__AuthorID" name="BookAuthors[0].AuthorID">
                <option></option>
            <option value="1">Morgan Beaman</option>
<option value="2">Cleveland Lemmer</option>
<option value="3">Joanie Wann</option>
<option value="4">Emil Shupp</option>
<option value="5">Zenia Witts</option>
</select>

如果我在提交前在 DevTools 中编辑此元素(我用 guid 值替换零),所有内容都会按预期提交。

让我感到困惑的是,如果 AuthorID 不是 select 元素,而是 input 元素,则相同的代码仍然有效。例如,在 _AuthorDetails.cshtml 部分视图中,如果我替换

<select asp-for="@Model.BookAuthors[i].AuthorID" asp-items="Model.AuthorsList" data-guid="@guid" class="dropdowns">
                <option></option>
            </select>

<input asp-for="@Model.BookAuthors[i].AuthorID" data-guid="@guid" />

生成的元素是:

<input data-guid="926eb580-d52f-4132-ab43-b8fbb63e3d2c" type="number" id="BookAuthors_926eb580-d52f-4132-ab43-b8fbb63e3d2c__AuthorID" name="BookAuthors[926eb580-d52f-4132-ab43-b8fbb63e3d2c].AuthorID" value="" minlength="0">

如果我手动输入作者 ID 并提交表单,数据将通过: Filled part of form, 5 is entered for author's id Data passed to action

那么,我该如何修正这段代码,以便生成具有正确索引的 select 元素?
此外,替代解决方案和一般改进技巧也很受欢迎。可能的替代解决方案应该允许在同一页面上添加(以及稍后编辑,使用相同的视图)具有任意数量作者的书籍...

if I fill the form like this Filled form, with two authors for both elements of list BookAuthors, preoperties AuthorID and AuthorRoleID are null

那是因为你的partial view包含了一个input元素,它不是BookAuthors中的属性,而你定义为BookAuthors.Index,模型绑定系统会误解它:

<input type="hidden" name="BookAuthors.Index" value="@guid" />

要解决此问题,只需更改如下名称,避免使用 BookAuthors.xxxBookAuthors[i].xxx:

这样的名称
<input type="hidden" name="data" value="@guid" />

更新:

无法将列表传递给后端的原因是每次渲染局部视图时,BookAuthors 的索引总是 0.You 需要渲染 html 如下所示:

一个简单的解决方法是设置一个 属性 来记录 BookAuthors.Change 您的代码的索引,如下所示:

型号:

public class BookViewModel
{
    //more properties...
    //add Index property...
    public int Index { get; set; }

    [DisplayName("Authors")]
    public List<BookAuthor> BookAuthors { get; set; }
    public BookViewModel()
    {
        BookAuthors = new List<BookAuthor>();
    }
}

_AuthorDetails.cshtml:

点击添加作者按钮时不要使用标签helper.Because,索引将更改为1,标签助手不允许列表计数来自1.Use 姓名属性而不是标签助手。

@model BookViewModel

@{
    string guid = Guid.NewGuid().ToString();
}
<div class="form-row">
    <input type="hidden" name="Index" value="@guid" />
    <input name="BookAuthors[@Model.Index].BookID" type="hidden" data-guid="@guid" />
    <input name="BookAuthors[@Model.Index].BookAuthorID" type="hidden" data-guid="@guid" />
    <div class="form-group col">
        <label asp-for="BookAuthors[Model.Index].AuthorID"></label>
        <select name="BookAuthors[@Model.Index].AuthorID" asp-items="Model.AuthorsList" data-guid="@guid" class="dropdowns">
            <option></option>
        </select>
        <span asp-validation-for="BookAuthors[Model.Index].AuthorID" class="text-danger" data-guid="@guid"></span>
    </div>
    <div class="form-group col">
        <label asp-for="BookAuthors[Model.Index].AuthorRoleID"></label>
        <select name="BookAuthors[@Model.Index].AuthorRoleID" asp-items="Model.AuthorRolesList" data-guid="@guid" class="dropdowns">
            <option></option>
        </select>
        <span asp-validation-for="BookAuthors[Model.Index].AuthorRoleID" data-guid="@guid" class="text-danger"></span>
    </div>
</div>

控制器:

[HttpGet]
public IActionResult AddAuthor(int? bookid)
{
    var index = HttpContext.Session.GetInt32("item").Value;
    BookViewModel book = new BookViewModel();
    BookAuthor newAuthor = new BookAuthor();
    if (bookid != null)
    {
        newAuthor.BookID = bookid;
    }
    PopulateDropdowns(book);
    book.BookAuthors.Add(newAuthor);
    book.Index= index+1;
    HttpContext.Session.SetInt32("item", book.Index);
    return PartialView("_AuthorDetails", book);
}
public IActionResult AddBook()
{                      
    BookViewModel book = new BookViewModel();
    PopulateDropdowns(book);
    book.BookAuthors.Add(new BookAuthor());

    book.Index = 0;
    HttpContext.Session.SetInt32("item", book.Index);

    return View(book);
}

确保在 Startup.cs:

中注册会话
public void ConfigureServices(IServiceCollection services)
{       
    services.AddSession();
    //....
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{        
    app.UseSession();
    //...
}

结果: