Nested Collection Models in MVC to Add Multiple Phone Numbers - Part 3

This is Part 3 of the article series. Actually, in this article series we were developing an MVC application that will allow adding multiple phone numbers using Nested Model concept. User will be able to add or remove (zero or more phone numbers, I need minimum two phone numbers for each employee) phone numbers for any single employee.

Previous logs:


Now, in this post you will learn how to list, how to edit, how to delete the employees. As we are working with nested models we need to follow quite different appraoch coz for each employee in Employee table we have zero or more phone numbers in Phone table associated with one to many relationships.

In the image given below you can see for each employee we have different set of phone numbers.


Okay, let’s create this one, just follow the steps.

Step 8: Listing Records

At very first, create an action method inside Employee controller class, here is the code.

:::::::::::
CompanyEntities db = new CompanyEntities();

public ActionResult List()
{
    return View(db.Employees.ToList());
}
:::::::::::

This code will return the list of employees to the view page. Add the view page for this action method by using following codes which loops through the collection of employees.

List.cshtml

@model IEnumerable<NestedModelsMvc.Models.Employee>

@{
    ViewBag.Title = "List";
}

<h2>Employee List</h2>

<p>
    @Html.ActionLink("Create New", "New")
</p>
<table>
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Name)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Salary)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Phones)
        </th>
        <th></th>
    </tr>
@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.Name)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Salary)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Phones)
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.EmployeeId }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.EmployeeId })
        </td>
    </tr>
}
</table>

Now, again in above code you can see, I am using modelItem => item.Phones which is nothing but an ICollection.


This will again look at the type of model, then go looking for available display templates in DisplayTemplates folder for a file named Phone.cshtml that we don’t have. Let’s add this too.


In Phone.cshtml file use following quick code.

Phone.cshtml

@model NestedModelsMvc.Models.Phone

@Html.DisplayFor(x => x.PhoneNumber)

And that’s it. Run the application it whould work now.

In List.cshtml view page, we have Edit and Delete links that we don’t have impletement yet, let’s implement.

Step 9: Editing Record

To allow editing, create two action methods one for GET and another for POST reqests inside Employee controller class, here is the code.

// :::::::::::
public ActionResult Edit(int id = 0)
{
    Employee employee = db.Employees.Find(id);
    if (employee == null)
    {
        return HttpNotFound();
    }
    return View(employee);
}
       
[HttpPost]
public ActionResult Edit(Employee employee)
{
    if (ModelState.IsValid)
    {
        db.Entry(employee).State = EntityState.Modified;
        db.SaveChanges();

        foreach (var item in employee.Phones)
        {
            db.Entry(item).State = EntityState.Modified;
            db.SaveChanges();
        }

        return RedirectToAction("List");
    }
    return View(employee);
}
// :::::::::::

GET version of the action method will look for passed url parameter (id) and then depending upon passed id an employee and its associated phone numbers will be returned to the view page. Now, add a new view page for this action method and use following code.

Edit.cshtml

@model NestedModelsMvc.Models.Employee

@{
    ViewBag.Title = "Edit";
}

<h2>Edit</h2>

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>Department</legend>

        @Html.HiddenFor(model => model.EmployeeId)

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

        <div class="editor-label">
            @Html.LabelFor(model => model.Salary)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Salary)
            @Html.ValidationMessageFor(model => model.Salary)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Phones)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Phones)
            @Html.ValidationMessageFor(model => model.Phones)
        </div>

        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
}
<div>
    @Html.ActionLink("Back to List", "List")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Now, again in above code you can see, I am using model => item.Phones which is nothing but an ICollection. This will again look at the type of model, then go looking for available editor templates in EditorTemplates folder for a file named Phone.cshtml that we already created in previous post. I added two new line (highligted in the code below) of codes to match the requirement, if you don’t do, you will experience following error.

A referential integrity constraint violation occurred: The property values that define the referential constraints are not consistent between principal and dependent objects in the relationship.

Phone.cshtml

@model NestedModelsMvc.Models.Phone
@using NestedModelsMvc.Models

<div class="phoneNumber">
  <p>
    <label>Phone Number</label>
      @Html.HiddenFor(model => model.PhoneId)
      @Html.HiddenFor(model => model.EmployeeId)

    @Html.TextBoxFor(x => x.PhoneNumber)
    @Html.HiddenFor(x => x.DeletePhone, new { @class = "mark-for-delete" })
    @Html.RemoveLink("Remove", "div.phoneNumber", "input.mark-for-delete")
  </p>
</div>

Look at the codes used in the POST version of the action method. This code is also quite different from normal update code.

Here you can run the application and test editing, it should work now.


Step 10: Deleting Record

To allow deleting, create two action methods one for GET and another for POST reqests inside Employee controller class, here is the code.

// ::::::::::::
public ActionResult Delete(int id = 0)
{
    Employee employee = db.Employees.Find(id);
    if (employee == null)
    {
        return HttpNotFound();
    }
    return View(employee);
}

[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id)
{
    Employee employee = db.Employees.Find(id);
    var phones = db.Phones.Where(i => i.EmployeeId == employee.EmployeeId);
    DeleteChilds(phones);
    db.Employees.Remove(employee);
    db.SaveChanges();
    return RedirectToAction("List");
}

private void DeleteChilds(IEnumerable<Phone> phones)
{
    foreach (var item in phones)
    {
        db.Phones.Remove(item);
    }
}
// ::::::::::::

GET version of the action method will look for passed url parameter (id) and then depending upon passed id an employee and its associated phone numbers will be returned to the view page asking ‘Are you sure you want to delete this?’.

Let’s add this view page and use following code.

Delete.cshtml

@model NestedModelsMvc.Models.Employee

@{
    ViewBag.Title = "Delete";
}

<h2>Delete</h2>

<h3>Are you sure you want to delete this?</h3>
<fieldset>
    <legend>Employee</legend>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Name)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Name)
    </div>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Salary)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Salary)
    </div>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Phones)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Phones)
    </div>
</fieldset>
@using (Html.BeginForm()) {
    <p>
        <input type="submit" value="Delete" /> |
        @Html.ActionLink("Back to List", "List")
    </p>
}

When user will hit Delete button POST version of the action method will be called. If you look at the POST version of the code, you will find an method DeleteChilds is being used. As you know each employee records has related recores in Phone table with one to many relationship, so deleteing such records will have quite different approach, you can check the code above. If you use normal deleting code, you will get following error.

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 you can run the application and test deleting record, it should work now.


That’s all. Ending the series here.

Source code: https://github.com/itorian/NestedCollectionMVC

Hope this helps.

Comments

  1. Wondering how you would go about handling nested nested collections. For example using your Employee Profile you could add contact people where they could have 0 to many contacts and each contact could have say 1 to many phone numbers.

    ReplyDelete
  2. It's nice but how do you add a new phone number while editing an employee? Editing current phone numbers works fine but there is a problem if you try to add a new phone number for an existing employee.

    ReplyDelete
    Replies
    1. I have the same problem with the edit mode when i add a new phone it doesn't post ...?
      Do you find how to do this, please. Thanks very much :)

      Delete
    2. You can do this in a couple ways. The way I handled it was by modifying the HTML helper for addlink. I added an extra optional property for employee id. If an employee ID is present, it runs a replace on the value of the employeeID field, and inserts the new employee ID.

      From there, you can modify your edit method inside your controller to be similar to the following:

      [HttpPost]
      [ValidateAntiForgeryToken]
      public ActionResult Edit(employees1 employees1)
      {
      if (ModelState.IsValid)
      {
      foreach (var item in employee.phone)
      {
      if (item.deletedPhone)
      {
      db.Entry(item).State = EntityState.Deleted;
      }
      else if (item.phoneId == 0)
      {
      db.Entry(item).State = EntityState.Added;
      }
      else
      {
      db.Entry(item).State = EntityState.Modified;
      }

      db.SaveChanges();
      }

      db.Entry(employee).State = EntityState.Modified;
      db.SaveChanges();

      return RedirectToAction("Index");
      }

      Delete
    3. To get it to work on the edit page, modify the addLink HTML Helper to have an optional ID field. If the ID field is present, then you will replace the employeeID value field within the hidden field. From there, you need to modify your edit method inside the employee controller to something similar to:

      [HttpPost]
      [ValidateAntiForgeryToken]
      public ActionResult Edit(employees1 employees1)
      {
      if (ModelState.IsValid)
      {
      foreach (var item in employee.Phones)
      {
      if (item.DeletePhone)
      {
      db.Entry(item).State = EntityState.Deleted;
      }
      else if (item.PhoneId == 0)
      {
      db.Entry(item).State = EntityState.Added;
      }
      else
      {
      db.Entry(item).State = EntityState.Modified;
      }

      db.SaveChanges();
      }

      db.Entry(employee).State = EntityState.Modified;
      db.SaveChanges();

      return RedirectToAction("Index");
      }

      Delete
    4. Dear Jdsfighter,

      Could you pls help me with the modification of the addlink HTML helper. I am really blank :(

      Delete
    5. Dear Jdsfighter,

      Could you pls give me the modified addlink HTML Helper as well? Because I have no idea at all about it :(

      Delete
    6. Dear jds fighter, pls post your modified code for the addlink helper

      Delete
    7. any luck with adding a new phone no while editing, after passing the employeeID how to create a new instance for the employee.phones

      Delete
    8. Has anyone figured out the modified code for addlink helper?

      Delete
  3. For adding a new Phone number to an Employee which has none, simply pass in the EmployeeId into the HtmlHelper AddLink method (pass in 0 when doing a Create) then use this

    object nestedObject;
    if (identifier > 0)
    {
    nestedObject = Activator.CreateInstance(nestedType, identifier);
    }
    else
    {
    nestedObject = Activator.CreateInstance(nestedType);
    }

    Finally, add a new constructor to the Phone class which takes in an EmployeeId. This then sets the EmployeeId property of the class and now it will be ready to save

    ReplyDelete
  4. how to add new phone number when we editing record

    ReplyDelete
  5. i have a dropdownlist instead of the phone textbox how could i update the DeletePhone where the select tag doesnt have a value property only the option have and its already used for displaying options

    any help?

    ReplyDelete
  6. Hello , If we begin with one phone number instead of two add phone number helper doesn't work! Any ideas?

    ReplyDelete
  7. Hello , If we begin with one phone number instead of two add phone number helper doesn't work! Any ideas?

    ReplyDelete
  8. Is there any update on the revised html helper that is needed to add phone numbers when editing employees? I followed the tutorial and got stuck here as well.

    ReplyDelete

Post a Comment

Popular posts from this blog

Migrating database from ASP.NET Identity to ASP.NET Core Identity

Customize User's Profile in ASP.NET Identity System