0

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 Colors 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.

Derek
  • 653
  • 7
  • 20
  • The `Bind` attribute is OK as long as you don't need to specify nested object bindings. I wouldn't use it unless absolutely necessary. Use view models/DTOs to control access to your model's properties. Another thing is that `db.Entry(person).State = EntityState.Modified` doesn't mark nested objects as `Modified`, only primitive properties. So my advice: use PersonDto/ColorDto in the VC-part of MVC, without `[Bind]` and map them back to the real things in your controller or (preferably) a service. – Gert Arnold Jun 06 '17 at 19:41
  • Thank you @gert-arnold, I've updated my initial question with a ViewModel, this is working out well. – Derek Jun 07 '17 at 20:40

2 Answers2

0

Consider removing the PersonId column in your Color entity. Set up your mapping to resolve the "Person" key to a PersonId column, but avoid exposing both a parent reference and it's FK property in your child entity. This leads to all kinds of issues where parent references and FKs could fall out of sync. With both in place depending on how the mapping resolves, you'd need to do something like below with all of your reference properties:

private Person _person = null;
public virtual Person Person 
{
  get {return _person;}
  set 
  {
    _person = value;
    if (value != null)
      PersonID = value.OId;
    else
      PersonID = null;
  }
}

Disclaimer: I personally do not use code-first mapping. I elect to use DB first with EntityTypeConfiguration declarations as I use bounded contexts and prefer to optimize my schema without the ORM "tinkering" with it. With explicit configuration I can use .Map to resolve my FK's without exposing them as properties in my entities. I don't know if/how this is achievable with code-first.

As a general rule I avoid adding Parent entity references entirely unless I know for a fact I will need reverse resolution. For example: if I always go from Person to list Colors, I wouldn't even have a Person reference (or FK property) in my Color object. However in the case of something like Customers and Orders, I would justify having a Customer reference on my order as I'd expect to be searching through Orders and want to see what customer they were associated to.

Steve Py
  • 26,149
  • 3
  • 25
  • 43
0

Starting to use view models is a sensible choice. When doing that, there are multiple ways to "paint the state" back to the EF entities. Looking at Person, one of them is:

person = db.People.Find(id);
person.Name = personViewModel.Name;
db.SaveChanges();

By the Find statement, EF starts tracking the person entity. EF notices the name modification and will update just that. Setting the state to Modified isn't necessary, less optimal even, because it will cause EF to update all person's properties.

Another way to paint the state is:

person = new Person { Oid = personViewModel.Oid, Name = personViewModel.Name };
db.Persons.Attach(person);
db.Entry(person).Property(p => p.Name).IsModified = true;
db.SaveChanges();

This removes a database roundtrip, which may improve performance, but it takes more code, especially when nested objects are involved. Another major drawback is that person, a so-called stub entity, is now likely to fail validation because required properties may be null. This can be skirted around by disabling validation on save (db.Configuration.ValidateOnSaveEnabled = false), but then you need some other mechanism validation than the one EF provides.

More on how to choose between these alternatives here.

Gert Arnold
  • 105,341
  • 31
  • 202
  • 291