Question: ASP.Net Core: How do I update (change/add/remove) nested item objects (One-to-Many relationship)?
I have an .Net 5.x project with "MSCustomers" and "MSLocations". c# error There's a many-to-one of MSLocations to MSCustomers.
My "Edit" page correctly displays an "MSCustomer" record and the corresponding c# error "MSLocations" fields.
PROBLEM:
"Edit" should allow me to modify or "remove" c# error any MSLocation. But when I save the record, none of the MSLocations are changed.
MSCustomer.cs:
public class MSCustomer { public int ID { get; set; } public string CustomerName { get; set; } public string EngagementType { get; set; } public string MSProjectNumber { get; set; } // EF Navigation public virtual ICollection<MSLocation> MSLocations { get; set; } }
MSLocation.cs
public class MSLocation { public int ID { get; set; } public string Address { get; set; } public string City { get; set; } public string State { get; set; } public string Zip { get; set; } public int MSCustomerId { get; set; } // FK // EF Navigation public MSCustomer MSCustomer { get; set; } }
Edit.cshtml.cs:
public class EditModel : PageModel { [BindProperty] public MSCustomer MSCustomer { get; set; } ... public IActionResult OnGet(int? id) { if (id == null) return NotFound(); MSCustomer = ctx.MSCustomer .Include(location => location.MSLocations) .FirstOrDefault(f => f.ID == id); return Page(); // This all works... } public async Task<IActionResult> OnPostAsync(string? submitButton) { ctx.Attach(MSCustomer).State = EntityState.Modified; await ctx.SaveChangesAsync(); return RedirectToPage("Index"); // Saves MSCustomer updates, but not MSLocations...
Edit.cshtml.cs
@page @model HelloNestedFields.Pages.MSFRD.EditModel @using HelloNestedFields.Models ... <form method="POST"> <input type="hidden" asp-for="MSCustomer.ID" /> ... <table> <thead> <tr> <th style="min-width:140px">Address</th> <th style="min-width:140px">City</th> <th style="min-width:140px">State</th> <th style="min-width:140px">Zip</th> </tr> </thead> <tbody> @foreach (MSLocation loc in @Model.MSCustomer.MSLocations) { <tr id="[email protected]"> <td><input asp-for="@loc.Address" /></td> <td><input asp-for="@loc.City" /></td> <td><input asp-for="@loc.State" /></td> <td><input asp-for="@loc.Zip" /></td> <td><button onclick="removeField(@loc.ID);">Remove</button></td> </tr> } <tr> <td></td> <td></td> <td></td> <td></td> <td><button id="add_location_btn">Add Location</button></td> </tr> </tbody> </table> ... @section Scripts { <script type="text/javascript"> function removeField(element_id) { try { let row_id = "row_" + element_id; console.log("removeField, element_id=" + element_id + ", row_id=" + row_id + "..."); let tr = document.getElementById(row_id); console.log("tr:", tr); tr.parentNode.removeChild(tr); } catch (e) { console.error(e); } debugger; }; </script> }
HelloNestedContext.cs
public class HelloNestedContext : DbContext { public HelloNestedContext(DbContextOptions<HelloNestedContext> options) : base(options) { } public DbSet<HelloNestedFields.Models.MSCustomer> MSCustomers { get; set; } public DbSet<HelloNestedFields.Models.MSLocation> MSLocations { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<MSCustomer>() .HasMany(d => d.MSLocations) .WithOne(c => c.MSCustomer) .HasForeignKey(d => d.MSCustomerId); } }
Q: What am I missing?
Q: What do I need to do so that MSCustomers.MSLocations updates are passed from the browser back to OnPostAsync(), and saved correctly?
I'm sure it's POSSIBLE. But I haven't been able to find any documentation or sample code anywhere for modifying "nested item objects" in a "record object".
Any suggestions would be very welcome!
Update:
Razor pages don't seem to support binding to a "complex" object c# error (with nested lists within a record).
So I tried Okan Karadag's excellent suggestion below - I split "MSLocations" into its own binding, then added it back to "MSCustomer" in the "POST" handler. This got me CLOSER - at least now I'm now able to update nested fields. But I'm still not able to add or remove MSLocations in my "Edit" page.
New Edit.cshtml.cs
[BindProperty] public MSCustomer MSCustomer { get; set; } [BindProperty] public List<MSLocation> MSLocations { get; set; } ... public IActionResult OnGet(int? id) { MSCustomer = ctx.MSCustomer .Include(location => location.MSLocations) .FirstOrDefault(f => f.ID == id); MSLocations = new List<MSLocation>(MSCustomer.MSLocations); ... public async Task<IActionResult> OnPostAsync(string? submitButton) { MSCustomer.MSLocations = new List<MSLocation>(MSLocations); // Update record with new ctx.Update(MSCustomer); await ctx.SaveChangesAsync(); ...
New Edit.cshtml
<div class="row"> ... <table> ... <tbody id="mslocations_tbody"> @for (int i=0; i < Model.MSLocations.Count(); i++) { <tr id="[email protected][i].ID"> <td><input asp-for="@Model.MSLocations[i].Address" /></td> <td><input asp-for="@Model.MSLocations[i].City" /></td> <td><input asp-for="@Model.MSLocations[i].State" /></td> <td><input asp-for="@Model.MSLocations[i].Zip" /></td> <td> <input type="hidden" asp-for="@Model.MSLocations[i].ID" /> <input type="hidden" asp-for="@Model.MSLocations[i].MSCustomerId" /> <button onclick="removeLocation([email protected][i].ID);">Remove</button> </td> </tr> } </tbody> </table> <button onclick="addLocation();">Add Location</button> </div>
Current Status:
- I can update top-level "MSCustomer" data fields OK.
- I can update existing "MSlocation" fields OK.
- I CANNOT add new or remove current MSLocation items.
- The "blocker" seems to be Razor bindings: communicating the "updated" MSLocations list from the Razor "Edit" page back to the C# "POST" action handler.
My next step will be to try this:
How to dynamically add items from different entities to lists in ASP.NET Core MVC
It would be great if it worked. It would be even BETTER if there were a simpler alternative that didn't involve Ajax calls..