PAYATHON 2026

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 请求无法自动组成一个数据库事务。图片上传失败后,应保留已生成的编号,允许按该编号重试,避免重新创建交易。

备注:内容仅供参考。