
本文介绍通过 javascript 序列化 + json 提交方式,解决 asp.net core 中因模型含递归引用(如树形 tvitem)导致的 model binding 失败问题,确保深层嵌套数据完整、可靠地传递至控制器。
本文介绍通过 javascript 序列化 + json 提交方式,解决 asp.net core 中因模型含递归引用(如树形 tvitem)导致的 model binding 失败问题,确保深层嵌套数据完整、可靠地传递至控制器。
在 ASP.NET Core 中,标准的 HTML 表单 POST 依赖于 name 属性的扁平化命名规则(如 selectionDetails.StandardsList[0].Children[1].IsSelected)进行模型绑定。但当模型中存在递归引用类型(如 TVItem.Parent 和 TVItem.Children 形成的树状结构)时,ASP.NET Core 的默认 ModelBinder 无法安全反序列化循环引用,且深层嵌套的索引路径易因 DOM 动态变化而错位,极易导致绑定失败或数据丢失。
因此,推荐采用 前端结构化采集 + JSON API 提交 的现代方案,绕过传统表单绑定限制,实现对任意深度、任意结构的复杂模型的精准传输。
✅ 推荐方案:JavaScript 构建 JSON 对象并 AJAX 提交
1. 前端:动态构建完整模型对象
使用 JavaScript 遍历 DOM 中的复选框与隐藏字段,按实际树形结构递归构造 TowerMeasureModel 对应的 JSON 对象:
function buildJSONObjectFromForm() {
const model = {
Id: parseInt(document.querySelector('input[name="Id"]').value) || 0,
Name: document.querySelector('input[name="LaunchName"]').value || "",
LaunchType: parseInt(document.querySelector('input[name="LaunchType"]:checked')?.value) || 0,
Calib_Standard: parseInt(document.querySelector('select[name="Calib_Standard"]').value) || 10,
Calib_Band: parseInt(document.querySelector('select[name="Calib_Band"]').value) || 12,
Calib_Setup: parseInt(document.querySelector('select[name="Calib_Setup"]').value) || 14,
Status: 20, // default STATUS_WAITING
selectionDetails: {
StandardsList: []
}
};
// 递归收集 TV 树节点状态(示例仅到频率层,可根据实际 TVStandard/TVSection/TVMoode/TVFreq 类型扩展)
const standardsList = document.querySelectorAll('ul > li');
standardsList.forEach((standardLi, idx) => {
const standardName = standardLi.querySelector('label')?.textContent?.trim();
const isStandardChecked = standardLi.querySelector('input[type="checkbox"]')?.checked || false;
const standard = {
folderName: standardLi.querySelector('input[name^="selectionDetails.StandardsList[' + idx + '].folderName"]')?.value || "",
Name: standardName,
IsSelected: isStandardChecked,
Children: []
};
const sections = standardLi.querySelector('.nested')?.querySelectorAll('> li');
if (sections) {
sections.forEach((sectionLi, secIdx) => {
const sectionName = sectionLi.querySelector('label')?.textContent?.trim();
const isSectionChecked = sectionLi.querySelector('input[type="checkbox"]')?.checked || false;
const section = {
scriptName: sectionLi.querySelector('input[name^="selectionDetails.StandardsList[' + idx + '].Children[' + secIdx + '].scriptName"]')?.value || "",
Name: sectionName,
IsSelected: isSectionChecked,
Children: []
};
const modes = sectionLi.querySelector('.nested')?.querySelectorAll('> li');
if (modes) {
modes.forEach((modeLi, modeIdx) => {
const modeName = modeLi.querySelector('label')?.textContent?.trim();
const isModeChecked = modeLi.querySelector('input[type="checkbox"]')?.checked || false;
const mode = {
Name: modeName,
IsSelected: isModeChecked,
Children: []
};
const freqs = modeLi.querySelector('.nested')?.querySelectorAll('> li');
if (freqs) {
freqs.forEach((freqLi, freqIdx) => {
const freqName = freqLi.querySelector('label')?.textContent?.trim();
const isFreqChecked = freqLi.querySelector('input[type="checkbox"]')?.checked || false;
const freqId = freqLi.querySelector('input[name^="selectionDetails.StandardsList[' + idx + '].Children[' + secIdx + '].Children[' + modeIdx + '].Children[' + freqIdx + '].ID"]')?.value || "";
mode.Children.push({
ID: freqId,
Name: freqName,
IsSelected: isFreqChecked
});
});
}
section.Children.push(mode);
});
}
standard.Children.push(section);
});
}
model.selectionDetails.StandardsList.push(standard);
});
return model;
}
2. 绑定提交事件并发送请求
为提交按钮添加事件监听,阻止默认表单提交,调用上述函数并发起 JSON POST 请求:
document.getElementById('myButton').addEventListener('click', function(event) {
event.preventDefault();
const model = buildJSONObjectFromForm();
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
fetch('/Launch/Home/RetrieveSelection', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': token
},
body: JSON.stringify(model)
})
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(data => {
console.log('Success:', data);
alert('提交成功!');
})
.catch(error => {
console.error('Error:', error);
alert('提交失败,请检查网络或数据格式');
});
});
3. 后端:控制器接收 [FromBody] JSON
确保控制器方法使用 [FromBody] 特性,并启用反 forgery 验证(若页面含 @Html.AntiForgeryToken()):
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult RetrieveSelection([FromBody] TowerMeasureModel model)
{
if (model == null)
{
return BadRequest("模型为空");
}
// ✅ 此时 model.SelectionDetails.StandardsList 已完整还原为树形结构
// 可直接映射到实体、保存数据库或触发业务逻辑
_logger.LogInformation($"收到 {model.Name},共 {model.selectionDetails?.StandardsList?.Count ?? 0} 个标准项");
// 示例:保存逻辑(请根据实际仓储层调整)
// _context.TowerMeasureModels.Add(model);
// await _context.SaveChangesAsync();
return Json(new { success = true, message = "配置已接收", id = model.Id });
}
⚠️ 关键注意事项
-
禁用默认表单提交:务必调用
event.preventDefault(),否则会触发传统表单跳转,JSON 请求失效。 - AntiForgeryToken 必须传递:ASP.NET Core 默认要求验证 Token,需从隐藏输入框中读取并放入请求头。
- 前端结构需严格匹配后端类定义:字段名、嵌套层级、数组索引顺序必须一致;建议使用 TypeScript 或 JSDoc 注释增强可维护性。
-
避免递归序列化
Parent字段:TVItem.Parent在 JSON 中不应被序列化(否则引发循环引用),前端构建时只保留Children数组,忽略Parent属性即可 —— 后端重建树时可按需重新关联。 -
服务端验证不可省略:即使前端校验完善,仍需在 Controller 或 Model 中使用
[Required]、[Range]等特性进行二次验证。
该方案彻底规避了 MVC 表单绑定对递归模型的限制,兼顾灵活性与健壮性,是处理深度嵌套、动态树形结构数据的推荐实践。










