I\'m coming across the same problem in my MVC 3 applications. I\'ve got a view to create an new product and that product can be assigned to one or more categories. Here are
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.
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.
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.