asp.net-coreasp.net-core-mvc

Adding collection of items in Many to Many Relationship in ASP.NET Core MVC Problem (Code first)


I have two Entities that have many To many relationship, Book and Tags asp Created automatically a table for the two entities, Using the code first approach

  1. I am trying to add a collection of tags in the book creation, but the tag items are null also there is (select asp-for="Tags" ) but it shows me null in [httppost]create.

  2. I tried to add it in through context as it catches the values of tags I add, but there is an error

    cannot convert ......dbset<> to Models.tags

Code:

    public class Book
    {
        public int BookID { get; set; }
        [Required]
        public string Name { get; set; } = null!;

        //Navigation property
        public virtual ICollection<Tags>? Tags { get; set; }   
    }

    public class Tags
    {
        public int TagsID { get; set; } 
        public string TagName { get; set; } = null!;

        //Navigation property
        public virtual ICollection<Book>? Book { get; set; }
    }

    //DB Context
    public class BLabContext: DbContext
    {
        public DbSet<Book> Book { get; set; }   
        public DbSet<Tags> Tags { get; set; } 
    }

    // Book Controller
    public class BooksController : Controller
    {
        private readonly BLabContext _context;

        public BooksController(BLabContext context)
        {
            _context = context;
        }

        // Tags objects
       
        // public ICollection<Tags> Tags { get; set; }
       
        // GET: Books
        public async Task<IActionResult> Index()
        {
            return View(await _context.Book.ToListAsync());
        }

        // GET: Books/Create
(on get )
        public IActionResult Create()
        {
            ///πŸ’€πŸ’€πŸ’€πŸ’€πŸ’€πŸ’€πŸ’€
            var tags = _context.Tags.ToList();
            ViewData["tags"] = tags;
            //ViewBag.tags = tags;

            return View();
        }

        // POST: Books/Create
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("BookID,Name,Description,PublishedOn,Publisher,Price,ImageURL,1)πŸ’€TagsπŸ’€")] Book book)
        {
            if (ModelState.IsValid)
            {
2)πŸ’€
                var tags =  _context.Tags;
                _context.Add(book);
                await _context.SaveChangesAsync();

2)πŸ’€(cannot convert ......dbset<> to Models.tags  

                _context.Book.FirstOrDefault(b => b.BookID == book.BookID).Tags.Add(tags);

                await _context.SaveChangesAsync();

                return RedirectToAction(nameof(Index));
            }
            return View(book);
        }

The create view:

@using Book_Library.Models;
@model Book_Library.Models.Book

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

<h1>Create</h1>

<h4>Book</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create" enctype="multipart/form-data">

            <div asp-validation-summary="ModelOnly" class="text-danger"></div>

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

            @*    ///πŸ’€πŸ’€πŸ’€πŸ’€πŸ’€πŸ’€πŸ’€*@
            <div class="form-group">
                <label asp-for="Tags" class="control-label"></label>

                @*name="Tags"*@
                <select asp-for="Tags" multiple>
                    @foreach (var tag in @ViewData["tags"] as IList<Tags>)
                    {
                        <option value="@tag.TagName">@tag.TagName </option>
                    }
                    </select>
            </div>


            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

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

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

I expect to add a collection of tags to the book on creation, and also view those tags on the details view


Solution

  • You can create a ViewModel to achieve it, Please refer to this demo:

    ViewModel

    public class CreateBookViewModel
        {
            [Required]
            public string BookName { get; set; }
            public List<int> TagId { get; set; }
        }
    

    Controller

    public IActionResult Create()
            {  
                var tags = _context.Tags.ToList();
    
                List<SelectListItem> dropdown = new List<SelectListItem>();
                foreach (var item in tags)
                {
                    var listItem = new SelectListItem();
                    listItem.Text = item.TagName;
                    listItem.Value = item.TagsID.ToString();
                    dropdown.Add(listItem);
                }
               
                ViewBag.tags = dropdown;
    
                return View();
            }
    
            [HttpPost]
            [ValidateAntiForgeryToken]
            public async Task<IActionResult> Create(CreateBookViewModel book)
            {
                if (ModelState.IsValid)
                {
                    Book b = new Book();
                    b.Name = book.BookName;
                    if (book.TagId.Count>0)
                    {
                        foreach (var item in book.TagId)
                        {
                            var tag = _context.Tags.Where(x => x.TagsID == item).FirstOrDefault();
                            b.Tags.Add(tag);
                        }
                    }
    
                    _context.Book.Add(b);
    
                    await _context.SaveChangesAsync();
    
                    return RedirectToAction(nameof(Index));
                }
                return View(book);
            }
    

    View

    @model CreateBookViewModel
    
    @{
        ViewData["Title"] = "Create";
       
    }
    
    <h1>Create</h1>
    
    <h4>Book</h4>
    <hr />
    <div class="row">
        <div class="col-md-4">
            <form asp-action="Create" enctype="multipart/form-data">
    
                <div asp-validation-summary="ModelOnly" class="text-danger"></div>
    
                <div class="form-group">
                    <label asp-for="BookName" class="control-label"></label>
                    <input asp-for="BookName" class="form-control" />
                    <span asp-validation-for="BookName" class="text-danger"></span>
                </div>
    
                
                <div class="form-group">
                    <label asp-for="@Model.TagId" class="control-label"></label>
    
    
                    <select asp-for="@Model.TagId" asp-items="@ViewBag.tags" multiple></select>
                </div>
    
    
                <div class="form-group">
                    <input type="submit" value="Create" class="btn btn-primary" />
                </div>
            </form>
        </div>
    </div>
    
    <div>
        <a asp-action="Index">Back to List</a>
    </div>
    
    @section Scripts {
        @{
            await Html.RenderPartialAsync("_ValidationScriptsPartial");
        }
    }
    

    Now, You can add collection of Tags to Book successfully.

    enter image description here

    Note: You need to instantiate Tags in your Book class, Otherwise the above code will report a nullreferenceexception.

     public class Book
        {
            //.........
    
            //Navigation property
            public virtual ICollection<Tags>? Tags { get; set; }  = new List<Tags>();
        }