Dynamic table for insert/edit/delete phone data into NewEdit Contact view

Pablo

Well-known member
Joined
Aug 12, 2021
Messages
82
Programming Experience
10+
Hello all,
I'm working on an MVC web application that consists of a Contact list with variable number of phone numbers. In what I'm stuck is in designing a dynamic table (as a part of the NewOrEdit view) that gives me the possibility of adding, editing, and deleting phone number objects. Supposedly I would need buttons and C# code for doing that. I tried to add C# code into @{ } to make something like a binding, because I have to know (in the C# code) the values I insert in the <input/>s I put into the <td> ... </td>. I tried but I could not, also many compiler errors appeared. By now I'm testing just Insert (or New), not Edit because it would be more difficult for you to help, but once Insert is fine, Edit will be very easy.
I give you the code I have by now, maybe you can help me to learn. I googled a lot but didn't find anything useful. I tried with javascript and ajax before but it didn't work.

1) The Contact Model
Contact Model:
namespace WebAppContacts.Models
{
    public class Contact
    {
        public int ContactId { get; set; }
        public string? FullName { get; set; }
        public string? EMail {  get; set; }
        public string? Address { get; set; }
        public List<Telephone> Telephones { get; set; } = new List<Telephone>();
        public bool IsActive { get; set; }
    }
}

2) The Telephone Model
Telephone Model:
namespace WebAppContacts.Models
{
    public class Telephone
    {
        public int TelephoneId { get; set; }
        public string? Description {  get; set; }
        public string? PhoneNumber { get; set; }
        public int ContactId {  get; set; }
    }
}

3) The NewOrEdit.cshtml View (where the big problem is - gives compiler errors)
NewOrEdit.cshtml View:
@model Contact;

@{
    public string phoneNumber = "", description = "";

    public void AddTelephone()
    {
        Telephone telephone = new Telephone();
        telephone.PhoneNumber = phoneNumber;
        telephone.Description = description;
        Model.Telephones.Add(telephone);
    }
}

<div class="row">
    <div class="col-sm-8 offset-sm-2">
        <h2> New/Edit Contact </h2>
        <form asp-controller="Contact" asp-action="NewOrEdit" method="post">
            <input asp-for="ContactId" type="hidden" />
            <div class="mb-3">
                <label>Full Name</label>
                <input class="form-control" asp-for="FullName" type="text" />
            </div>
            <div class="mb-3">
                <label>E-Mail</label>
                <input class="form-control" asp-for="EMail" type="text" />
            </div>
            <div class="mb-3">
                <label>Address</label>
                <input class="form-control" asp-for="Address" type="text" />
            </div>
            <div class="mb-3">
                <table class="table table-bordered">
                    <thead>
                        <tr>
                            <th>Description></th>
                            <th>Phone Number</th>
                            <th>Update</th>
                            <th>Delete</th>
                        </tr>
                    </thead>
                    <tbody>
                        @for (int i = 0; i < Model.Telephones.Count; i++)
                        {
                            <tr>
                                <td> <input type="text" asp-for="@Model.Telephones[i].PhoneNumber" /> </td>
                                <td> <input type="text" asp-for="@Model.Telephones[i].Description" /> </td>
                                <td> <input type="button" class="btn btn-secondary" value="Update" /> </td>
                                <td> <input type="button" class="btn btn-danger" value="Remove" onclick="Remove(this)" /> </td>
                            </tr>
                        }
                    </tbody>
                    <tfoot>
                        <tr>
                            <td><input type="text" value=@phoneNumber /></td>
                            <td><input type="text" value=@description /></td>
                            <td><input type="button" class="btn btn-outline-success" value="Agregar" onclick="AddTelephone"/></td>
                        </tr>
                    </tfoot>
                </table>
            </div>
            <div class="mb-3">
                <label>Is Active</label>
                <input class="form-control" asp-for="IsActive" type="text" />
            </div>
            <input type="submit" class="btn btn-outline-primary" value="Save contact" />
        </form>
    </div>
</div>

4) Part of the ContactController (I think you need just this to test, debugging, if the data has been sent to the Post Action Method, looking in the contact parameter, you don't need the DB either)
C#:
namespace WebAppContacts.Controllers
{
    public class ContactController : Controller
    {
        public ContactController()
        {
        
        }
      
        [HttpGet]
        public IActionResult NewOrEdit()
        {
            Contact contact = new Contact();
            return View(contact);
        }

        [HttpPost]
        public IActionResult NewOrEdit(Contact contact)
        {
            // contents was deleted for practicality
            // you can see, debugging, if data (especially the
            // Telephone List<>) has reached the parameter
        }
    }
}

I would appreciate your help very much. Please tell me if my explanation is not very clear.
Thank you
Pablo
Bs As Arg.
 
*** REMOVED UNNECESSARY QUOTE ***

The problem you're encountering stems from a fundamental misunderstanding of how ASP.NET Core MVC (Razor pages) interacts with client-side JavaScript. The C# code within the @ block of your .cshtml file runs on the server when the page is rendered and sent to the browser. It cannot directly react to user input or button clicks that happen on the client-side in the browser.

To create a dynamic table where you can add, edit, and delete rows without a full page refresh, you need to use JavaScript (or a JavaScript framework like jQuery, which is commonly used with ASP.
Here's a corrected and enhanced version of your NewOrEdit.cshtml view and an updated ContactController that demonstrates how to achieve this, along with explanations:

Update your NewOrEdit.cshtml View:

Update your NewOrEdit.cshtml View:
@model WebAppContacts.Models.Contact;

@{
    // Remove all C# variables and methods from this block.
    // This block is for server-side C# logic during view rendering,
    // not for client-side dynamic interactions.
    ViewData["Title"] = "New/Edit Contact"; // Add a title for better practice
}

<div class="row">
    <div class="col-sm-8 offset-sm-2">
        <h2> New/Edit Contact </h2>
        <form asp-controller="Contact" asp-action="NewOrEdit" method="post">
            <input asp-for="ContactId" type="hidden" />
            <div class="mb-3">
                <label asp-for="FullName"></label>
                <input class="form-control" asp-for="FullName" type="text" />
                <span asp-validation-for="FullName" class="text-danger"></span>
            </div>
            <div class="mb-3">
                <label asp-for="EMail"></label>
                <input class="form-control" asp-for="EMail" type="text" />
                <span asp-validation-for="EMail" class="text-danger"></span>
            </div>
            <div class="mb-3">
                <label asp-for="Address"></label>
                <input class="form-control" asp-for="Address" type="text" />
                <span asp-validation-for="Address" class="text-danger"></span>
            </div>
            <div class="mb-3">
                <label>Phone Numbers</label>
                <table class="table table-bordered" id="telephoneTable">
                    <thead>
                        <tr>
                            <th>Description</th>
                            <th>Phone Number</th>
                            <th>Actions</th>
                        </tr>
                    </thead>
                    <tbody id="telephoneTableBody">
                        @if (Model.Telephones != null)
                        {
                            for (int i = 0; i < Model.Telephones.Count; i++)
                            {
                                <tr data-index="@i">
                                    <td>
                                        <input type="hidden" name="Telephones[@i].TelephoneId" value="@Model.Telephones[i].TelephoneId" />
                                        <input type="text" name="Telephones[@i].Description" value="@Model.Telephones[i].Description" class="form-control" />
                                    </td>
                                    <td>
                                        <input type="text" name="Telephones[@i].PhoneNumber" value="@Model.Telephones[i].PhoneNumber" class="form-control" />
                                    </td>
                                    <td>
                                        <button type="button" class="btn btn-danger btn-sm remove-telephone-btn">Remove</button>
                                    </td>
                                </tr>
                            }
                        }
                    </tbody>
                    <tfoot>
                        <tr>
                            <td><input type="text" id="newDescription" class="form-control" placeholder="New Description" /></td>
                            <td><input type="text" id="newPhoneNumber" class="form-control" placeholder="New Phone Number" /></td>
                            <td><button type="button" class="btn btn-outline-success" id="addTelephoneBtn">Add</button></td>
                        </tr>
                    </tfoot>
                </table>
            </div>
            <div class="mb-3">
                <label asp-for="IsActive"></label>
                <input type="checkbox" asp-for="IsActive" class="form-check-input" />
                <span asp-validation-for="IsActive" class="text-danger"></span>
            </div>
            <button type="submit" class="btn btn-outline-primary">Save contact</button>
        </form>
    </div>
</div>

@section Scripts {
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script> @* Include jQuery if not already included in _Layout.cshtml *@
    <script>
        $(document).ready(function () {
            // Function to re-index all rows after add/remove
            function reIndexTelephoneRows() {
                $('#telephoneTableBody tr').each(function (index) {
                    $(this).attr('data-index', index); // Update data-index attribute for reference
                    $(this).find('input').each(function () {
                        var name = $(this).attr('name');
                        if (name) {
                            // Update name attribute for model binding (e.g., Telephones[0].Description -> Telephones[1].Description)
                            name = name.replace(/Telephones\[\d+\]/, 'Telephones[' + index + ']');
                            $(this).attr('name', name);
                        }
                    });
                });
            }

            // Add Telephone Button Click Handler
            $('#addTelephoneBtn').click(function () {
                var newDescription = $('#newDescription').val().trim();
                var newPhoneNumber = $('#newPhoneNumber').val().trim();

                if (newDescription === "" || newPhoneNumber === "") {
                    alert('Please enter both description and phone number to add.');
                    return; // Stop if inputs are empty
                }

                var currentIndex = $('#telephoneTableBody tr').length; // Get the current number of rows for the next index

                // Construct the new table row HTML
                var newRowHtml = '<tr data-index="' + currentIndex + '">' +
                                 '<td>' +
                                     '<input type="hidden" name="Telephones[' + currentIndex + '].TelephoneId" value="0" />' + // TelephoneId 0 for new records
                                     '<input type="text" name="Telephones[' + currentIndex + '].Description" value="' + newDescription + '" class="form-control" />' +
                                 '</td>' +
                                 '<td>' +
                                     '<input type="text" name="Telephones[' + currentIndex + '].PhoneNumber" value="' + newPhoneNumber + '" class="form-control" />' +
                                 '</td>' +
                                 '<td>' +
                                     '<button type="button" class="btn btn-danger btn-sm remove-telephone-btn">Remove</button>' +
                                 '</td>' +
                                 '</tr>';

                $('#telephoneTableBody').append(newRowHtml); // Append the new row to the table body

                // Clear the input fields for adding new numbers
                $('#newDescription').val('');
                $('#newPhoneNumber').val('');
            });

            // Remove Telephone Button Click Handler (using event delegation)
            // Event delegation is used because remove buttons are added dynamically.
            // It attaches the event listener to the static parent (#telephoneTableBody)
            // and only triggers the function when the click event originates from a '.remove-telephone-btn'.
            $('#telephoneTableBody').on('click', '.remove-telephone-btn', function () {
                $(this).closest('tr').remove(); // Remove the closest parent <tr>
                reIndexTelephoneRows(); // Re-index all remaining rows to maintain correct binding
            });
        });
    </script>
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} @* Include client-side validation scripts *@
}
Key Changes and Explanations in the View:

Removed Server-Side C# Logic:
The public string phoneNumber = "", description = ""; variables and the public void AddTelephone() { ... } method are completely removed from the @ block. These are server-side constructs and cannot be used for client-side dynamic behavior.
JavaScript for Dynamic Operations:
  • The addTelephoneBtn button (in <tfoot>) now has type="button" to prevent it from submitting the form. Its click event is handled by a jQuery function.
  • The remove-telephone-btn buttons (in <tbody>) also have type="button" and their clicks are handled by a jQuery function using event delegation.
Unique IDs for New Input Fields: The newDescription and newPhoneNumber inputs in the <tfoot> now have specific IDs (id="newDescription", id="newPhoneNumber") so that JavaScript can easily get their values.

name Attributes for Model Binding:
  • When existing phone numbers are rendered in the @for loop, asp-for correctly generates name attributes like Telephones[0].Description, Telephones[0].PhoneNumber, Telephones[1].Description, etc. This is crucial for MVC's model binder to recognize and populate the List<Telephone>.
  • When new rows are added via JavaScript, the JavaScript code dynamically constructs the name attributes using the Telephones[currentIndex].PropertyName format.
  • The reIndexTelephoneRows() function is paramount. When a row is removed, the indices of the subsequent rows must be updated. For example, if you have Telephones[0], Telephones[1], Telephones[2] and you remove Telephones[1], then Telephones[2] must be re-indexed to Telephones[1] so that the submitted list remains contiguous and correctly bound by MVC.
  • TelephoneId is included as a hidden input. For new phones added dynamically, TelephoneId is set to 0 (or default(int)). This helps your server-side code distinguish between new phones (Id 0) and existing phones (Id > 0) when saving.
jQuery Inclusion: Make sure you have jQuery included in your _Layout.cshtml file or directly in the NewOrEdit.cshtml file within the @section Scripts block (as shown). A CDN link like https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js is a common way to do this.
@section Scripts { ... }: This is the standard ASP.NET Core MVC way to include page-specific JavaScript. Ensure your _Layout.cshtml has @RenderSection("Scripts", required: false) just before the closing </body> tag.

Client-Side Validation: Added asp-validation-for and _ValidationScriptsPartial for basic client-side validation.

Update your ContactController:

The [HttpPost] NewOrEdit action method's signature and model binding capabilities will now correctly receive the Contact object with its Telephones list populated, thanks to the correct name attributes generated by JavaScript.

Controller Update:
using WebAppContacts.Models;
using Microsoft.AspNetCore.Mvc;
using System.Linq; // Required for Linq operations

namespace WebAppContacts.Controllers
{
    public class ContactController : Controller
    {
        // In a real application, you would inject your DbContext or a repository here
        // private readonly ApplicationDbContext _context;
        // public ContactController(ApplicationDbContext context)
        // {
        //     _context = context;
        // }

        [HttpGet]
        public IActionResult NewOrEdit(int? id) // Accept an optional ID for editing existing contacts
        {
            Contact contact;
            if (id.HasValue && id.Value > 0)
            {
                // In a real application, fetch the contact from your database
                // contact = _context.Contacts.Include(c => c.Telephones).FirstOrDefault(c => c.ContactId == id.Value);
                // if (contact == null)
                // {
                //     return NotFound();
                // }

                // For demonstration purposes, let's create a dummy existing contact
                contact = new Contact
                {
                    ContactId = id.Value,
                    FullName = "John Doe (Existing)",
                    EMail = "john.doe@example.com",
                    Address = "123 Main St",
                    IsActive = true,
                    Telephones = new List<Telephone>
                    {
                        new Telephone { TelephoneId = 101, Description = "Home", PhoneNumber = "555-1234" },
                        new Telephone { TelephoneId = 102, Description = "Work", PhoneNumber = "555-5678" }
                    }
                };
            }
            else
            {
                // New contact
                contact = new Contact();
            }
            return View(contact);
        }

        [HttpPost]
        [ValidateAntiForgeryToken] // Important for security: prevents Cross-Site Request Forgery
        public IActionResult NewOrEdit(Contact contact)
        {
            if (!ModelState.IsValid)
            {
                // If there are validation errors, return the view with the current model
                // This will display validation messages.
                return View(contact);
            }

            // At this point, the 'contact' object will contain the main contact details
            // AND the List<Telephone> populated by the model binder.

            Console.WriteLine($"--- Received Contact Data ---");
            Console.WriteLine($"ContactId: {contact.ContactId}");
            Console.WriteLine($"FullName: {contact.FullName}");
            Console.WriteLine($"EMail: {contact.EMail}");
            Console.WriteLine($"Address: {contact.Address}");
            Console.WriteLine($"IsActive: {contact.IsActive}");

            Console.WriteLine("Telephones:");
            if (contact.Telephones != null && contact.Telephones.Any())
            {
                foreach (var telephone in contact.Telephones)
                {
                    Console.WriteLine($"- TelephoneId: {telephone.TelephoneId}, Description: {telephone.Description}, PhoneNumber: {telephone.PhoneNumber}, ContactId (will be 0 if not set on client): {telephone.ContactId}");

                    // In a real application, you would process these telephone records:
                    if (telephone.TelephoneId == 0)
                    {
                        // This is a new telephone entry. Add it to the database.
                        // telephone.ContactId = contact.ContactId; // Link to parent contact if needed immediately
                        // _context.Telephones.Add(telephone);
                    }
                    else
                    {
                        // This is an existing telephone entry. Update it in the database.
                        // var existingTelephone = _context.Telephones.Find(telephone.TelephoneId);
                        // if (existingTelephone != null)
                        // {
                        //     existingTelephone.Description = telephone.Description;
                        //     existingTelephone.PhoneNumber = telephone.PhoneNumber;
                        //     _context.Telephones.Update(existingTelephone);
                        // }
                    }
                }

                // Important: Handling deleted phones
                // If this is an edit operation (contact.ContactId > 0), you need to identify
                // phones that existed in the database for this contact but were removed by the user
                // from the UI.
                // You would fetch the original phones from the DB for this contact ID.
                // var existingDbPhones = _context.Telephones.Where(t => t.ContactId == contact.ContactId).ToList();
                // var submittedPhoneIds = contact.Telephones.Where(t => t.TelephoneId > 0).Select(t => t.TelephoneId).ToList();
                // var phonesToDelete = existingDbPhones.Where(dbPhone => !submittedPhoneIds.Contains(dbPhone.TelephoneId)).ToList();
                // _context.Telephones.RemoveRange(phonesToDelete);
            }
            else
            {
                Console.WriteLine("- No telephones submitted or list is empty.");
                // If it's an edit operation and no telephones are submitted,
                // you might need to delete all existing phones for this contact in the DB.
                // if (contact.ContactId > 0)
                // {
                //     var existingDbPhones = _context.Telephones.Where(t => t.ContactId == contact.ContactId).ToList();
                //     _context.Telephones.RemoveRange(existingDbPhones);
                // }
            }

            // Example save logic:
            if (contact.ContactId == 0)
            {
                // Add new contact and its telephones (after setting ContactId on telephones)
                // _context.Contacts.Add(contact);
                // _context.SaveChanges(); // Save contact first to get its ID
                // foreach (var tel in contact.Telephones)
                // {
                //     tel.ContactId = contact.ContactId;
                //     _context.Telephones.Add(tel);
                // }
            }
            else
            {
                // Update existing contact and handle its telephones
                // _context.Contacts.Update(contact);
            }

            // _context.SaveChanges(); // Save all changes for phones (adds, updates, deletes)

            TempData["Message"] = "Contact saved successfully!";
            return RedirectToAction("Index", "Contact"); // Redirect to a contact list or success page
        }

        // Add an Index action for redirection after save
        public IActionResult Index()
        {
            // In a real application, this would display a list of contacts
            // For demonstration, it's just a placeholder view.
            return View();
        }
    }
}

Explanation of Controller Changes:

[HttpGet] NewOrEdit(int? id):
Modified to accept an optional id parameter. If an id is provided, it simulates fetching an existing contact with its associated phone numbers. This allows you to test the "edit" functionality (populating the table with existing data).

[HttpPost] NewOrEdit(Contact contact):
  • [ValidateAntiForgeryToken]: Essential for security. This attribute works in conjunction with the anti-forgery token that asp-controller and asp-action tag helpers automatically embed in your form.
  • if (!ModelState.IsValid): Always check ModelState.IsValid first. If there are validation errors (e.g., from data annotations in your Contact or Telephone models), this will return the view, displaying the validation messages.
  • Model Binding: MVC's default model binder will automatically populate the contact object, including its Telephones List, because the name attributes of your input fields adhere to the Telephones[index].PropertyName convention.
  • Server-Side Logic for Telephones:This is where you'd interact with your database:
    • New Phones: Iterate through contact.Telephones. If telephone.TelephoneId is 0, it means it's a new phone number added by the user, and you should insert it into your Telephones table.
    • Updated Phones: If telephone.TelephoneId is greater than 0, it's an existing phone number. You would fetch the original phone record from the database by its ID and update its Description and PhoneNumber properties.
    • Deleted Phones:This is the trickiest part. If you allow users to remove existing phone numbers, you need to:
      1. Retrieve the original list of phone numbers associated with the contact from your database before processing the submitted contact object.
      2. Compare the TelephoneIds in the submitted contact.Telephones list with the TelephoneIds from the original database list.
      3. Any TelephoneId present in the original database list but not in the submitted list means that phone number was removed by the user, and it should be deleted from your database.The commented-out code shows a conceptual approach for this.
How to Test This:

Ensure jQuery:
Make sure jQuery is loaded in your project (_Layout.cshtml is the common place for global scripts).
Run the Application:
Navigate to /Contact/NewOrEdit to add a new contact and its phones.
Navigate to /Contact/NewOrEdit/1 (or any ID) to simulate editing an existing contact and its phones.
Interact with the Table:
  • Add new phone numbers using the "Add" button.
  • Edit the values of existing or newly added phone numbers.
  • Remove phone numbers using the "Remove" button.
Submit the Form: Click "Save Contact".

Check Console Output: Observe the Console.WriteLine output in your Visual Studio's "Output" window (or where your application logs are displayed) to see how the Contact object and its Telephones list are populated by the MVC model binder. This will confirm if the dynamic table is correctly sending data to the server.
This approach provides a robust solution for managing dynamic lists within your MVC forms using a combination of server-side rendering and client-side JavaScript. This is untested but I'm certain the answers I've given you will provide a solution. :)
 
@Justin : There is no need to quote the past above yours.
 
Back
Top Bottom