利用ModelBinding處理FormData回傳表單「多值集合」與「多重檔案」之作法

今天遇到使用Model Binding來快速處理表單AJAX回傳FormData時,表單中需要包含多值集合的項目,一直以來沒有在Model Binding的模型下進行這樣的資料型態處理,因此進行個小實驗來驗證一下,另外也順便實驗了多重檔案上傳的可能性,將程式碼紀錄於此供給有需要的人查閱。

表單包含多值集合的項目

舉例來說,有一份考卷資料,裡面包含了成績單的名稱,另外包含了一個學生成績清冊列表的集合,另外也有可能存在著考卷的影像檔案,這些資料如何透過FormData以AJAX的方式向後端傳送,並讓後端可以順利的讀取呢?

後端資料模型

public class Exam
{
  public string cName { get; set; }
  public System.Collections.Generic.List<Student> oStudents { get; set; }
}

public class Student
{
  public string cName { get; set; }
  public int iScore { get; set; }
}

值得一提的是,在一些網路文獻裡面有提到,可以在Exam類別下加上HttpPostedFile或是HttpPostedFileBase型別的檔案屬性資料,但就我實際上的測試來說,並不會被Model Binding自動識別與讀取。原因是先前我的WebForm DataBinding支援能力是從微軟的原始碼幹過來的,而有關於這方面全部的實作微軟早就轉移陣地至System.Web.MVC之中,包含IValueProvider、ValueProviderFactories、HttpFileCollectionValueProviderFactory HttpFileCollectionValueProvider...等實作,這麼龐大的架構移植到現行的WebForm框架中根本不符成本,因此直接放棄改成由Request.Files(HttpFileCollection)來拿即可。

說白點就是有關「HTTP檔案上傳」這部分放棄由ModelBinding來完成。

前端表單HTML

<form id="myForm">
  <fieldset>
    <div class="form-row">
      <div class="form-group col-12">
        <label>考卷名稱</label>
        <div class="input-group">
          <input type="text" class="form-control" id="cName" name="cName">
        </div>
      </div>
    </div>
    <div class="form-row">
      <div class="form-group col-12 col-md-6">
        <label>學生A姓名</label>
        <div class="input-group">
          <input type="text" class="form-control" id="oStudents[0].cName" name="oStudents[0].cName">
        </div>
      </div>
      <div class="form-group col-12 col-md-6">
        <label>學生A分數</label>
        <input type="text" class="form-control" id="oStudents[0].iScore" name="oStudents[0].iScore">
      </div>
    </div>
    <div class="form-row">
      <div class="form-group col-12 col-md-6">
        <label>學生B姓名</label>
        <div class="input-group">
          <input type="text" class="form-control" id="oStudents[1].cName" name="oStudents[1].cName">
        </div>
      </div>
      <div class="form-group col-12 col-md-6">
        <label for="iMoney">學生B分數</label>
        <input type="text" class="form-control" id="oStudents[1].iScore" name="oStudents[1].iScore">
      </div>
    </div>
    <div class="form-row">
      <div class="form-group col-12 col-md-6">
        <label>考卷影像檔1</label>
        <div class="input-group">
          <input type="file" id="oPaperFile1" name="oPaperFile1">
        </div>
      </div>
      <div class="form-group col-12 col-md-6">
        <label>考卷影像檔2</label>
        <div class="input-group">
          <input type="file" id="oPaperFile2" name="oPaperFile2">
        </div>
      </div>
    </div>
  </fieldset>
</form>

前端表單Javascript

$.ajax({
  url: cUrl,
  type: "POST",
  data: new FormData($("#myForm")[0]),
  processData: false, //FormDataRequired
  contentType: false, //FormDataRequired
  dataType: "json",
  beforeSend: function () {
    //傳輸中
  },
  complete: function () {
    //傳輸完成
  },
  error: function (oXHR, cStatus, cError) {
    //傳輸錯誤
  },
  success: function (oData) {
    //傳輸成功
  }
});

觀察一下傳輸過去的資料:

可以藉機看到multipart/form-data的依據,全部都是表單中的name屬性而已,所以在HTML中id屬性就算拿掉也可以正常運作。而「多值集合」與「多重檔案」的傳輸秘訣,關鍵就在於name屬性以陣列[n]的方式來描述即可。

後端讀取模型

//WebForm ModelBinding
var oMB = WebFormModelBinding.ModelBinding<Exam>();

//取得試卷名稱
oMB.oData.cName;
//取得考生姓名與成績
oMB.oData.oStudents.Select(x => $"{x.cName}|{x.iScore}");

//取得前端AJAX回傳的檔案並儲存
for (int i = 0; i < Request.Files.Count; i++)
{
  var oFile = Request.Files[i];
  oFile.SaveAs($@"\\YourFileServer\" + oFile.FileName);
}

Slashview.ModelBinding這個方法,可以在相關參考中找到之前的文章。

相關參考

ASP.NET MVC WebForm WebAPI FormData AJAX FileUpload Model ContainsLsit ContainsCollection ModelBinding