Search code examples
c#asp.net-core-mvcmany-to-many

Edit many-to-many relation data in ASP.NET Core MVC


I have created tags for my website posts using Many-to-Many relationship data table. I want to edit/update the tag data in database table using Controller and ViewModel for posts

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

ViewModel:

public class EditPostViewModel
{
    public string? Title { get; set; }
    public string? Description { get; set; }
    public List<int>? TagId { get; set; }

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

Controller:

[HttpGet]
public async Task<IActionResult> Edit(int id)
{
    // Id Check
    var post = await _postInterface.GetByIdAsync(id);

    // Check
    if (post == null)
    {
        return View("Error");
    }


    var postVM = new EditPostViewModel
    {
        Title = post.Title,
        Description = post.Description,
        TagId = post.Tags, // Gives error CS0029

        // List
        TagList = await _context.Tags.ToListAsync()
    };

    return View(postVM);
}

[HttpPost] 
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, EditPostViewModel postVM)
{
    // Check
    if (!ModelState.IsValid)
    {
        ModelState.AddModelError("", "Failed to edit post");

        return View("Edit", postVM);
    }

    // Id Select
    var userPost = await _postInterface.GetByIdAsyncNoTracking(id);

    // Selected Tag
    var tags = await _tagInterface.GetAll();

    List<TagModel> selectedTags = new List<TagModel>();

    if (postVM.TagId != null && postVM.TagId.Any())
    {
        selectedTags = tags.Where(x => postVM.TagId.Contains(x.Id)).ToList();
    }



    if (userPost != null)
    {
        var post = new PostModel
        {
            Id = id,
            Title = postVM.Title,
            Description = postVM.Description,
            Tags = selectedTags
        };

        _postInterface.Update(post);

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

View:

@using SimpleWebsite.ViewModels.PostViewModel

@model EditPostViewModel

<!-- Code above -->


<!-- Tag -->
@if (Model.TagList != null)
{
    <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 name="TagId" 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 have resolved my issue. I have also updated the code on my GitHub Repository.

    Models:

    Added virtual to many-to-many database table in all models. No need to migrate or update database

    public class PostModel
    {
        [Key]
        public int Id { get; set; }
        public string? Title { get; set; }
        public string Description { get; set; }
    
        public List<TagModel> Tags { get; set; }
        public virtual List<PostTagModel> PostTags { get; set; }
    }
    
    
    public class TagModel
    {
        [Key]
        public int Id { get; set; }
        public string? Title { get; set; }
        public string Description { get; set; }
    
        public List<PostModel> Posts { get; set; }
        public virtual List<PostTagModel> PostTags { get; set; }
    }
    
    
    public class PostTagModel
    {
        public int PostId { get; set; }
        public int TagId { get; set; }
    
        public virtual PostModel Post { get; set; }
        public virtual TagModel Tag { get; set; }
    }
    

    ViewModels: Added SelectedItemViewModel

    public class SelectedItemViewModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public bool Selected { get; set; }
    }
    
    
    public class EditPostViewModel
    {
        public string? Title { get; set; }
        public string? Description { get; set; }
        public List<SelectedItemViewModel> Tag { get; set; }
    }
    

    Controller:

    [HttpGet]
    public async Task<IActionResult> Edit(int id)
    {
        // Id Check
        var post = await _postInterface.GetByIdAsync(id);
    
        // Check
        if (post == null)
        {
            return View("Error");
        }
    
        // Initialize Selected Tags
        var Results = from t in _context.Tags
                      select new
                      {
                          t.Id,
                          t.Title,
                          t.Description,
                          Checked = ((from pt in _context.PostTags
                                      where (pt.PostId == id) &
                                            (pt.TagId == t.Id)
                                      select pt).Count() > 0),
    
                      };
    
        // Tags List
        var TagList = new List<SelectedItemViewModel>();
    
        // Selected Tags
        foreach (var item in Results)
        {
            TagList.Add(new SelectedItemViewModel
            {
                Id = item.Id,
                Name = item.Title,
                Description = item.Description,
                Selected = item.Checked
            });
        }
    
        var postVM = new EditPostViewModel
        {
            Title = post.Title,
            Description = post.Description,
            Tag = TagList
        };
    
        return View(postVM);
    }
    
    
    [HttpPost] 
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Edit(int id, EditPostViewModel postVM)
    {
        // Check
        if (ModelState.IsValid)
        {
            ModelState.AddModelError("", "Failed to edit post");
    
            return View("Edit", postVM);
        }
    
        // Id Select
        var userPost = await _postInterface.GetByIdAsyncNoTracking(id);
    
        if (userPost != null)
        {
            var post = new PostModel
            {
                Id = id,
                Title = postVM.Title,
                Description = postVM.Description,
            };
    
            // Delete Selected Tags
            foreach (var item in _context.PostTags)
            {
                if (item.PostId == id)
                {
                    _context.Entry(item).State = EntityState.Deleted;
                }
            }
    
            // Update Selected tags
            foreach (var item in postVM.Tag)
            {
                if (item.Selected)
                {
                    _context.PostTags.Add(new PostTagModel()
                    {
                        PostId = id,
                        TagId = item.Id
                    });
                }
            }
    
            _postInterface.Update(post);
    
            return RedirectToAction("Index");
        }
        else
        {
            return View(postVM);
        }
    }
    

    View:

    @using SimpleWebsite.ViewModels.PostViewModel
    
    @model EditPostViewModel
    
    <!-- Code above -->
    
    
    <!-- Tag -->
    @if (Model.Tag != null)
    {
        <div class="col-md-12 mb-3">
            <label asp-for="Tag" class="control-label"></label>
    
            <br />
    
            <div class="form-check-inline">
                @for (int i = 0; i < Model.Tag.Count(); i++)
                {
                    <input type="checkbox" asp-for="@Model.Tag[i].Selected" class="form-check-input" id="@Model.Tag[i].Id">
                    <label class="form-check-label me-3" title="@Model.Tag[i].Description" for="@Model.Tag[i].Id">@Model.Tag[i].Name</label>
    
                    <input type="hidden" asp-for="@Model.Tag[i].Id">
                }
            </div>
    
            <span asp-validation-for="Tag" class="text-danger"></span>
        </div>
    }
    
    <!-- Code Below -->