using AjaxUpload.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
namespace AjaxUpload.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult Upload(string file, string connId)
{
//註: 程式僅為展示用,實際開發時應加入更嚴謹的防錯防呆
string errMsg = null;
try
{
//取得檔案內容,解析為List<string>
string content = new StreamReader(Request.InputStream).ReadToEnd();
var sr = new StringReader(content);
List<string> lines = new List<string>();
string line = null;
while ((line = sr.ReadLine()) != null)
{
lines.Add(line);
}
//總筆數及目前處理筆數
int total = lines.Count, count = 0;
//使用Task進行非同步處理
var task = Task.Factory.StartNew(() =>
{
Random rnd = new Random();
for (var i = 0; i < lines.Count; i++)
{
string[] p = lines[i].Split('\t');
if (p.Length != 8)
throw new ApplicationException("Not a valid text file!");
//假裝將資料寫入資料庫,Delay 0-4ms
Thread.Sleep(rnd.Next(5));
count++;
}
});
//透過SignalR
float ratio = 0;
Action updateProgress = () =>
{
ratio = (float)count / total;
UploaderHub.UpdateProgress(connId, file, ratio * 100,
string.Format("{0:n0}/{1:n0}({2:p1})", count, total, ratio));
};
//每0.2秒回報一次進度
while (!task.IsCompleted && !task.IsCanceled && !task.IsFaulted)
{
updateProgress();
Thread.Sleep(200);
}
updateProgress();
//若正確完成,傳回OK
if (task.IsCompleted && !task.IsFaulted)
return Content("OK");
else
errMsg = string.Join(" | ",
task.Exception.InnerExceptions.Select(o => o.Message).ToArray());
}
catch (Exception ex)
{
errMsg = ex.Message;
}
UploaderHub.UpdateProgress(connId, file, 0, "-", errMsg);
return Content("Error:" + errMsg);
}
}
}
上傳時除了POST是二進位的檔案內容,還另外以QueryString傳入connId(SingalR的連線Id,當有多個網頁連線時才知道要回傳給哪一個Client)及file(檔案名稱)。Action內部先用StreamReader讀入上傳內容轉為字串,再用StringReader將字串解析成List<string>,接著逐行讀取。由於只是展示用途並不需要真的寫入資料庫,每讀一筆後用Thread.Sleep()暫停0-4ms(亂數決定),把處理兩千多筆的時間拉長到幾秒鐘方便觀察。至於回報進度部分,我決定採固定時間間隔回報一次的策略,故將處理資料邏輯放在Task裡非同執行,啟動後另外跑迴圈每0.2秒回報一次進度到前端。UploaderHub是這個專案自訂的SingalR Hub類別,它提供一個靜態方法UpdateProgress,可傳入connId、file、percentage(進度百分比)、progress(由於Client端不知道資料解析後的行數,故總行數及目前處理行數資訊全由Server端提供)、message(供錯誤時傳回訊息)。
安裝及設定SignalR的細節此處略過(基本上透過NuGet下載安裝並依Readme文件加上RouteTable.Routes.MapHubs();就搞定)。至於UploaderHub.cs,幾乎是個空殼子。繼承Hub之後,絕大部分的工作皆由父類別定義的方法搞定。唯一增加的UpdateProgress()靜態方式,在其中由GlobalHost.ConnectionManager.GetHubContext<UploaderHub>()取得UploaderHub執行個體,再經由Clients.Client(connId).updateProgress()呼叫JavaScript端的updateProgress函式。理論上這段程式可以寫在任何類別,因為UploaderHub太空虛怕引來其他類別抗議基於相關邏輯集中的考量,決定將它納為UploaderHub的方法。
using Microsoft.AspNet.SignalR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace AjaxUpload.Models
{
public class UploaderHub : Hub
{
//從其他類別呼叫需用以下方法取得UploaderHub Instance
static IHubContext HubContext =
GlobalHost.ConnectionManager.GetHubContext<UploaderHub>();
public static void UpdateProgress(string connId, string name, float percentage,
string progress, string message = null)
{
HubContext.Clients.Client(connId)
.updateProgress(name, percentage, progress, message);
}
}
}
<script src="~/Scripts/jquery-2.1.0.js"></script>
<script src="~/Scripts/jquery.signalR-1.2.1.js"></script>
<script src="@Url.Content("~/signalr/hubs")"></script>
<script src="~/Scripts/knockout-3.1.0.debug.js"></script>
self.files = ko.observableArray();
self.selectorChange = function (item, e) {
$.each(e.target.files, function (i, file) {
file.percentage = ko.observable(0);
file.progress = ko.observable();
file.widthStyle = ko.computed(function () {
return "right:" + (100 - file.percentage()) + "%";
file.message = ko.observable();
file.status = ko.computed(function () {
var msg = file.message(), perc = file.percentage();
if (perc == 0) return "Waiting";
else if (perc == 100) return "Done";
else return "Uploading...";
//以檔名為索引建立Dictionary,方便更新進度資訊
ko.computed(function () {
$.each(self.files(), function (i, file) {
self.dictionary[file.name] = file;
}).extend({ throttle: 100 });
self.upload = function () {
$.each(self.files(), function (i, file) {
var reader = new FileReader();
reader.onload = function (e) {
var data = e.target.result;
url: "@Url.Content("~/home/upload")" +
"?file=" + file.name + "&connId=" + connId,
contentType: "application/octect-stream",
processData: false, //不做任何處理,只上傳原始資料
reader.readAsArrayBuffer(file);
var vm = new viewModel();
//建立SignalR連線並取得Connection Id
var hub = $.connection.uploaderHub;
$.connection.hub.start().done(function () {
connId = $.connection.hub.id;
hub.client.updateProgress = function (name, percentage, progress, message) {
var file = vm.dictionary[name];
file.percentage(percentage);
if (message) file.message(message);
就這樣,一個會即時回報Server處理進度的網頁介面就完成囉! HTML5 File API + jQuery + ASP.NET MVC + SignalR + Knockout.js 合作演出大成功~