Alex
Alex

Reputation: 47

MVC5 / Entity | Update/Create Complex Model

I have created a complex model and been struggling for a while to find a way to add/update the form data to the database.

Basically my model looks like the one below :

namespace Models
{
    public class ModelClass1
    {
        public int Id { get; set; }
        public string Prop1 { get; set; }
        public string Prop2 { get; set; }

        public ICollection<ModelClass2> ModelClass2 { get; set; }
    }
    public class ModelClass2
    {
        public int Id { get; set; }
        public string Prop3 { get; set; }
        public string Prop4 { get; set; }

        public ICollection<ModelClass3> ModelClass3 { get; set; }
    }

    public class ModelClass3
    {
        public int Id { get; set; }
        public string Prop5 { get; set; }
        public string Prop6 { get; set; }
    }
}

I have created a scaffold CRUD controller for ModelClass1 and trying to update all 3 entities using one Edit class.

My Edit (get/post) classes :

// GET
public ActionResult Edit(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }

    ModelClass1 ModelClass1 = db.ModelClass1
        .Include(i => i.ModelClass2.Select(c => c.ModelClass3))
        .Where(x => x.Id == id)
        .Single();

    if (config == null)
    {
        return HttpNotFound();
    }
    return View(ModelClass1);
}


// POST
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(ModelClass1 ModelClass1)
{
    if (ModelState.IsValid)
    {
        db.Entry(ModelClass1).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(ModelClass1);
}

I have chosen to use Editor templates to populate the forms within the Edit view so the Edi.cshtml will be like :

@model ModelClass1

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    <div class="form-horizontal">
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        @Html.HiddenFor(model => model.Id)

        <div class="form-group">
            @Html.LabelFor(model => model.Prop1, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Prop1, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Prop1, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Prop2, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Prop2, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Prop2, "", new { @class = "text-danger" })
            </div>
        </div>

        @Html.EditorFor(model => model.ModelClass2, new { htmlAttributes = new { @class = "form-control" } })

        @Html.EditorFor(model => model.ModelClass2.FirstOrDefault().ModelClass3, new { htmlAttributes = new { @class = "form-control" } })

    </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>
}

And created templates for each of my 3 model classes similar to the one below :

@model ModelClass2
<div class="form-group">
    @Html.LabelFor(model => model.Prop3, htmlAttributes: new { @class = "control-label col-md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.Prop3, new { htmlAttributes = new { @class = "form-control" } })
        @Html.ValidationMessageFor(model => model.Prop3, "", new { @class = "text-danger" })
    </div>
</div>

<div class="form-group">
    @Html.LabelFor(model => model.Prop4, htmlAttributes: new { @class = "control-label col-md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.Prop4, new { htmlAttributes = new { @class = "form-control" } })
        @Html.ValidationMessageFor(model => model.Prop4, "", new { @class = "text-danger" })
    </div>
</div>

The data is being shown correctly then I'm calling the Edit controller but when I modify the data within the forms and post it calling the HTTPPost Edit method I receive the below error :

Attaching an entity of type 'ModelClass3' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate.

If I totally remove the ModelClass3 from the Controller / View I don't receive any errors but the modified data is not being saved to the database.

I'm pretty sure that my approach is not the best one so any help will be much appreciated as I'm running out of ideas.

Upvotes: 1

Views: 132

Answers (1)

Rodrigo Riskalla Leal
Rodrigo Riskalla Leal

Reputation: 515

First of All, I wouldn't recomend working with your domain entities directly on the Front End. I would create a View Model (VM) for your domain entities. Any way,

This error you are reporting is probably due to the entity not being attached to the Context. So you should attach it before setting the state to Modified:

// POST
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(ModelClass1 ModelClass1)
{
    if (ModelState.IsValid)
    {
        db.Attach(ModelClass1);
        db.Entry(ModelClass1).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(ModelClass1);
}

However I would prefer first fetching the entity from the DB and then updating the properties:

// POST
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(ModelClass1 ModelClass1)
{
    if (ModelState.IsValid)
    {
        ModelClass1 entityFromDB = db.ModelClass1
           .Include(i => i.ModelClass2.Select(c => c.ModelClass3))
           .Where(x => x.Id == id)
           .Single();
        MapProperties(entityFromDB, ModelClass1); // You could use a mapper (Auto Mapper)
        db.Entry(ModelClass1).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(ModelClass1);
}

Upvotes: 1

Related Questions