ASP.NET Core视图提交仅部分ViewModel回传,求便捷解决方案
问题根因
ASP.NET MVC的模型绑定只会从请求的表单字段、路由数据、查询字符串里取值填充ViewModel,没有提交的字段自然就是默认值null。你之前反射遍历的写法生成的隐藏域name属性不符合绑定规则(正确的name需要体现属性的嵌套层级,比如Mandant.S_MD_FIR,你的写法生成的name是property,所以无法绑定)。
最优解决方案推荐
方案1:自定义HtmlHelper扩展批量生成隐藏域(最适配你的需求)
写个通用的扩展方法,递归处理嵌套类、集合属性,自动生成符合绑定规则的隐藏域,代码如下:
public static class HtmlHelperExtensions { public static IHtmlContent HiddenForModelProperties<TModel, TProperty>(this IHtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression) { var sb = new StringBuilder(); var modelMetadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData); var propertyPath = html.NameFor(expression).ToString(); // 处理值类型、字符串等基础类型属性 if (modelMetadata.IsComplexType == false || modelMetadata.ModelType == typeof(string)) { sb.AppendLine(html.Hidden(expression).ToString()); return new HtmlString(sb.ToString()); } // 处理集合类型 if (typeof(IEnumerable).IsAssignableFrom(modelMetadata.ModelType) && modelMetadata.ModelType != typeof(string)) { var collection = modelMetadata.Model as IEnumerable; if (collection == null) return new HtmlString(sb.ToString()); var index = 0; foreach (var item in collection) { var itemType = item.GetType(); foreach (var prop in itemType.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { if (prop.GetGetMethod() == null || !prop.CanWrite) continue; var propValue = prop.GetValue(item); var inputName = $"{propertyPath}[{index}].{prop.Name}"; sb.AppendLine(html.Hidden(inputName, propValue).ToString()); } index++; } return new HtmlString(sb.ToString()); } // 处理复杂类型 foreach (var property in modelMetadata.Properties) { if (property.IsComplexType && property.ModelType != typeof(string)) { // 递归处理嵌套复杂类型 var parameter = Expression.Parameter(typeof(TModel), "m"); var propertyAccess = Expression.Property(expression.Body, property.PropertyName); var lambda = Expression.Lambda<Func<TModel, object>>(Expression.Convert(propertyAccess, typeof(object)), parameter); sb.AppendLine(html.HiddenForModelProperties(lambda).ToString()); } else { var inputName = $"{propertyPath}.{property.PropertyName}"; sb.AppendLine(html.Hidden(inputName, property.Model).ToString()); } } return new HtmlString(sb.ToString()); } }
使用方法非常简单,在表单里直接调用即可:
@using (Html.BeginForm("Update", "Home")) { @* 批量生成三个复杂类型的所有隐藏域 *@ @Html.HiddenForModelProperties(m => m.Mandant) @Html.HiddenForModelProperties(m => m.Kreditor) @Html.HiddenForModelProperties(m => m.Positions) @Html.HiddenForModelProperties(m => m.Invoice.M_IV_ID) @* 原有表单代码不变 *@ @Html.ValidationSummary() <div class="container"> <!-- 原有输入框代码 --> </div> <button type="submit" class="btn btn-primary">Update</button> }
这个方案的优势是完全不用手动写每个属性的隐藏域,自动适配你ViewModel的结构变更,无服务端依赖。
方案2:服务端缓存初始ViewModel合并数据
如果你的ViewModel属性非常多,生成大量隐藏域会增加请求体积,可以用这个方案:
- GET请求渲染表单时,将完整的
ValidationViewModel按Invoice.M_IV_ID作为Key存入Session或者分布式缓存,设置合理的过期时间。 - POST接收提交的ViewModel后,先从缓存中取出完整的原始ViewModel,再将提交过来的已修改属性覆盖到原始模型上,就能得到完整的ViewModel。
[HttpPost] public IActionResult Update(ValidationViewModel validationViewModel) { if (ModelState.IsValid) { // 从缓存拿完整模型 var fullModel = HttpContext.Session.Get<ValidationViewModel>(validationViewModel.Invoice.M_IV_ID.ToString()); // 覆盖修改过的属性 fullModel.Invoice.M_IV_KREDITOR = validationViewModel.Invoice.M_IV_KREDITOR; fullModel.Invoice.M_IV_INVOICEDATE = validationViewModel.Invoice.M_IV_INVOICEDATE; // 后续直接用fullModel操作 fullModel.Invoice.Update(); } return View("Index", validationViewModel); }
方案3:AJAX提交JSON数据
如果可以接受异步提交,直接把整个ViewModel序列化为JSON放到请求体里提交,后端用[FromBody]接收即可,不需要任何隐藏域:
// 前端提交代码 $('form').submit(function(e) { e.preventDefault(); var model = @Html.Raw(Json.Serialize(Model)); // 覆盖输入框修改的属性 model.Invoice.M_IV_KREDITOR = $('input[name="Invoice.M_IV_KREDITOR"]').val(); model.Invoice.M_IV_INVOICEDATE = $('input[name="Invoice.M_IV_INVOICEDATE"]').val(); $.ajax({ url: '@Url.Action("Update", "Home")', type: 'POST', contentType: 'application/json', data: JSON.stringify(model), success: function(res) { // 处理返回结果 } }); });
后端接口修改为:
[HttpPost] public IActionResult Update([FromBody] ValidationViewModel validationViewModel) { // 逻辑不变 }
额外注意点
你的Positions属性目前只有get访问器,无论是模型绑定还是手动赋值都无法生效,需要修改为:
public List<M_IP_INVOICE_POS> Positions { get; set; }
内容的提问来源于stack exchange,提问作者user3793935
相关产品推荐
相关产品推荐

