(一整個上傳檔案的流程結果:編輯>選擇檔案>挷定在頁面上>更新儲存>測試下載>成功>再編輯>刪除已存在檔案>完成此流程)
(第一次使用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>
(<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結構處理
則Post 回 MVC時,就可以直接使用Model接收的:
而其中的檔案串流,我Model採用 byte[] 型別,是可以接收的。 (client端直接取得其 base64String 即可)
剛上傳的檔案,要再直接讓使用者下載
在 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)