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

122 Views Asked by At

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

There are 2 best solutions below

0
Yong Shun On BEST ANSWER

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);
}
1
Miles On

Currently there can only be one tag in your CreatePostViewModel, since it is stored as int TagId. Change this to a list to store more tags:

public class CreatePostViewModel
{
    public string? Title { get; set; }
    public List<int> SelectedTagIds { get; set; } = new List<int>();  // To store multiple tag IDs

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

You need to update the view accordingly:

@model CreatePostViewModel

<!-- Other fields -->

<!-- Tag Selection -->
<div class="col-md-12 mb-3">
    <label asp-for="SelectedTagIds" class="control-label"></label>
    <div class="form-check-inline">
        @foreach (var tag in Model.TagList)
        {
            <input type="checkbox" class="btn-check" name="SelectedTagIds" id="[email protected]" value="@tag.Id">
            <label class="btn btn-outline-primary" for="[email protected]">@tag.Title</label>
        }
    </div>
</div>

<!-- Submit button and other code -->

When using Entity Framework, this is how to update the database:

  • open command line
  • navigate to the directory where your project is in
  • run:
dotnet ef migrations add UpdatePostTagRelationship //or whatever you want to call it
  • when this has finished, run:
dotnet ef database update

You can open the command line of your laptop or do it in the command line of the IDE you are using.