12

Hopefully a very simple question. But I'm using Code First with an MVC app and I have a Category and ServiceType object that have a many to many relationship:

public class Category
{
    public Category()
    {
        ServiceTypes = new HashSet<ServiceType>();
    }

    public Guid CategoryId { get; set; }

    [Required(ErrorMessage = "Name is required")]
    public string Name { get; set; }

    public virtual ICollection<ServiceType> ServiceTypes { get; set; }
}

The database has been generated correctly and contains a linking table called CategoryServiceTypes. My issue is I add items to my ServiceTypes collection and call save and although no error takes place, no rows are added to CategoryServiceTypes. When the below code gets to SaveChanges the count on category.ServiceTypes is 1, so something is definitely in the collection:

    [HttpPost]
    public ActionResult Edit(Category category, Guid[] serviceTypeIds)
    {
        if (ModelState.IsValid)
        {
            // Clear existing ServiceTypes
            category.ServiceTypes.Clear();

            // Add checked ServiceTypes
            foreach (Guid serviceType in serviceTypeIds)
            {
                ServiceType st = db.ServiceTypes.Find(serviceType);
                category.ServiceTypes.Add(st);
            }

            db.Entry(category).State = EntityState.Modified;
            db.SaveChanges();
            return RedirectToAction("Index");
        }
        return View(category);
    }

I hope I'm doing something obviously wrong here. Any ideas?

Thanks.

EDIT:

Although the below response is indeed the correct answer I thought I'd add the following final version of the Edit post method:

    [HttpPost]
    public ActionResult Edit(Category category, Guid[] serviceTypeIds)
    {
        if (ModelState.IsValid)
        {
            // Must set to modified or adding child records does not set to modified
            db.Entry(category).State = EntityState.Modified;

            // Force loading of ServiceTypes collection due to lazy loading
            db.Entry(category).Collection(st => st.ServiceTypes).Load(); 

            // Clear existing ServiceTypes
            category.ServiceTypes.Clear();

            // Set checked ServiceTypes
            if (serviceTypeIds != null)
            {
                foreach (Guid serviceType in serviceTypeIds)
                {
                    ServiceType st = db.ServiceTypes.Find(serviceType);
                    category.ServiceTypes.Add(st);
                }
            }

            db.SaveChanges();
            return RedirectToAction("Index");
        }
        return View(category);
    }

Notice the line that forces the loading of the ServiceTypes collection, this is needed as lazy loading does not include those child items, which means clearing the ServiceTypes collection did nothing.

Gary
  • 742
  • 8
  • 20

1 Answers1

8

Try to move the line where you attach the category to the context in front of the loop:

[HttpPost]
public ActionResult Edit(Category category, Guid[] serviceTypeIds)
{
    if (ModelState.IsValid)
    {
        // Clear existing ServiceTypes
        category.ServiceTypes.Clear();
        db.Entry(category).State = EntityState.Modified;
        // category attached now, state Modified

        // Add checked ServiceTypes
        foreach (Guid serviceType in serviceTypeIds)
        {
            ServiceType st = db.ServiceTypes.Find(serviceType);
            // st attached now, state Unchanged
            category.ServiceTypes.Add(st);
            // EF will detect this as a change of category and create SQL
            // to add rows to the link table when you call SaveChanges
        }

        db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(category);
}

In your code EF doesn't notice that you have added the servicetypes because you attach the category to the context when the servicetypes are already in the category.ServiceTypes collection and all servicetypes are already attached to the context in state Unchanged.

Slauma
  • 175,098
  • 59
  • 401
  • 420
  • Thanks Slauma, that was exactly the problem. – Gary Mar 04 '12 at 14:32
  • Although your answer now correctly saves entries to the database, I'm finding that when the Edit httppost method is entered, the category.ServiceTypes count is 0, even though on the Edit response method it definitely contains 1 saved ServiceType. Any ideas what is causing that? – Gary Mar 04 '12 at 14:44
  • @Gary: This would be a MVC modelbinder problem. For me it looks that it doesn't matter really if `category.ServiceTypes` contains something because the first thing you are doing is clearing that list anyway. Important is only the `Guid[] serviceTypeIds` collection because you are loading the ServiceType from the DB with `Find` using those Ids and then create the association with the loaded entity. – Slauma Mar 04 '12 at 14:50
  • The issue is because the Category from the MVC Modelbinder does not contain any ServiceTypes (even though they exist in the DB) the Clear method does not do anything. So it actually attempts to add a duplicate child row in CategoryServiceTypes and obviosly fails. Do you have any idea why the collection is empty from the MVC Modelbinder? – Gary Mar 04 '12 at 16:22
  • @Gary: No. It would be necessary to see your view code in more detail. If I understand you correctly your view is supposed to not only add a relationship between category and servicetype, but also to delete a relationship? Basically you must compare what's in the DB and what comes from the view (in the Guid collection?) to detect what has been added, deleted and what didn't change. Possibly you need something along the lines of this: http://stackoverflow.com/a/8869774/270591 (especially the last code snippet in that answer). Maybe create a new question to describe the problem in more detail. – Slauma Mar 04 '12 at 16:49
  • Hi Slauma, thanks a lot. I think I understand the issue here now, and the snippet you've sent me will hopefully help me resolve it. If not I'll open a new question. – Gary Mar 05 '12 at 10:08