2018/12/06

20181206-程式-knockout 搭配 Fileupload 的思考過程記錄

20181206-上傳檔案結果

(一整個上傳檔案的流程結果:編輯>選擇檔案>挷定在頁面上>更新儲存>測試下載>成功>再編輯>刪除已存在檔案>完成此流程)

(第一次使用git動畫圖示意~~)

(使用技術:ASP.NET MVC、Knockout 、FileUpload (knockout-file-bindings Framework) ….)


情境說明


就這一次的技術需求:

「編輯時可上傳檔案,於儲存時,將檔案儲存起來」(呈現檔名/檔案大小)

是的,需求就是「這麼的簡單」。

而在技術實作上,我也要盡量做到「操作簡單化」的方向前進。

這之中遇到的思考問題還真是多呀!


而 先解決 Service 端針對檔案的儲存實作。使用Byte[]的方式來做為 串流的傳輸。


再來,就是著重在前端Client 與 Web Server之間的傳輸處理。


思考的問題有下列:(針對檔案處理的部份)

■  為了讓使用者操作方便,採用Knockout 的開發模式,而檔案部份,期望如上圖模式的動線。

■  我如何先將現在的檔案清單 以Knockout呈現? 並可以供下載 ?

■  Client端的 FileUpload 它的原理?不是很熟。

■  Client端的 File 串流,如何Post 至 Server端? MVC的Action要如何的接??

     我可以利用現有的Model直接post 整包回去Server端嗎? 重點是我要在 Knockout 的架構下實作

■  在上傳檔案後,於使用者按下「儲存」前,我不想將檔案 先暫存於Server端 (原本有思考這樣的模式,but….)。

    而是在他點選「儲存」時,我再一併去儲存檔案。


開發處理流程


記錄下自已依序實作的重點項目,一步步的將這一份需求完成:

一、Server端的服務應用,先可以完成 新增/更新/刪除 檔案的動作,其中包括:

       a.  思考傳輸所用的 Contract 如何設計?

       a.  依儲存規則儲存檔案。

       b.  更新時,整包Model參數傳上來時,針對檔案的部份,決定是採 新增動作?還是 刪除動作?


二、前端 Detail的呈現,加上了「檔案」的資訊,其中包括:

      a.  呈現檔案清單時,只保留Key值資訊,而點選後,另一個Action 單純處理下載檔案的功能。

      b.  使用Knockout 的挷定呈現。並加上HyperLink 點選時可以下載檔案。


三、檔案上傳的功能,並可以挷定至Knockout,且可以Post串流至Controller接受端,其中包括:

     a.  Client端的檔案上傳,它的串流是如何取得?這是我不懂的一塊邏輯。

     b.  因為我是直接用整個Model 傳輸,所以,它byte[]的型別 可以接受嗎??

     c.  檔案上傳後的動作,我透過「knockout-file-bindings」(link) 來達到此功能,好處是:

               1) 原來開發操作這麼簡單  (官方範例:(Link))

               2) 很容易取到它上傳的檔案資訊(檔名/大小/…)、檔案串流(base64String)   (我若用原生的方式取得 byte串流,效能很差 )

               3) 它post 回 controller,可以直接 使用 byte[] 屬性 接受串流的 (使用 base64String的資訊)。 (超有幫助)


就這樣,完成這樣的實作過程!!


重點程式碼結果說明(檔案上傳部份)

(原本的blog coding style css 沒有生效@@,僅能先貼程式碼上來啦~)


//Html的檔案上傳元件

<!-- ko if: ($root.IsEdit()||$root.IsCreate()) -->
        <input type="file" multiple data-bind="fileInput: multiFileData" />
<!-- /ko -->


//檔案清單的呈現區寫法(Knockout)

<div data-bind="foreach:FileInfos" style="max-height:100px;overflow:auto">
                   <div class="col-md-12">
                      <!-- ko if: ($root.IsEdit()||$root.IsCreate()) -->
                       <div class="col-md-2">
                           <span class="glyphicon glyphicon-remove onpath" data-bind="click:removeFileInfo"></span>
                       </div>
                       <!-- /ko -->
                       <div class="col-md-10">
                           <a href="#" data-bind="text:FileName,click:downloadFileInfo"></a>

                          &nbsp;(<span data-bind="text:FileSizeDisplay"></span>)
                       </div>
                   </div>
               </div>


//於Script區 ,定義了其multiFileData,這樣才可以在它有變動時,觸發我想做的事

viewModel.multiFileData = ko.observable({
         fileArray: ko.observableArray(),
         base64StringArray: ko.observableArray(),
         dataURLArray: ko.observableArray(),
     });


//當檔案選擇時的觸發事件時,我要將資料加入至「viewModel.FileInfos」,而其中的「content」資訊就是挷定其byte[]資料

//使用knockout-file-binding,但它「base64StringArray.subscribe若上傳4個檔案,它會觸發4次

viewModel.multiFileData().base64StringArray.subscribe(function (base64StringArray) {
          console.log("call:" + base64StringArray.length);
          if (base64StringArray.length > 0) {
              var last = base64StringArray.length - 1;   //因為觸發多次關係,所以僅取得每次的最後一筆
              var fileInfo = viewModel.multiFileData().fileArray()[last];
              var base64String = viewModel.multiFileData().base64StringArray()[last];
              addFileInfo(fileInfo, base64String);
          }
      });
      function addFileInfo(fileInfo, base64String) {   //加入至 FileInfos的陣列中
          var name = fileInfo.name;
          var size = fileInfo.size;
          var kofile = ko.mapping.fromJS(initFileMaintainModel);
          kofile.FileSizeDisplay = ko.observable("");
          kofile.FileName(name);
          kofile.FileSize(size);
          kofile.FileSizeDisplay(fileSizeDisplay(size));
          kofile.Content(base64String);
          viewModel.FileInfos.push(kofile);
      }

     function fileSizeDisplay(FileSize) {    //將檔案大小轉為/MB/KB/byte的模式呈現
          var result =
              (FileSize >= 1024 * 1024) ? Math.ceil((FileSize / 1024 * 1024 )) + " MB" :
                  (FileSize >= 1024) ? Math.ceil((FileSize / 1024)) + " KB" :
                  (FileSize) + " byte";
          return result;
      }

//而儲存時傳送Ajax回去的參數處理

        JSON.stringify(ko.mapping.toJSON(viewModel))

而使用Ajax傳送時,這個contentType很重要,意思是指:將傳送的參數當json結構處理

image

則Post 回 MVC時,就可以直接使用Model接收的:

image

而其中的檔案串流,我Model採用 byte[] 型別,是可以接收的。 (client端直接取得其  base64String 即可)

image


剛上傳的檔案,要再直接讓使用者下載


在 User 未儲存前,其剛剛上傳的檔案,點選其Link,又要讓User可以下載 剛剛所上傳的檔案,

這需求,因全在Client端操作,這個想法需求 又讓我 「學了一關」 了!


而這主要的概念動作為:

     a.  透過base64String 產生 Blob() 串流資訊

     b.  透過 Url.CreateObjectURL(blob) 產生 暫時用的 連結。

     c.  若使用 window.open (url)的方式開啟,可下載,但檔名無法定義。

     d. 若使用 <a> 下的download 屬性,可以指定它的「檔案名稱」。



//從base64String 轉換為 Blob的函式


         // http://bit.ly/2Pp9K4N
         function b64toBlob(b64Data, contentType, sliceSize) {
             contentType = contentType || '';
             sliceSize = sliceSize || 512;

            var byteCharacters = atob(b64Data);  //將base-64 解碼(decode)為字串
             var byteArrays = [];

            for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
                 var slice = byteCharacters.slice(offset, offset + sliceSize);

                var byteNumbers = new Array(slice.length);
                 for (var i = 0; i < slice.length; i++) {
                     byteNumbers[i] = slice.charCodeAt(i);
                 }

                var byteArray = new Uint8Array(byteNumbers);

                byteArrays.push(byteArray);
             }

            var blob = new Blob(byteArrays, { type: contentType });
             return blob;
         }


讓使用者下載的方式

             var name = item.FileName();
             var mimetype = item.MimeType();
             var content = item.Content();
             var blob = b64toBlob(content, mimetype);
             var blobUrl = URL.createObjectURL(blob);      //透過Blob建立下載的Url,若是用window.open()則無法指定檔名
             //透過<a>來指定下載檔名
         var a = document.createElement("a");
             document.body.appendChild(a);
             a.style = "display: none";
             a.href = blobUrl;
             a.download = name;     //指定FileName
             a.click();
             window.URL.revokeObjectURL(url);      //消除url
             a.remove();


MVC無法傳送大量的JSON資料?

因為我需上傳 大於1MB的檔案,而得到此錯誤。

遇到此Error:

System.ArgumentException: Error during serialization or deserialization using the JSON JavaScriptSerializer. The length of the string exceeds the value set on the maxJsonLength property.


網路上查的解法是:

<configuration>

<system.web.extensions>
        <scripting>
            <webServices>
                <jsonSerialization maxJsonLength="50000000"/>
            </webServices>
        </scripting>
</system.web.extensions>

</configuration>

但這是沒有用的!!這指的是 Server端 傳回給 Client端的最大Json 長度。


而我需要的是: post json data to MVC action

最終的解決方式:

僅能去改寫原本 MVC5 的 JsonValueProviderFactory 的程式碼:  (官方原始程式碼為:http://bit.ly/2Pn5OBn )

JavaScriptSerializer serializer = new JavaScriptSerializer();
serializer.MaxJsonLength = int.MaxValue;


調整完後,再於 Application_Start 下,改掉它的Factory:

//howard:增加 request Json length
             ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<JsonValueProviderFactory>().FirstOrDefault());
             ValueProviderFactories.Factories.Add(new MyJsonValueProviderFactory());



(THE END)

0 意見 :

張貼留言