EF 4 CTP 5 saving a many-to-many navigation property
I'm using EF4 CTP5 and am having trouble saving records back to the database. I have Contact and ContactType entities. As the post title states, I have set up a many-to-many navigation property between the tables.
The problem is with validating the ContactType values. ModelState.IsValid is false because it's unable to convert the values passed back from the form (a string array of ContactType id's into ContactType objects.
POCO's
public partial class Contact
{
public Contact()
{
this.ContactTypes = new HashSet<ContactType>();
}
// Primitive properties
public int ContactId { get; set; }
public string ContactName { get; set; }
// Navigation properties
public virtual ICollection<ContactType> ContactTypes { get; set; }
}
public partial class ContactType
{
public ContactType()
{
this.Contacts = new HashSet<Contact>();
}
// Primitive properties
public int ContactTypeId { get; set; }
public string Name { get; set; }
// Navigation properties
public virtual ICollection<Contact> Contacts { get; set; }
}
Controller
//
// GET: /Contact/Edit/5
public virtual ActionResult Edit(int id)
{
Contact contact = context.Contacts.Include(c => c.ContactTypes).Single(x => x.ContactId == id);
ViewData["ContactTypesAll"] = GetTypesList();
return View(contact);
}
//
// POST: /Contact/Edit/5
[HttpPost]
public virtual ActionResult Edit(Contact contact)
{
if (ModelState.IsValid)
{
context.Entry(contact).State = EntityState.Modified;
context.SaveChanges();
return RedirectToAction("Index");
}
ViewData["ContactTypesAll"] = GetTypesList();
return View(contact);
}
View
<div class="field-block">
@Html.LabelFor(model => model.ContactId)
@Html.EditorFor(model => model.ContactId, new { fieldName = "ContactId" })
@Html.ValidationMessageFor(model => model.ContactId)
</div>
<div class="field-block">
@Html.LabelFor(model => model.OrganizationNameInternal)
@Html.EditorFor(model => mode开发者_运维问答l.OrganizationNameInternal)
@Html.ValidationMessageFor(model => model.OrganizationNameInternal)
</div>
<div class="field-block">
@Html.LabelFor(model => model.ContactTypes)
@Html.ListBoxFor(modelContactType,
new MultiSelectList((IEnumerable<TDAMISObjects.ContactType>)ViewData["ContactTypesAll"],
"ContactTypeId",
"Name",
Model.ContactTypes))
@Html.ValidationMessageFor(model => model.ContactTypes)
</div>
ModelState error
ModelState.Values.ElementAt(2).Value
{System.Web.Mvc.ValueProviderResult}
AttemptedValue: "5"
Culture: {en-US}
RawValue: {string[1]}
ModelState.Values.ElementAt(2).Errors[0]
{System.Web.Mvc.ModelError}
ErrorMessage: ""
Exception: {"The parameter conversion from type 'System.String' to type 'ProjectObjects.ContactType' failed because no type converter can convert between these types."}
So it seems pretty clear what the problem is, but I can't seem to find the solution. I have tried to manually convert the ContactType id's into ContactType objects, and adding them to the Contact object passed into the Edit function (called 'contact'):
contact.ContactTypes.Clear();
string[] ids = this.HttpContext.Request.Form["ContactTypes"].Split(',');
for(int i = 0; i< ids.Length; i++)
{
int x = Convert.ToInt32(ids[i]);
ContactType selectedType = context.ContactTypes.Single(t => t.ContactTypeId == x);
contact.ContactTypes.Add(selectedType);
}
but the error persists. I've also tried calling
context.ChangeTracker.DetectChanges();
but that did not do the trick. I also manually set the ValueProviderResult for the value that will not validate, using
ModelState.SetModelValue("ContactTypes", val);
Which also did not work. I feel like I'm missing something basic here. Any ideas?
Thanks, Steve
Well after more work on this, I found a work around. Basically, I had to ignore the validation errors, then manually remove existing ContactTypes, then add in ones selected by the user. I did try to build a custom validator for the Contact.ContactTypes propery, but a ContactType object was always passed in to that method; I never saw the array of strings. Admittedly, that was the first custom validator I have built, so perhaps I was missing something.
Anyway, here's the Edit method I ended up with:
//
// POST: /Contact/Edit/5
[HttpPost]
public virtual ActionResult Edit(Contact contact)
{
// clear up ModelState.IsValid for contact type
ModelState.Remove("ContactTypes");
if(ModelState.IsValid)
{
// remove all contact types for contact
Contact dummy = context.Contacts.Single(c => c.ContactId == contact.ContactId);
if(dummy.ContactTypes.Count > 0)
{
dummy.ContactTypes.Clear();
context.Entry(dummy).State = EntityState.Modified;
context.SaveChanges();
}
context.Detach(dummy);
context.Contacts.Attach(contact);
// add currently selected contact types, then save
string[] ids = this.HttpContext.Request.Form["ContactTypes"].Split(',');
for(int i = 0; i< ids.Length; i++)
{
int x = Convert.ToInt32(ids[i]);
ContactType selectedType = context.ContactTypes.Single(t => t.ContactTypeId == x);
contact.ContactTypes.Add(selectedType);
}
context.Entry(contact).State = EntityState.Modified;
context.SaveChanges();
ViewBag.Message = "Save was successful.";
}
ViewData["ContactTypes"] = contact.ContactTypes.Select(t => t.ContactTypeId);
ViewData["ContactTypesAll"] = GetTypesList();
return View(contact);
}
I had to add a Detach method to my DBContext class as well (in CTP 5 this is not exposed):
public void Detach(object entity)
{
((System.Data.Entity.Infrastructure.IObjectContextAdapter)this).ObjectContext.Detach(entity);
}
精彩评论