I'm attempting to understand how to edit a One-to-Many relationship using MVC and Entity Framework and I am running into a few issues, I am attempting to edit the One (Person
) and the Many (Color
) on the same View (I will move onto Many-to-Many once this is complete).
I've reviewed many other posts and don't see a direct solution to what I think I am experiencing.
BaseObject
Class:
public class BaseObject
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
[Column(Order = 1)]
public Guid Oid { get; set; }
}
Person
Class:
public class Person : BaseObject
{
public string Name { get; set; }
public virtual List<Color> Colors { get; set; }
}
Color
Class:
public class Color : BaseObject
{
public string Name { get; set; }
[DisplayName("Person")]
public Guid? PersonID { get; set; }
[ForeignKey("PersonID")]
public virtual Person Person { get; set; }
}
After this I create my Scafolding (and Views), from there I have modified the Person/Edit.cshtml
to use a Model Binder
method starting with @for (var i = 0; i < Model.Colors.Count(); i++)
:
@model x.Models.One_to_Many.Person
@using x.Models.One_to_Many
@{
ViewBag.Title = "Edit Person and associated Colors";
}
<h2>Edit</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Person showing associated Colors</h4>
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
@Html.HiddenFor(model => model.Oid)
<div class="form-group">
@Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Colors, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@for(var i = 0; i < Model.Colors.Count(); i++)
{
@Html.HiddenFor(model => Model.Colors[i].Oid)
@Html.EditorFor(modelItem => Model.Colors[i].Name, new { htmlAttributes = new { @class = "form-control" } })
}
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
From there, I have the Person Controller (PeopleController
) basically untouched (I've added Colors
to the Bind > Include as a test):
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "Oid,Name,Colors")] Person person)
{
if (ModelState.IsValid)
{
db.Entry(person).State = EntityState.Modified; //<-- Error
db.SaveChanges();
return RedirectToAction("Index");
}
return View(person);
}
When I run the app, open an existing Person I see the Person Name and a list of the colors (Blue and Green). Modifying or not, when I click "Save", db.Entry(person).State = EntityState.Modified;
returns the following error:
System.InvalidOperationException: 'A referential integrity constraint violation occurred: The property value(s) of 'Person.Oid' on one end of a relationship do not match the property value(s) of 'Color.PersonID' on the other end.'
Inspecting at the Person
object, it does have 2 Colors (got past that using the Model Binder), going into the Colors
collection I see the Name
of the color has changed, the Person
object is populated, but the PersonID
is not.
person.Colors[0].Name "Blue updated" string
person.Colors[0].PersonID null System.Guid?
+person.Colors[0].Person {x.Models.One_to_Many.Person} x.Models.One_to_Many.Person
I believe that the PersonID
is my issue, it needs to be populated with the current Person
.
Do I need to Bind or Include in the Edit GET?
public ActionResult Edit(Guid? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Person person = db.One.Find(id);
if (person == null)
{
return HttpNotFound();
}
return View(person);
}
I'm not sure what the best approach is to having this set or setting it on Save.
Edit
This is based on @gert-arnold advice to go to a ViewModel, my plan was to move to that after I figure out the One-to-Many and future Many-to-Many.
Saving the Model wasn't working out too well, if you see below, I would be able to adapt the code to the PeopleController, but putting it into a ViewModel and Controller definitely made sense.
I have now created a PersonViewModel
:
public class PersonView
{
public string Name { get; set; }
public virtual List<Color> Colors { get; set; }
}
And an associated Controller; PersonViewController
:
public class PersonViewController : Controller
{
private ApplicationDBContext db = new ApplicationDBContext();
private Person person;
private Color color;
public ActionResult Edit(Guid id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
person = db.People.Find(id);
if (person == null)
{
return HttpNotFound();
}
//Setup ViewModel
PersonView personView = new PersonView();
personView.Name = person.Name;
personView.Colors = person.Colors;
return View(personView);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(Guid? id, PersonView personViewModel)
{
if (ModelState.IsValid)
{
//Person
person = db.People.Find(id);
person.Name = personViewModel.Name;
db.Entry(person).State = EntityState.Modified;
db.SaveChanges();
//Colors
foreach (Color item in personViewModel.Colors)
{
color = db.Colors.Find(item.Oid);
color.Name = item.Name;
//color.PersonID = id; //Is this neccessary?
db.Entry(color).State = EntityState.Modified;
db.SaveChanges();
}
return View(personViewModel); //return RedirectToAction("Index");
}
return View(personViewModel);
}
}
Finally, the Edit.cshtml
is very similar (if not the same) as above. I believe that I am getting the results that I am looking for and I can expand to allow new Color
s to be added inline with Ajax, etc.
I know that I need to check for and catch errors, but I feel this will suffice as a proof of concept. In short, does anyone have any advice on this code and approach, I am looking to improve it now and apply to my actual project.