已创建含双模型的ViewModel,如何实现视图到模型的数据绑定?
实现视图到ViewModel的数据绑定(含EF Core保存逻辑)
嘿,我来帮你搞定视图和ViewModel之间的数据绑定问题!基于你提供的代码,我会一步步展示如何实现双向绑定,包括单个客户信息和地址列表的绑定,还有后续结合EF Core的保存逻辑。
1. 先优化你的模型(关键!)
首先注意到你的addressdetails模型没有和customerdetails关联的外键,这会导致保存地址时无法关联到对应的客户。我先给你调整下模型,同时顺便规范下C#的命名(大驼峰更符合规范):
namespace customer2.Models { public class CustomerDetails { [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { set; get; } [Key] public int CustomerId { set; get; } [Required(ErrorMessage = "客户名称不能为空")] public string CustomerName { set; get; } // 导航属性:关联地址列表 public virtual List<AddressDetails> Addresses { get; set; } } public class AddressDetails { [Key] public int AddressNo { set; get; } // 添加外键关联客户 public int CustomerId { set; get; } [Required(ErrorMessage = "街道不能为空")] public string Street { set; get; } public string Landmark { set; get; } [Required(ErrorMessage = "邮编不能为空")] public int Pincode { set; get; } // 导航属性:关联客户 [ForeignKey("CustomerId")] public virtual CustomerDetails Customer { get; set; } } public class MkContext : DbContext { public virtual DbSet<CustomerDetails> Customers { get; set; } public virtual DbSet<AddressDetails> Addresses { get; set; } // 记得配置DbContext的连接字符串,比如在OnConfiguring方法里 protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { optionsBuilder.UseSqlServer("你的数据库连接字符串"); } } } public class CustomerViewModel { public CustomerDetails Cd { set; get; } public List<AddressDetails> Ad { set; get; } } }
2. 编写控制器Action
接下来创建控制器,负责加载视图数据和处理表单提交:
using customer2.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Linq; namespace customer2.Controllers { public class CustomerController : Controller { private readonly MkContext _context; public CustomerController(MkContext context) { _context = context; } // 新增/编辑客户的页面 public IActionResult CreateOrEdit(int? customerId) { var viewModel = new CustomerViewModel(); if (customerId.HasValue) { // 编辑场景:从数据库加载现有客户和地址 viewModel.Cd = _context.Customers .Include(c => c.Addresses) .FirstOrDefault(c => c.CustomerId == customerId.Value); viewModel.Ad = viewModel.Cd.Addresses; } else { // 新增场景:初始化空对象和至少一个空地址行 viewModel.Cd = new CustomerDetails(); viewModel.Ad = new List<AddressDetails> { new AddressDetails() }; } return View(viewModel); } // 处理表单提交 [HttpPost] [ValidateAntiForgeryToken] public IActionResult CreateOrEdit(CustomerViewModel viewModel) { if (ModelState.IsValid) { // 处理客户信息 if (viewModel.Cd.CustomerId == 0) { // 新增客户 _context.Customers.Add(viewModel.Cd); } else { // 编辑客户:标记为修改状态 _context.Entry(viewModel.Cd).State = EntityState.Modified; } _context.SaveChanges(); // 先保存客户,获取生成的CustomerId // 处理地址列表 foreach (var address in viewModel.Ad) { address.CustomerId = viewModel.Cd.CustomerId; // 关联客户ID if (address.AddressNo == 0) { // 新增地址 _context.Addresses.Add(address); } else { // 编辑地址 _context.Entry(address).State = EntityState.Modified; } } _context.SaveChanges(); return RedirectToAction(nameof(Index)); // 跳转到客户列表页 } // 如果验证失败,返回原视图并保留输入 return View(viewModel); } } }
3. 编写Razor视图(强类型绑定)
创建对应视图,使用asp-for标签助手自动完成数据绑定,同时支持动态增减地址:
@model customer2.Models.CustomerViewModel @{ ViewData["Title"] = "客户信息管理"; } <h1>@(Model.Cd.CustomerId == 0 ? "新增客户" : "编辑客户")</h1> <form asp-action="CreateOrEdit"> <div asp-validation-summary="All" class="text-danger mb-3"></div> <!-- 客户基本信息 --> <div class="card mb-3"> <div class="card-header">客户基本信息</div> <div class="card-body"> @if (Model.Cd.CustomerId != 0) { <input type="hidden" asp-for="Cd.CustomerId" /> } <div class="form-group"> <label asp-for="Cd.CustomerName" class="control-label"></label> <input asp-for="Cd.CustomerName" class="form-control" /> <span asp-validation-for="Cd.CustomerName" class="text-danger"></span> </div> </div> </div> <!-- 地址列表 --> <div class="card mb-3"> <div class="card-header">客户地址 <button type="button" class="btn btn-sm btn-primary float-right" id="addAddress">添加新地址</button> </div> <div class="card-body" id="addressContainer"> @for (int i = 0; i < Model.Ad.Count; i++) { <div class="address-row border p-3 mb-3"> <input type="hidden" asp-for="Ad[i].AddressNo" /> <input type="hidden" asp-for="Ad[i].CustomerId" /> <div class="form-row"> <div class="form-group col-md-4"> <label asp-for="Ad[i].Street" class="control-label"></label> <input asp-for="Ad[i].Street" class="form-control" /> <span asp-validation-for="Ad[i].Street" class="text-danger"></span> </div> <div class="form-group col-md-4"> <label asp-for="Ad[i].Landmark" class="control-label"></label> <input asp-for="Ad[i].Landmark" class="form-control" /> <span asp-validation-for="Ad[i].Landmark" class="text-danger"></span> </div> <div class="form-group col-md-3"> <label asp-for="Ad[i].Pincode" class="control-label"></label> <input asp-for="Ad[i].Pincode" class="form-control" /> <span asp-validation-for="Ad[i].Pincode" class="text-danger"></span> </div> <div class="form-group col-md-1 align-self-end"> @if (i > 0) { <button type="button" class="btn btn-danger btn-block remove-address">删除</button> } </div> </div> </div> } </div> </div> <button type="submit" class="btn btn-success">保存</button> <a asp-action="Index" class="btn btn-secondary ml-2">返回列表</a> </form> @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} <script> $(document).ready(function() { // 动态添加地址行 $("#addAddress").click(function() { const lastIndex = $(".address-row").length; const customerId = @Model.Cd.CustomerId; const newRow = ` <div class="address-row border p-3 mb-3"> <input type="hidden" name="Ad[${lastIndex}].AddressNo" value="0" /> <input type="hidden" name="Ad[${lastIndex}].CustomerId" value="${customerId}" /> <div class="form-row"> <div class="form-group col-md-4"> <label class="control-label">街道</label> <input name="Ad[${lastIndex}].Street" class="form-control" /> <span class="text-danger field-validation-valid" data-valmsg-for="Ad[${lastIndex}].Street" data-valmsg-replace="true"></span> </div> <div class="form-group col-md-4"> <label class="control-label">地标</label> <input name="Ad[${lastIndex}].Landmark" class="form-control" /> </div> <div class="form-group col-md-3"> <label class="control-label">邮编</label> <input name="Ad[${lastIndex}].Pincode" class="form-control" /> <span class="text-danger field-validation-valid" data-valmsg-for="Ad[${lastIndex}].Pincode" data-valmsg-replace="true"></span> </div> <div class="form-group col-md-1 align-self-end"> <button type="button" class="btn btn-danger btn-block remove-address">删除</button> </div> </div> </div> `; $("#addressContainer").append(newRow); }); // 删除地址行并重新索引 $(document).on("click", ".remove-address", function() { $(this).closest(".address-row").remove(); // 重新调整所有地址行的name索引,确保绑定正确 $(".address-row").each(function(index) { $(this).find("input[name^='Ad[']").each(function() { const newName = $(this).attr("name").replace(/Ad\[\d+\]/, `Ad[${index}]`); $(this).attr("name", newName); // 同步验证提示的data-valmsg-for属性 const validationSpan = $(this).next(".text-danger"); if (validationSpan.length > 0) { validationSpan.attr("data-valmsg-for", newName); } }); }); }); }); </script> }
关键绑定逻辑说明
- 单个模型绑定:通过
asp-for="Cd.CustomerName"生成的表单name属性为Cd.CustomerName,ASP.NET Core会自动将表单值映射到ViewModel的Cd对象的CustomerName属性。 - 列表绑定:使用
for循环遍历地址列表,生成Ad[i].Street格式的name属性,框架会自动将这些值收集到ViewModel的Ad列表中。 - 动态列表处理:JS部分负责新增/删除地址行,同时更新name属性的索引,确保提交时框架能正确识别列表项。
内容的提问来源于stack exchange,提问作者manikumar




