Search code examples
c#asp.net-core-mvcmany-to-manyasp.net-mvc-viewmodel

Create data for Many-to-Many relationship table in ASP.NET Core MVC


I am creating tags for my website posts using Many-to-Many relationship. I managed to create PostTags database table using Entity Framework following the Microsoft Documentation.

I now want to insert tag data in database using Controller and ViewModel, when creating the post, but have no idea how to proceed.

GitHub Repository

My Code and what I have Tried-

Models:

public class PostModel
{
    [Key]
    public int Id { get; set; }
    public string? Title { get; set; }

    public List<TagModel> Tags { get; set; }
    public List<PostTagModel> PostTags { get; set; }
}


public class TagModel
{
    [Key]
    public int Id { get; set; }
    public string? Title { get; set; }

    public List<PostModel> Posts { get; set; }
    public List<PostTagModel> PostTags { get; set; }
}


public class PostTagModel
{
    public int PostId { get; set; }
    public int TagId { get; set; }

    public PostModel Post { get; set; }
    public TagModel Tag { get; set; }
}

DatabaseContext.cs:

public class DatabaseContext : DbContext
{
    public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options)
    {

    }

    public DbSet<PostModel> Posts { get; set; }
    public DbSet<TagModel> Tags { get; set; }


    public DbSet<PostTagModel> PostTags { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<PostModel>()
            .HasMany(e => e.Tags)
            .WithMany(e => e.Posts)
            .UsingEntity<PostTagModel>();
    }
}

ViewModel:

public class CreatePostViewModel
{
    public string? Title { get; set; }
    public int? TagId { get; set; }

    public List<TagModel> TagList { get; set; }
}

Controller:

CS0029: Cannot implicitly convert type 'int?' to 'System.Collections.GenericList<Website.Model.TagModel>

[HttpGet]
public IActionResult Create()
{
    // Tags initialization for view    

    return View();
}

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(CreatePostViewModel postVM)
{
    if (ModelState.IsValid)
    {
        var post = new PostModel
        {
            Title = postVM.Title,
            Tags = postVM.TagId  // CS0029 : cannot implicitly convert type 'int?' to 'System.Collections.GenericList<Website.Model.TagModel>
        };

        _postInterface.Add(post);

        return RedirectToAction("Index");
    }
    else
    {
        // Error
    }
    return View(postVM);
}

View:

@using SimpleWebsite.ViewModels.PostViewModel

@model CreatePostViewModel

<!-- Code above -->


<!-- Tag -->
<div class="col-md-12 mb-3">
    <label asp-for="TagId" class="control-label"></label>

    <div class="form-check-inline">
        @foreach (var item in Model.TagList)
        {
            <input type="checkbox" class="btn-check" id="@item.Id" value="@item.Id">
            <label asp-for="TagId" class="btn btn-outline-primary" for="@item.Id">@item.Title</label>
        }
    </div>

    <span asp-validation-for="TagId" class="text-danger"></span>
</div>


<!-- Code Below -->

Solution

  • I doubt that you can pass the TagId value to the controller as the checkbox needs to have the name attribute: name="TagId".

    Note that, with the loop, user can check for multiple checkboxes and would suggest to change the TagId property to TagIds with an int[]? type.

    <label asp-for="TagIds" class="control-label"></label>
    
    <div class="form-check-inline">
        @foreach (var item in Model.TagList)
        {
            <input type="checkbox" class="btn-check" id="@item.Id" value="@item.Id" name="TagIds">
            <label asp-for="TagIds" class="btn btn-outline-primary" for="@item.Id">@item.Title</label>
        }
     </div>
    
    public class CreatePostViewModel
    {
        public string? Title { get; set; }
        public int[]? TagIds { get; set; }
    
        public List<TagModel> TagList { get; set; }
    }
    

    In your controller, you have to retrieve the Tag data from the database and filter based on user-selected TagIds. And insert the post with the selectedTags.

    ...
    using System.Linq;
    
    private readonly ITagInterface _tagInterface;
    
    public PostController(DatabaseContext context, IPostInterface postInterface, ITagInterface tagInterface)
    {
        _context = context;
        _postInterface = postInterface;
        _tagInterface = tagInterface;
    }
    
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Create(CreatePostViewModel postVM)
    {
        if (ModelState.IsValid)
        {
            var tags = await _tagInterface.GetAll();
    
            List<TagModel> selectedTags = new List<TagModel>();
    
            if (postVM.TagIds != null && postVM.TagIds.Any())
                selectedTags = tags.Where(x => postVM.TagIds.Contains(x.Id))
                    .ToList();
    
            var post = new PostModel
            {
                Title = postVM.Title,
                Tags = selectedTags
            };
    
            _postInterface.Add(post);
    
            return RedirectToAction("Index");
        }
        else
        {
            // Error
        }
    
        return View(postVM);
    }