Update relationships on edit POST with model bind from MVC controller

Using Entity Framework with a many-to-many relationship, I'm confused how to update that relationship from an ASP.NET MVC controller where the model is bound.

For example, a blog: where posts have many tags, and tags have many posts.

Posts controller edit action fails to lazy load tags entities:

public class PostsController : Controller
{
    [Route("posts/edit/{id}")]
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Edit([Bind(Include = "Id,Title,Tags")] Post post)
    {
        // Post tags is null
        Post.tags.ToList();
    }
}

Above, the HTTP Post binds properties to the model; however, the Post.tags relationship is null.

There's no way for me to query, .Include(p => p.Tags), or Attach() the post to retrieve the related tag entities using this [Bind()].

On the view side, I'm using a tokenizer and passing formdata - I'm not using a MVC list component. So, the issue is not binding the view formdata - the issue is that the .tags property is not lazy loading the entities.

This relationship is functional - from the Razor cshtml view I am able to traverse the collection and view children tags.

From the Post View, I can view tags (this works)

@foreach (var tag in Model.Tags) {
}

From the Tag View, I can view posts (this works)

@foreach (var post in Model.Posts) {
}

Likewise on create action from the Posts controller, I can create new tag entities and persist their relationship.

public class PostsController : Controller
{
    [Route("posts/create")]
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Create([Bind(Include = "Title,Content")] Post post)
    {
        string[] tagNames = this.Request.Form["TagNames"].Split(',').Select(tag => tag.Trim()).ToArray();

        post.Tags = new HashSet<Tag>();

        Tag tag = new Tag();
        post.Tags.Add(tag);

        if (ModelState.IsValid)
        {
            db.posts.Add(post);
            await db.SaveChangesAsync();
            return RedirectToAction("Admin");
        }

        return View(post);
    }

These relationships work everywhere except for this edit HTTP Post. For reference, they are defined as:

public class Post
{
    public virtual ICollection<Tag> Tags { get; set; }
}

public class Tag
{
    public virtual ICollection<Post> Posts { get; set; }
}

Only on HTTP Post of edit action can I not access tags of posts.

How can I load related tags to alter that collection?

Answers


Your problem has nothing to do with Entity framework. It is basically an issue when you post a model/viewmodel with a collection property from your form, the collection property becomes null.

You can solve this by using EditorTemplates.

It looks like you are using the entity classes generated by Entity framework in your views. Generally this is not a good idea because now your UI layer is tightly coupled to EF entities. What if tomorrow you want to change your data access code implemenation form EF to something else for any reasons ?

So Let's create some viewmodels to be used in the UI layer. Viewmodels are simple POCO classes. The viewmodels will have properties which the view absolutely need. Do not just copy all your entity class properties and paste in your viewmodels.

public class PostViewModel
{
    public int Id { set; get; }
    public string Title { set; get; }
    public List<PostTagViewModel> Tags { set; get; }
}

public class PostTagViewModel
{
    public int Id { set; get; }
    public string TagName { set; get; }
    public bool IsSelected { set; get; }
}

Now in your GET action, you will create an object of your PostViewModel class, Initialize the Tags collection and send to the view.

public ActionResult Create()
{
    var v =new PostViewModel();
    v.Tags = GetTags();
    return View(v);
}
private List<PostTagViewModel> GetTags()
{
    var db = new YourDbContext();
    return db.Tags.Select(x=> new PostTagViewModel { Id=x.Id, TagName=x.Name})
         .ToList();

} 

Now, Let's create an editor template. Go to the ~/Views/YourControllerName directory and create a sub directory called EditorTemplates. Create a new view there with the name PostTagViewModel.cshtml.

Add the below code to the new file.

@model YourNamespaceHere.PostTagViewModel
<div>
    @Model.TagName
    @Html.CheckBoxFor(s=>s.IsSelected)
    @Html.HiddenFor(s=>s.Id)      
</div>

Now, in our main view (create.cshtml) which is strongly typed to PostViewModel, we will call Html.EditorFor helper method to use the editor template.

@model YourNamespaceHere.PostViewModel
@using (Html.BeginForm())
{
    <label>Post title</label>
    @Html.TextBoxFor(s=>s.Title)

    <h3>Select tags</h3>
    @Html.EditorFor(s=>s.Tags)

    <input type="submit"/>
}

Now in your HttpPost action method, you can inspect the posted model for Tags collection.

[HttpPost]
public ActionResult Create(PostViewModel model)
{
    if (ModelState.IsValid)
    {
       // Put a break point here and inspect model.
        foreach (var tag in model.Tags)
        {
            if (tag.IsSelected)
            {
                // Tag was checked from UI.Save it
            }
        }
        // to do : Save Post,Tags and then redirect.
    }
    model.Tags = GetTags(); //reload tags again 
    return View(model);
}

So since our HttpPost action's parameter is an object of PostViewModel, we need to map it to your Entity classes to save it using entity framework.

var post= new Post { Title = model.Title };
foreach(var t in model.Tags)
{
   if(t.IsSelected)
   {
      var t = dbContext.Tags.FirstOrDefault(s=>s.Id==t.Id);
      if(t!=null)
      { 
        post.Tags.Add(t);
      }
   }
}
dbContext.Posts.Add(post);
await dbContext.SaveChangesAsync();

Your create method that missing an Id parametres of post that you want to insert tags. You have to declare Post Id at the begining of the method.

[Route("posts/create")]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create([Bind(Include = "Title,Content")] Post post)
{
        Post nPost= db.Posts.FirstOrDefault(x => x.Id == post.Id);
        nPost.Id= post.Id;
        nPost.Title= post.Title;
        string[] tagNames = this.Request.Form["TagNames"].Split(',').Select(tag => tag.Trim()).ToArray();
        foreach(string item in tagNames)
        {
          //First check if you got tag in Tags table
          Tag tg= db.Tags.FirstOrDefault(x => x.Name.ToLower() == item.ToLower().Trim());
          // If no row than create one.
          if (tg== null)
          {
             tg= new Tag();
             tg.Name= item;
             db.Tags.Add(tg);
             await db.SaveChanges();
           }
          // Now adding those tags to Post Tags.
           if (nPost.Tags.FirstOrDefault(x => x.Id == tg.Id) == null)
           {
              nPost.Tags.Add(etk);
              await db.SaveChanges();
           }
        }
        if (ModelState.IsValid)
        {
          await db.SaveChangesAsync();
          return RedirectToAction("Admin");
        }
    return View(post);
}

That's it.


Need Your Help

Ensure NHibernate SessionFactory is only created once

c# nhibernate fluent-nhibernate sessionfactory

I have written an NHibernateSessionFactory class which holds a static Nhibernate ISessionFactory. This is used to make sure we only have one session factory, and the first time OpenSession() is cal...

how to change the window style of a form outside your app?

c# .net windows handles

how to change the window style of a form outside your app?hard question?