简体   繁体   中英

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

In ASP.NET Core MVC web app I'm working on, I have a problem that I'll illustrate with the following example.

Let's say the user has to save book with its title and number of pages, and one or more authors. For each author, user should choose his name and role from dropdown list.

Model has four classes:

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>();
    }
}

The action for form initialization

[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 the view:

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

If book has more than one author, the user should click "ADD AUTHOR" which calls action

[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);
        }

that returns view:

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

On form submit, model should be passed to action:

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

with all properties - title and number of pages, but also whole list of authors, each with AuthorID and AuthorRoleID.
The problem is that authors' properties (AuthorID and AuthorRoleID, which are choosen for dropdown) are not passed, ie have null values.
For example, if I fill the form like this Filled form, with two authors for both elements of list BookAuthors, preoperties AuthorID and AuthorRoleID are null: Data not passed to action

I believe that the problem arises because "asp-for" tag helper for AuthorID (and AuthorRoleID) generates wrong values for "id" and "name" attributes of select element - it uses 0 as index, not guid. So instead of

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

element looks like this

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

If I edit this element in DevTools before submit (I replace zeros with guid value), everything is submitted as expected.

What confuses me is that the same code works if AuthorID is not select element, but input element. For example, in _AuthorDetails.cshtml partial view, if I replace

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

with

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

generated element is:

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

If I manually enter author's ID and submit form, data is passed: Filled part of form, 5 is entered for author's id Data passed to action

So, how can I fix this code so that select elements are generated with correct indexes?
Also, alternative solutions and general improvement tips are appreciated. Possible alternative solution should enable adding (and later editing, using the same view) book with arbitrary number of authors on the same page...

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

That is because your partial view contains an input element which is not the property in BookAuthors,but you define it as BookAuthors.Index ,the model binding system would misunderstand it:

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

To fix such issue,just change the name like below,avoid using name like BookAuthors.xxx or BookAuthors[i].xxx :

<input type="hidden" name="data" value="@guid" />

Update:

The reason for why you could not pass the list to the backend is because every time you render the partial view,the index of the BookAuthors is always 0.You need render the html like below: 在此处输入图片说明

A simple workaround is to set a property to record the index of the BookAuthors.Change your code like below:

Model:

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:

Do not use tag helper.Because when you click add author button,the index would be changed to 1,and tag helper does not allow the list count from 1.Use name property instead of tag helper.

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

Controller:

[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);
}

Be sure register session in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{       
    services.AddSession();
    //....
}

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

Result: 在此处输入图片说明

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM