0

The environment is VS2019, ASP.NET Core MVC 2.1. I'm fairly new to this. At post the BookCategory list of Book object is null. I can't figure out which part (or parts) is wrong.

There are 3 models, Book, Category and the join model BookCategory. Book and Category are many-to-many, the relationship is stored in the BookCategory table.

public class Book
{
    public int BookId { get; set; }

    [Required]
    public string Title { get; set; }

    public ICollection<BookCategory> BookCategories { get; set; }
}

public class Category
{
    public int CategoryId { get; set; }
    public string CategoryName { get; set; }

    public ICollection<BookCategory> BookCategories { get; set; }
}

public class BookCategory
{
    public int BookId { get; set; }
    public Book Book { get; set; }
    public int CategoryId { get; set; }
    public Category Category { get; set; }

    public bool IsSelected { get; set; }
}

The BooksController is shown below. GetBookAndBookCategoryData will get a Book object with associated BookCategory list. PopulateBookCategoriesList will add a Category list to the view bag.

    [HttpGet]
    public async Task<IActionResult> Book(int? id)
    {
        int bookId = id.GetValueOrDefault();

        await PopulateBookCategoriesList();
        var book = await GetBookAndBookCategoryData(bookId);
        return View(book);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Book(int? id, Book book)
    {
        id = id ?? 0;
        if (id != book.BookId)      // ?? means coalesce, it takes the first non-null value.
        {
            return NotFound();
        }

        var category = _context.Categories;

        if (ModelState.IsValid)
        {
            // if create a new book
            if (id == 0)
            {
                _context.Add(book);
                await _context.SaveChangesAsync();

                foreach (var bc in book.BookCategories)
                {
                    var bcat = new BookCategory() { BookId = book.BookId, CategoryId = bc.CategoryId, IsSelected = bc.IsSelected };
                    _context.Add(bcat);
                }

                await _context.SaveChangesAsync();
            }
            else
            {
                _context.Update(book);

                foreach (var i in book.BookCategories)
                {
                    var bcat = _context.BookCategories.FirstOrDefault(bc => bc.BookId == book.BookId && bc.CategoryId == i.CategoryId);
                    bcat.IsSelected = i.IsSelected;
                    _context.Update(bcat);
                }
            }

            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }

        return View(book);
    }

    private async Task<Book>  GetBookAndBookCategoryData(int BookId = 0)
    {
        var book = new Book();
        var category = await _context.Categories.ToListAsync();
        if (BookId == 0)
        {
                            book.BookCategories = new List<BookCategory>();
            foreach (var c in category)     
            {
                book.BookCategories.Add(new BookCategory() { BookId = BookId, CategoryId = c.CategoryId, IsSelected = false });
            }
        }
        else
        {
            book = await _context.Books.FindAsync(BookId);

            if (book.BookCategories is null || book.BookCategories.Count == 0)
            {
                book.BookCategories = new List<BookCategory>();

                foreach (var c in _context.Categories)
                {
                    book.BookCategories.Add(new BookCategory() { BookId = book.BookId, CategoryId = c.CategoryId, IsSelected = false });
                }
            }
        }

        return book;
    }

    private async Task PopulateBookCategoriesList()
    {
        var list = await _context.Categories.ToListAsync();
        ViewBag.CategoryList = list;
    }

In DBContext:

public class ApplicationDbContext : IdentityDbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    public DbSet<MvcTest.Models.Book> Books { get; set; }
    public DbSet<MvcTest.Models.Category> Categories { get; set; }

    public DbSet<MvcTest.Models.BookCategory> BookCategories { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Category>().HasData(
            new Category { CategoryId = 1, CategoryName = "AAA" },
            new Category { CategoryId = 2, CategoryName = "BBB" },
            new Category { CategoryId = 3, CategoryName = "CCC" },
            new Category { CategoryId = 4, CategoryName = "DDD" },
            new Category { CategoryId = 5, CategoryName = "EEE" },
            new Category { CategoryId = 6, CategoryName = "FFF" },
            new Category { CategoryId = 7, CategoryName = "GGG" }
            );

        modelBuilder.Entity<BookCategory>()
            .HasKey(bc => new { bc.BookId, bc.CategoryId });
        modelBuilder.Entity<BookCategory>()
            .HasOne(bc => bc.Book)
            .WithMany(b => b.BookCategories)
            .HasForeignKey(bc => bc.BookId);
        modelBuilder.Entity<BookCategory>()
            .HasOne(bc => bc.Category)
            .WithMany(c => c.BookCategories)
            .HasForeignKey(bc => bc.CategoryId);

        base.OnModelCreating(modelBuilder);
    }
}

The View:

@model MvcTest.Models.Book

@{
    ViewData["Title"] = "Create";
}

<h2>Create</h2>

<h4>Book</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Book">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="BookId" class="control-label"></label>
                <input asp-for="BookId" disabled="disabled" class="form-control" />
                <input asp-for="BookId" class="form-control invisible" />
                <span asp-validation-for="BookId" class="text-danger"></span>
            </div>
            <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">
                @{ var categoryList = ViewBag.CategoryList as List<Category>; }
                @foreach (var bc in Model.BookCategories)
                {
                    <div class="col-md-3 col-sm-6">
                        <div class="checkbox">
                            <label>
                                <input type="checkbox" checked="@bc.IsSelected" name="BookCategories"
                                       value="@bc.CategoryId">@categoryList.Where(c => c.CategoryId == bc.CategoryId).SingleOrDefault().CategoryName
                            </label>
                        </div>
                    </div>
                }
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

It's a lot of code, I thank you for your time and patience.

2
  • Could you please provide a request? Network - right-click on request -> copy-> Copy as cUrl bash Commented Dec 5, 2019 at 20:43
  • @BasilKosovan it doesn't allow me to copy full cUrl bash here, the parsed request form is: "Title: 123 SelectedCategoryValues: 1 SelectedCategoryValues: 2". Title is 123, 1st & 2nd checkboxes are checked, which are reflected in the request Commented Dec 5, 2019 at 21:06

2 Answers 2

1

The name for your checkboxs is SelectedCategoryValues, when you submit, you need a List<int> SelectedCategoryValues on action parameter to store all selected categories's id list.And you could not directly map to the book.BookCategories using form submit.

Try to use below code to populate your BookCategory list of Book object in Post action:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Book(int? id, List<int> SelectedCategoryValues, Book book)
    {
        var bookCategories = new List<BookCategory>();
        foreach (var c in _context.Categories)
        {
            bookCategories.Add(
                new BookCategory() { BookId = book.BookId, CategoryId = c.CategoryId, IsSelected = SelectedCategoryValues.Contains(c.CategoryId) ? true : false }
            );

        }
        book.BookCategories = bookCategories;
        //...
    }
Sign up to request clarification or add additional context in comments.

Comments

0

How are you retrieving it from the database? if you want the book object and the category object to be included in the model, you need to add a

.Include(x => x.Book)
.Include(x => x.Category)

for the book object also to be included in the object returned from the database

2 Comments

Hi I'm not fully understand your answer. I'm having trouble when posting data to controller from browser, instead of retrieving it from database (not there yet LOL).
Can you post the full view here to clarification?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.