ABP Framework MVC Razor自定义CRUD UI:复杂表单与本地化问题
ABP Framework 复杂父子表单(Order-OrderDetails)实现指南
一、核心:Order-OrderDetails 父子表单实现
1. 实体层(遵循ABP聚合根规范)
// Order 聚合根(封装业务规则,确保数据一致性) public class Order : AggregateRoot<Guid> { public string Customer { get; set; } public DateTime OrderDate { get; set; } public string Currency { get; set; } public List<OrderDetail> OrderDetails { get; set; } = new List<OrderDetail>(); // ABP聚合根要求私有无参构造函数 private Order() {} public Order(Guid id, string customer, DateTime orderDate, string currency) : base(id) { Customer = customer; OrderDate = orderDate; Currency = currency; } // 封装添加明细的业务逻辑 public void AddDetail(string stockCode, decimal price, int quantity) { OrderDetails.Add(new OrderDetail(Guid.NewGuid(), stockCode, price, quantity)); } } // OrderDetail 子实体 public class OrderDetail : Entity<Guid> { public string StockCode { get; set; } public decimal Price { get; set; } public int Quantity { get; set; } // 外键关联聚合根 public Guid OrderId { get; set; } public Order Order { get; set; } private OrderDetail() {} public OrderDetail(Guid id, string stockCode, decimal price, int quantity) : base(id) { StockCode = stockCode; Price = price; Quantity = quantity; } }
2. DTO层(适配UI与验证)
public class OrderCreateDto { [Required] [Display(Name = "Customer")] public string Customer { get; set; } [Required] [Display(Name = "OrderDate")] public DateTime OrderDate { get; set; } [Required] [Display(Name = "Currency")] public string Currency { get; set; } [Required] public List<OrderDetailCreateDto> OrderDetails { get; set; } = new List<OrderDetailCreateDto>(); } public class OrderDetailCreateDto { [Required] [Display(Name = "StockCode")] public string StockCode { get; set; } [Required] [Range(0.01, double.MaxValue)] [Display(Name = "Price")] public decimal Price { get; set; } [Required] [Range(1, int.MaxValue)] [Display(Name = "Quantity")] public int Quantity { get; set; } }
3. UI层(动态表单+多行明细录入)
<abp-dynamic-form abp-model="OrderCreateDto" asp-page="/Orders/CreateModal"> <abp-modal class="custom-order-modal"> <abp-modal-header title="@L["NewOrder"].Value"></abp-modal-header> <abp-modal-body> <!-- 主订单动态表单 --> <abp-form-content /> <!-- 订单明细多行录入区域 --> <div class="mt-4"> <h5>@L["OrderDetails"].Value</h5> <table class="table table-bordered"> <thead> <tr> <th>@L["StockCode"].Value</th> <th>@L["Price"].Value</th> <th>@L["Quantity"].Value</th> <th>@L["Actions"].Value</th> </tr> </thead> <tbody id="order-details-body"> <!-- 初始空行 --> <tr class="detail-row"> <td><input type="text" class="form-control" name="OrderDetails[0].StockCode" required /></td> <td><input type="number" step="0.01" class="form-control" name="OrderDetails[0].Price" required /></td> <td><input type="number" class="form-control" name="OrderDetails[0].Quantity" required /></td> <td><button type="button" class="btn btn-danger btn-sm remove-row">@L["Delete"].Value</button></td> </tr> </tbody> </table> <button type="button" class="btn btn-primary btn-sm mt-2" id="add-detail-row">@L["AddRow"].Value</button> </div> </abp-modal-body> <abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer> </abp-modal> </abp-dynamic-form> <!-- 多行录入交互逻辑 --> <script> let rowIndex = 1; // 添加新行 document.getElementById('add-detail-row').addEventListener('click', () => { const body = document.getElementById('order-details-body'); const newRow = document.querySelector('.detail-row').cloneNode(true); // 更新表单字段索引,确保模型绑定正常 newRow.querySelectorAll('input').forEach(input => { input.name = input.name.replace('[0]', `[${rowIndex}]`); input.value = ''; }); // 绑定删除事件 newRow.querySelector('.remove-row').addEventListener('click', function() { this.closest('.detail-row').remove(); }); body.appendChild(newRow); rowIndex++; }); // 初始行删除事件绑定 document.querySelectorAll('.remove-row').forEach(btn => { btn.addEventListener('click', function() { this.closest('.detail-row').remove(); }); }); </script>
4. 数据存储(确保事务与一致性)
public class OrderAppService : ApplicationService, IOrderAppService { private readonly IRepository<Order, Guid> _orderRepository; public OrderAppService(IRepository<Order, Guid> orderRepository) { _orderRepository = orderRepository; } [UnitOfWork] public async Task CreateAsync(OrderCreateDto input) { // 从DTO转换为聚合根,封装业务逻辑 var order = new Order(GuidGenerator.Create(), input.Customer, input.OrderDate, input.Currency); foreach (var detailDto in input.OrderDetails) { order.AddDetail(detailDto.StockCode, detailDto.Price, detailDto.Quantity); } // 保存聚合根,自动关联保存子实体 await _orderRepository.InsertAsync(order); } }
二、表单尺寸宽度调整
通过自定义CSS修改模态框与表单字段宽度,不破坏ABP原有样式规则:
/* 修改模态框整体宽度 */ .custom-order-modal .modal-dialog { max-width: 900px; width: 90%; /* 响应式适配 */ } /* 调整表单标签与输入框的比例 */ .abp-form-content .form-group .col-sm-2 { flex: 0 0 20%; } .abp-form-content .form-group .col-sm-9 { flex: 0 0 78%; }
三、本地化调整
- 在项目
Localization/[项目名]/zh-Hans.json中添加对应键值:
{ "culture": "zh-Hans", "texts": { "NewOrder": "新建订单", "OrderDetails": "订单明细", "StockCode": "库存编码", "Price": "单价", "Quantity": "数量", "Actions": "操作", "Delete": "删除", "AddRow": "添加行", "Customer": "客户", "OrderDate": "订单日期", "Currency": "币种" } }
- 动态表单会自动读取DTO上
[Display(Name = "XXX")]对应的本地化键,无需额外配置。
四、验证与规则保障
- 前端:ABP动态表单会自动根据DTO的验证属性(
[Required]、[Range]等)生成HTML5验证与提示。 - 后端:ABP会自动在应用服务层执行DTO验证,确保非法数据无法进入业务逻辑。
- 聚合根封装:通过
AddDetail方法管控子实体的创建,确保业务规则不被绕过。
内容的提问来源于stack exchange,提问作者3ddark
相关产品推荐
相关产品推荐

