How do I ModelBind a many-to-many relationship with MVC 3 and Entity Framework Code First?

空扰寡人 提交于 2019-11-30 23:46:01
Slauma

What you could try is the following: Bind to your ViewModel instead of Product in your post action:

[HttpPost]
public ActionResult Create(ProductEditViewModel viewModel)
{
    if (ModelState.IsValid)
    {
        foreach (var value in viewModel.CategorySelections
                                       .Where(c => c.Selected)
                                       .Select(c => c.Value))
        {
            // Attach "stub" entity only with key to make EF aware that the
            // category already exists in the DB to avoid creating a new category
            var category = new Category { CategoryID = int.Parse(value) };
            db.Categories.Attach(category);

            viewModel.Product.Categories.Add(category);
        }
        db.Products.Add(viewModel.Product);
        db.SaveChanges();
        return RedirectToAction("Index");
    }

    return View(new ProductEditViewModel(
        viewModel.Product, db.Categories.ToList()));
}

I am not sure though if this is the "standard way".

Edit

The return case when the model is invalid cannot work in my example above because viewModel.Product.Categories collection is empty, so you would get no selected category item in the view and not the items which the user had selected before.

I don't know how exactly you bind the collection to the view (your "list of checkboxes"?) but when using a ListBox which allows multiple selection then there seems to be a solution along the lines of this answer: Challenges with selecting values in ListBoxFor. I just had asked Darin in the comments if the list of selected item ids also will get bound to the ViewModel in an post action and he confirmed that.

Thanks @Slauma, that got me on the right track. Here is my Create and Edit post methods that detail how to manage the relationships (the edit is a bit trickier, because it has to add items that don't exist in the database and delete items that have been removed and do exist in the database). I added a SelectedCategories property (List of ints) to my ProductEditViewModel to hold the result from the form.

[HttpPost]
public ActionResult Create(ProductEditViewModel)
{
    viewModel.Product.Categories = new List<Category>();

    foreach (var id in viewModel.SelectedCategories)
    {
        var category = new Category { CategoryID = id };
        db.Category.Attach(category);

        viewModel.Product.Categories.Add(category);
    }

    if (ModelState.IsValid)
    {
        db.Products.Add(viewModel.Product);
        db.SaveChanges();
        return RedirectToAction("Index");
    }

    return View(new ProductEditViewModel(viewModel.Product, GetCategories()));
}

For the Edit method I had to query the database for the current product and then compare that with the viewModel.

[HttpPost]
public ActionResult Edit(ProductEditViewModel viewModel)
{
    var product = db.Products.Find(viewModel.Product.ProductID);

    if (ModelState.IsValid)
    {
        UpdateModel<Product>(product, "Product");

        var keys = product.CategoryKeys; // Returns CategoryIDs

        // Add categories not already in database
        foreach (var id in viewModel.SelectedCategorys.Except(keys))
        {
            var category = new Category { CategoryID = id }; // Create a stub
            db.Categorys.Attach(category);

            product.Categories.Add(Category);
        }

        // Delete categories not in viewModel, but in database
        foreach (var id in keys.Except(viewModel.SelectedCategories))
        {
            var category = product.Categories.Where(c => c.CategoryID == id).Single();

            product.Categories.Remove(category);
        }

        db.SaveChanges();
        return RedirectToAction("Index");
    }
    else
    {
        // Update viewModel categories so it keeps users selections
        foreach (var id in viewModel.SelectedCategories)
        {
            var category = new Category { CategoryID = id }; // Create a stub
            db.Categories.Attach(category);

            viewModel.Product.Categories.Add(category);
        }
    }

    return View(new ProductEditViewModel(viewModel.Product, GetCategories()));
}

It is more code that I was hoping it would be, but it is actually pretty efficient with using the stubs and only adding/deleting what has changed.

I had a similar issue few days ago. Ended up using a "hack" - MVC 3 - Binding to a Complex Type with a List type property

Please leave a message if you find an alternative way.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!