nacho10f
nacho10f

Reputation: 5886

Help with asp.net mvc nested model binding

So I have this demo project almost completely working.

public class Project
    {
        public int ID { get; set; }
        [Required]
        public string Name { get; set; }
        public virtual ICollection<Task> Tasks { get; set; }

    }

    public class Task
    {
        public int ID { get; set; }
        [Required]
        public string Name { get; set; }
        public int ProjectID { get; set; }

        public virtual Project Project { get; set; }

    }

Controller

public ActionResult Edit(int id)
        {           
            var project = db.Projects.Where(p=>p.ID==id).Single();
            return View(project);
        }

        [HttpPost]
        public ActionResult Edit(Project project)
        {
            if (ModelState.IsValid)
            {
                var dbProject = db.Projects.Where(p => p.ID == project.ID).Single();

                UpdateModel(dbProject);
                db.SaveChanges();                
                TempData["Success"] = "Modelo Valido";
            }
            return RedirectToAction("Index");
        }

View//strongly typed for project

@using (Html.BeginForm())
{
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>Project</legend>
        @Html.HiddenFor(model => model.ID)
        <div class="editor-label">
            @Html.LabelFor(model => model.Name)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Name)
            @Html.ValidationMessageFor(model => model.Name)
        </div>
        <h1>Tasks</h1>
        @Html.EditorFor(m => m.Tasks)
        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
}

EditorTemplate

@model MvcApplication2.Models.Task
<span>Task</span>
<br />
    @Html.LabelFor(m => m.Name)
    @Html.EditorFor(m => m.Name)
    @Html.HiddenFor(m => m.ID)
    @Html.HiddenFor(m => m.ProjectID)
    @Html.ValidationMessageFor(m => m.Name)

The view displays this

enter image description here

The problem is that when I submit the form the Tasks are populated with everything except the virtual Project property... so the error i get it is

The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.

Here is a pic of my debugging breakpoint result

enter image description here

Please Help.

UPDATE:

I have changed my controller action to this

[HttpPost]
        public ActionResult Edit(Project project)
        {
            if (ModelState.IsValid)
            {
                db.Entry(project).State = EntityState.Modified;
                db.SaveChanges();                
                TempData["Success"] = "Modelo Valido";
                return RedirectToAction("Index");
            }
            return View(project);
        }

it is still not working correctly. Now changes made to the Name of the project are updated correctly in the database. but changes made to any Task Name are ignored completely.

Upvotes: 4

Views: 5640

Answers (2)

nacho10f
nacho10f

Reputation: 5886

I have found a way to get this to work but Im not completely happy with the approach.

see this question on how to refactor my current code to see how I am currently (hopefully temporarily doing it)

Help improving (refactoring) my code. Automapper - EF - asp.net mvc-3

Upvotes: 1

Muhammad Adeel Zahid
Muhammad Adeel Zahid

Reputation: 17784

i believe @Html.EditorFor(m => m.Tasks) is generating html like (approximately)

<label>Name</label>
<input type="text" name="Tasks[0].Name" id="auto-gen-id"/>
<input type="hidden" name="Tasks[0].ID" id = "auto-gen-id"/>
<input type="hidden" name="Tasks[0].ProjectID" id = "auto-gen-id"/>
<!--html for validation span-->

Above is the approximate html generated for first Task in Collection and similar html will be generated for each task in the collection. The only difference is that index will be incremented in name attributes of all inputs i.e Tasks[1].Name, Tasks[1].ProjectID etc. This portion will actually bind to the Collection<Task> Tasks property of Project but you can see that in detail portion you don't have any inputs like

<input type="whatever" name="Tasks[0].Project.ProjectID" .../>
<input type="whatever" name="Tasks[0].Project.Name" ..../>

Modelbinder needs input elements with proper naming conventions to bind values to all properties of action method parameters. For testing purpose you can inlude these two lines in your Editor template for Task

@Html.TextBoxFor(x=>x.Project.ID)
@Html.TextBoxFor(x=>x.Project.Name)

input proper values for them in the form and you will have Project property of Task populated with these values. But may not be what you desire i.e entering project information twice and this may not be needed (if u are using Linq to sql its sure not needed). When you call your ORM for attaching entities to db entities it will know which Project elements, current Task belongs to.
Side Note: When you have problems with modelbinding, always pay attention to generated html. Generated html will dictate which form values will map to which properties of the model as long as you are using default modelbinder. it becomes especially important if you are having master detail kind of scenario as in your example.

Upvotes: 4

Related Questions