jQuery 如何通过 FormData 向 C# MVC 发送图片和列表?
可以用同一个 FormData 请求发送图片和列表。当前代码把列表转成了 JSON 字符串,Controller 应用 string 接收,再反序列化为 List<EntidadDetalleTransaccion>。
如果拆成两个请求,就要等第一个请求成功返回 numeromovimiento,再调用图片上传方法,并将编号直接作为参数传入。连续调用两个 Ajax 方法,并不能保证第一个请求先完成。
问题中使用了 HttpPostedFileBase,因此以下示例采用经典 ASP.NET MVC。ASP.NET Core 的文件参数应使用 IFormFile,这些 Controller 签名不能直接照搬。仅凭 VS 2022 无法判断项目使用的 MVC 版本。
为什么 FormData 中有数据,列表却绑定失败?
这段代码:
request.append("listaconceptos", JSON.stringify(lista_conceptos));
发送了一个名为 listaconceptos 的普通表单字段,字段值是 JSON 文本,例如:
[{"movNumero":0,"movIdTransaction":2,"movConcepto":"检查项","movCalificacion":"CUMPLE"}]
经典 ASP.NET MVC 的默认模型绑定器不会自动将表单字段中的 JSON 文本解析为复杂对象列表,因此下面的参数类型与发送格式不匹配:
List<EntidadDetalleTransaccion> listaconceptos
有两种处理方式,按现有代码改动较少的是第一种:
- 发送 JSON 字符串,服务端用
string接收并反序列化。 - 按
listaconceptos[0].movConcepto这样的字段名发送,让模型绑定器直接绑定列表。
方案一:一次请求发送 JSON 字符串和图片
前端整理业务数据
将业务数据收集和文件添加分别封装成函数,单请求和双请求方案都能复用:
function recogerDatos(numero) {
var transaction = {
movIdTransaction: 2,
movNumero: numero,
movEntidad: $("#cbbentidad").val()
};
var lista_conceptos = [];
$("#tabla tbody tr").each(function () {
lista_conceptos.push({
movNumero: numero,
movIdTransaction: 2,
movConcepto: $(this).find("td").eq(0).text().trim(),
movCalificacion: $(this)
.find('input[type="checkbox"]')
.prop("checked") ? "CUMPLE" : "NO CUMPLE"
});
});
return {
transaction: transaction,
listaconceptos: lista_conceptos
};
}
function agregarImagenes(request) {
for (var i = 1; i <= 3; i++) {
var archivo = $("#fileProducto" + i)[0].files[0];
if (archivo) {
request.append("archivoImagen" + i, archivo);
}
}
}
这里用 .text() 读取单元格文字,只有在检查项需要保存 HTML 时才使用 .html()。
发送时,将业务对象和列表序列化,图片保留为 File:
function EnviarTodo() {
var numero = Number($("#txtNumero").val() || 0);
var datos = recogerDatos(numero);
var request = new FormData();
request.append("objeto", JSON.stringify(datos.transaction));
request.append("listaconceptos", JSON.stringify(datos.listaconceptos));
agregarImagenes(request);
return $.ajax({
url: '@Url.Action("RegistrarInspeccion", "Transactions")',
type: "POST",
data: request,
processData: false,
contentType: false,
dataType: "json"
}).fail(function () {
alert("提交失败,请检查服务端响应。");
});
}
processData: false 让 jQuery 保留 FormData,contentType: false 则让浏览器生成带有正确 boundary 的 multipart/form-data 请求头。不要手动将 contentType 设为 "multipart/form-data"。
包含 @Url.Action(...) 的 JavaScript 应放在 Razor 视图中;如果放在独立 .js 文件里,需要由视图传入接口地址。
Controller 用字符串接收,再反序列化
DTO 的字段类型应与项目实际模型一致。下面暂将 movEntidad 定义为 string;项目中已有这些实体时,直接使用现有类型即可:
public class TransaccionEntrada
{
public int movIdTransaction { get; set; }
public int movNumero { get; set; }
public string movEntidad { get; set; }
}
public class EntidadDetalleTransaccion
{
public int movNumero { get; set; }
public int movIdTransaction { get; set; }
public string movConcepto { get; set; }
public string movCalificacion { get; set; }
}
Controller 将两个 JSON 字段都声明为 string:
using System.Collections.Generic;
using System.Web;
using System.Web.Mvc;
using Newtonsoft.Json;
[HttpPost]
public ActionResult RegistrarInspeccion(
string objeto,
string listaconceptos,
HttpPostedFileBase archivoImagen1,
HttpPostedFileBase archivoImagen2,
HttpPostedFileBase archivoImagen3)
{
if (string.IsNullOrWhiteSpace(objeto) ||
string.IsNullOrWhiteSpace(listaconceptos))
{
return new HttpStatusCodeResult(400, "Missing form data.");
}
TransaccionEntrada transaction;
List<EntidadDetalleTransaccion> conceptos;
try
{
transaction =
JsonConvert.DeserializeObject<TransaccionEntrada>(objeto);
conceptos =
JsonConvert.DeserializeObject<
List<EntidadDetalleTransaccion>>(listaconceptos);
}
catch (JsonException)
{
return new HttpStatusCodeResult(400, "Invalid JSON.");
}
if (transaction == null || conceptos == null)
{
return new HttpStatusCodeResult(400, "Invalid form data.");
}
// 在这里校验业务字段、保存交易和明细,并处理图片。
// 未选择的文件可能为 null;使用前应检查 ContentLength。
// 当前登录用户应从服务端 User.Identity 获取。
return Json(new
{
recibido = true,
cantidadConceptos = conceptos.Count
});
}
这段 Controller 只演示接收和解析,数据库及文件保存逻辑还需要接入项目现有代码。手动反序列化得到的对象也要显式进行业务校验,JSON 解析成功并不代表数据有效。
示例没有异步操作,所以使用 ActionResult。如果保存过程调用了异步 API,再改为 async Task<ActionResult>,并使用 await。
另一种写法:按索引字段直接绑定列表
要保留 Controller 中的 List<EntidadDetalleTransaccion> 参数,可以将列表按索引逐项添加到表单,无需对列表调用 JSON.stringify():
var datos = recogerDatos(0);
var request = new FormData();
Object.keys(datos.transaction).forEach(function (key) {
request.append("objeto." + key, datos.transaction[key]);
});
datos.listaconceptos.forEach(function (item, index) {
Object.keys(item).forEach(function (key) {
request.append(
"listaconceptos[" + index + "]." + key,
item[key]
);
});
});
agregarImagenes(request);
生成的字段名类似:
objeto.movNumero
objeto.movEntidad
listaconceptos[0].movNumero
listaconceptos[0].movConcepto
listaconceptos[1].movNumero
listaconceptos[1].movConcepto
对应的 Controller 参数改为:
public ActionResult RegistrarInspeccion(
TransaccionEntrada objeto,
List<EntidadDetalleTransaccion> listaconceptos,
HttpPostedFileBase archivoImagen1,
HttpPostedFileBase archivoImagen2,
HttpPostedFileBase archivoImagen3)
发送请求时仍需设置 processData: false 和 contentType: false。索引应从 0 开始连续生成,模型属性需要有可写的 public setter,服务端还应检查 ModelState.IsValid。
两种传参格式任选其一,前端发送格式与 Controller 参数类型保持一致即可。
为什么 Split 后创建 List 会报 CS1503?
下面的代码得到的是 string[]:
string[] substrings = listaconceptos.Split(',');
List<EntidadDetalleTransaccion> 的集合构造函数接收的则是:
IEnumerable<EntidadDetalleTransaccion>
string[] 的元素是字符串,不能直接当作 EntidadDetalleTransaccion 使用,也不符合另一个构造函数所需的 int capacity 参数类型,因此会报参数转换错误。
JSON 对象和字段值中都可能包含逗号,用 Split(',') 会破坏数据结构。这里应直接反序列化:
var conceptos =
JsonConvert.DeserializeObject<List<EntidadDetalleTransaccion>>(
listaconceptos);
方案二:第一个请求返回编号后,再上传图片
原代码这样调用:
EnviarTransaccionOperacion();
EnviarImagenesenTransaccionOperacion();
这只能保证两个函数按顺序调用。第一个函数发出 Ajax 请求后就会返回,第二个函数往往在响应到达前便读取了 #txtNumero,拿到的仍是旧值 0。
应在第一个请求的成功回调中调用图片上传方法,并直接传入返回的编号:
function AdicionarMovimiento() {
var entidad = $("#cbbentidad").val();
if (!entidad || entidad === "0") {
swal("", "Debe seleccionar una entidad.", "warning");
return;
}
EnviarTransaccionOperacion();
}
function EnviarTransaccionOperacion() {
var datos = recogerDatos(0);
return $.ajax({
url: '@Url.Action("RegistrarInspeccion", "Transactions")',
type: "POST",
data: JSON.stringify({
oTransaction: datos.transaction,
listaconceptos: datos.listaconceptos
}),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (data) {
// 假设业务编号为正整数。
var numero = Number(data && data.numeromovimiento);
if (!Number.isSafeInteger(numero) || numero <= 0) {
alert("服务端未返回有效的交易编号。");
return;
}
$("#txtNumero").val(numero);
EnviarImagenesenTransaccionOperacion(numero);
},
error: function () {
alert("交易保存失败。");
}
});
}
function EnviarImagenesenTransaccionOperacion(numeromovimiento) {
var request = new FormData();
request.append("numeromovimiento", numeromovimiento);
agregarImagenes(request);
return $.ajax({
url: '@Url.Action("RegistrarInspeccionImagenes", "Transactions")',
type: "POST",
data: request,
processData: false,
contentType: false,
dataType: "json"
}).fail(function () {
alert("交易已保存,但图片上传失败,请重试上传。");
});
}
#txtNumero 仍会更新,供页面显示。图片请求直接使用函数参数中的编号,不再通过 input 中转。
这组代码假设编号是 JavaScript 能安全表示的正整数。如果实际编号是字符串,或超出安全整数范围,应保留字符串,并按项目规则校验。
两个接口的参数与返回值如何对应?
双请求方案的第一个接口接收整个 JSON 请求体,参数可以写成:
public ActionResult RegistrarInspeccion(
TransaccionEntrada oTransaction,
List<EntidadDetalleTransaccion> listaconceptos)
这个签名对应 JSON 请求体,前面接收两个字符串的签名对应单次 FormData 请求,使用时要与发送方式匹配。
业务数据保存成功并取得真实编号后,第一个接口返回:
return Json(new
{
numeromovimiento = numeroGenerado
});
numeroGenerado 应来自数据库或业务保存方法,返回字段名须与前端读取的 data.numeromovimiento 一致。
图片接口的签名可以写成:
public ActionResult RegistrarInspeccionImagenes(
int numeromovimiento,
HttpPostedFileBase archivoImagen1,
HttpPostedFileBase archivoImagen2,
HttpPostedFileBase archivoImagen3)
保存图片时,用 numeromovimiento 参数关联交易,同时检查交易是否存在,以及当前用户是否有权操作。
如果图片接口只需要编号和文件,就不必发送 objeto。如果确实需要业务对象,也应将其中的 movNumero 设为返回编号,避免一次传入两个不同的编号。
还需要修正的细节
- 原代码定义了
transaccion,随后却调用JSON.stringify(transaction)。变量名必须统一,否则可能报ReferenceError。 - jQuery 的响应格式配置项是
dataType,不是datatype。dataType控制响应解析,contentType控制请求体类型。 movUser和movConductor是不同的属性名,应按实际模型统一。登录用户信息应由服务端读取。- 未选择文件时,不要将
undefined添加到FormData。服务端也要检查文件是否为空,并校验实际内容和大小。 - 不要用
async: false或固定延时处理请求顺序。应在成功回调中发起第二个请求,或通过 Promise 正确串联请求。 - 两个 HTTP 请求无法自动组成一个数据库事务。图片上传失败后,应保留已生成的编号,允许按该编号重试,避免重新创建交易。
备注:内容仅供参考。
