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

I am facing the same problem in my MVC 3 applications. I have an idea about creating a new product, and this product can be assigned to one or more categories. Here are my first EF Code classes:

public class Product { public int ProductID { get; set; } public string Name { get; set; } public virtual ICollection<Category> Categories { get; set; } } public class Category { public int CategoryID { get; set; } public string Name { get; set; } public virtual ICollection<Product> Products { get; set; } } 

So, I create a presentation model to represent the product of the product and include the product and the list of categories:

 public class ProductEditViewModel { public Product Product { get; set; } public List<SelectListItem> CategorySelections { get; set; } public ProductEditViewModel(Product product, List<Category> categories) { this.Product = product; CategorySelections = categories.Select(c => new SelectListItem() { Text = c.Name, Value = c.CategoryID.ToString(), Selected = (product != null ? product.Categories.Contains(c) : false) }).ToList(); } } 

So, I am visualizing a view with an input for the name and a list of flags for each category (called "Product.Categories"). When my form is submitted back, I want to save the product with the categories associated with it (or if the ModelState is invalid to re-display the view with the selected categories that the user made intact).

 [HttpPost] public ActionResult Create(Product product) { if (ModelState.IsValid) { db.Products.Add(product); db.SaveChanges(); return RedirectToAction("Index"); } return View(new ProductEditViewModel(product, db.Categories.ToList())); } 

When I do this and select one or more categories, ModelState is invalid and returns an edit view with the following validation error:

The value '25, 2 'is invalid. // 25 and 2 are category identifiers

It seems to me that it cannot associate 25 and 2 with objects of the real category, but is there a standard way to use a custom ModelBinder that will allow me to translate identifiers into categories and attach them to the context?

+3
asp.net-mvc-3 entity-framework viewmodel model-binding
Aug 31 '11 at 15:54
source share
3 answers

Thanks @Slauma for leading me on the right track. Here are my message creation and editing methods, which describe in detail how to manage relationships (editing is a bit more complicated as it should add elements that do not exist in the database and delete elements that have been deleted and exist in the database). I added the SelectedCategories (List of ints) property to my ProductEditViewModel to save 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 this 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())); } 

This is more of the code I was hoping for, but it is really effective using stubs and only adding / removing what has changed.

+1
Sep 01 2018-11-11T00:
source share

What you can try is this: bind to 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'm 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 the collection viewModel.Product.Categories empty, so you will not get the element of the selected category in the view, and not the elements that the user selected earlier.

I don’t know exactly how you bind the collection to the view (your "list of flags"?), But when using a ListBox that allows multiple selection, there seems to be a solution according to this answer: Problems with selecting values ​​in ListBoxFor . I just asked Darin in the comments if the list of selected elements would also be associated with the ViewModel as a result of the post action, and he confirmed that.

+1
Aug 31 '11 at 16:26
source share

I had a similar problem a few days ago. Ended up using "hack" - MVC 3 - binding to complex type with list type property

Please leave a message if you find an alternative way.

0
Aug 31 '11 at 16:18
source share



All Articles