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 my EF Code First Model 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 view model for the create product view and include the product and a list of the 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 render a view with an input for the name and a list of checkboxes for each category (named "Product.Categories"). When my form gets posted back I want to save the product with its associated categories (or if the ModelState is invalid, to redisplay the view with the category selections 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 that and select one or more categories, the ModelState is invalid and it returns the Edit view with the following validation error:
The value '25,2' is invalid. // 25 and 2 being the CategoryIDs
It makes sense to me that it can't bind 25 and 2 into actual category objects, but is there a standard way to use a custom ModelBinder that would allow me to translate IDs into Categories and attach them to the context?
What you could try is the following: Bind to your ViewModel instead of
Product
in your post action: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 becauseviewModel.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.
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.
For the Edit method I had to query the database for the current product and then compare that with the viewModel.
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.