You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

已创建含双模型的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

火山引擎 最新活动