2013年3月16日 星期六

ASP.NET MVC - 不使用 HttpPostedFileBase 處理檔案上傳

上一篇「ASP.NET MVC - 檔案上傳的基本操作」說明了在 ASP.NET MVC 檔案上傳的基本處理方式,並且在文章的最後也提到了 Controller 的 Action() 方法在處理由前端所傳過來資料處理的不同,一般表單輸入的資料是從 FormCollection 來取得,因為 FormCollection 繼承 NameValueCollection 為 String 索引鍵與 String 值的集合,而檔案上傳並非單純的 String 值,所以上傳檔案其 default value provider 是由 HttpFileCollectionValueProvider 類別,這就說明了為何在 FormCollection 裡是找不到上傳檔案的原因。

然而之前碰到有人遇到這麼一個狀況,因為在不知道前端上傳檔案時所使用 File Upload Input Tag 所設定的 Name 為何,所以無法在後端明確的在 Action () 方法中使用 HttpPostedFileBase 來處理上傳檔案。

必須說這樣的狀況蠻特殊的,但也並非沒有任何的解決方式,在這篇文章當中就來研究看看如何解決不使用 HttpPostedFileBase 的情況下處理檔案上傳。

 


先回想一下上一篇「ASP.NET MVC - 檔案上傳的基本操作」最後我們有提到的「HttpFileCollectionValueProvider」,以下是程式的內容,

public sealed class HttpFileCollectionValueProvider : DictionaryValueProvider<HttpPostedFileBase[]>
{
    private static readonly Dictionary<string, HttpPostedFileBase[]> _emptyDictionary = new Dictionary<string, HttpPostedFileBase[]>();
 
    public HttpFileCollectionValueProvider(ControllerContext controllerContext)
        : base(GetHttpPostedFileDictionary(controllerContext), CultureInfo.InvariantCulture)
    {
    }
 
    private static Dictionary<string, HttpPostedFileBase[]> GetHttpPostedFileDictionary(ControllerContext controllerContext)
    {
        HttpFileCollectionBase files = controllerContext.HttpContext.Request.Files;
 
        // fast-track common case of no files
        if (files.Count == 0)
        {
            return _emptyDictionary;
        }
 
        // build up the 1:many file mapping
        List<KeyValuePair<string, HttpPostedFileBase>> mapping = new List<KeyValuePair<string, HttpPostedFileBase>>();
        string[] allKeys = files.AllKeys;
        for (int i = 0; i < files.Count; i++)
        {
            string key = allKeys[i];
            if (key != null)
            {
                HttpPostedFileBase file = HttpPostedFileBaseModelBinder.ChooseFileOrNull(files[i]);
                mapping.Add(new KeyValuePair<string, HttpPostedFileBase>(key, file));
            }
        }
 
        // turn the mapping into a 1:many dictionary
        var grouped = mapping.GroupBy(el => el.Key, el => el.Value, StringComparer.OrdinalIgnoreCase);
        return grouped.ToDictionary(g => g.Key, g => g.ToArray(), StringComparer.OrdinalIgnoreCase);
    }
}

在 HttpFileCollectionValueProvider 類別中,是從 ControllerContext.HttpContext Request.Files 取得上傳檔案,

HttpFileCollectionBase files = controllerContext.HttpContext.Request.Files;

所以我們可以把原本上傳檔案的 Controller Action() 修改成以下的方式,

public ActionResult FileUpload()
{
    return View();
}
 
[HttpPost]
public ActionResult FileUploads()
{
    foreach (string file in Request.Files)
    {
        HttpPostedFileBase uploadFile = Request.Files[file] as HttpPostedFileBase;
        if (uploadFile != null && uploadFile.ContentLength > 0)
        {
            var fileName = Path.GetFileName(uploadFile.FileName);
            var path = Path.Combine(Server.MapPath("~/FileUploads"), fileName);
            uploadFile.SaveAs(path);
        }
    }
    return RedirectToAction("FileUpload");
}

上面的程式可以看到我們並沒有使用 IEnumerable<HttpPostedFileBase> 來接前端所上傳的檔案,而在 FileUploads 這個 Action() 方法中是從 Request.Files 裡去取得上傳的檔案,接下來看 View 頁面的內容,在 View 的內容裡,我刻意的將三個 File Input Tage 的 Name 使用了不同的名稱,

@{
    ViewBag.Title = "FileUpload";
}
 
<h2>FileUpload</h2>
 
<form action="@Url.Action("FileUploads")" method="post" enctype="multipart/form-data">
    
  <label for="file1">Filename:</label>
  <input type="file" name="file1" id="file1" />
  
  <label for="file2">Filename:</label>
  <input type="file" name="file2" id="file2" />
 
  <label for="file3">Filename:</label>
  <input type="file" name="file3" id="file3" />
 
  <input type="submit"  />
 
</form>

接著執行並觀察內容,跟前一篇所做的執行一樣,刻意不在第二個上傳檔案欄位去選取檔案,

image

在後端的 Action() 方法去下中斷點,觀察 Request.Files 的內容,

image

由上面可以看到在 Request.Files 裡有找到前端所傳送過來的三個上傳檔案欄位,

下圖,有找到第一個上傳的檔案,

image

下圖,第二個檔案就沒有檔案內容,

image

下圖,有找到第三個上傳的檔案,

image

而最後也有確實的把上傳檔案給存到指定的路徑位置下,

image

由此可知我們可以不必使用 HttpPostedFileBase 的情況下來取得上傳的檔案,這個情況大多會發生在需要 Ajax 下處理檔案上傳的情況,其實這樣的例子在以前的文章中就有出現過,在「ASP.NET MVC上傳檔案,使用file-uploader : 基本操作 」所提到的處理就是一個不使用 HttpPostedFileBase 來取得上傳檔案的例子,以下是那篇文章範例中處理檔案上傳的 View 頁面與 Action() 方法內容,

View

@{
    ViewBag.Title = "Basic1";
    Layout = "~/Views/Shared/_Layout.cshtml";
}
@section HeadContent
{
    <link href="@Url.Content("~/Content/fileuploader.css")" rel="stylesheet" type="text/css" />
}
 
<h2>Basic1</h2>
 
<div id="file-uploader">
    <noscript>
        <p>
            Please enable JavaScript to use file uploader.</p>
        <!-- or put a simple form for upload here -->
    </noscript>
</div>
 
@section JavaScriptContent
{
    @if (false){ <script src="../../Scripts/jquery-1.7.1.min.js" type="text/javascript"></script> }
    <script src="@Url.Content("~/Scripts/fileuploader.js")" type="text/javascript"></script>
    <script type="text/javascript" language="javascript">
    <!--
        $(document).ready(function ()
        {
            createUploader();
        });
 
        function createUploader()
        {
            var uploader = new qq.FileUploader(
            {
                element: $('#file-uploader')[0],
                action: '@Url.Action("BasicUpload", "Home")',
            });
        }
    -->
    </script>
}

因為使用 file-uploader,所以在 View 的頁面中是不需要實際加入 File Input Tag,而是由 file-uploader 的 javascript 於網頁執行時動態處理檔案上傳欄位,如下圖用紅線所框起來的部分,

image

 

Controller Action

public ActionResult BasicUpload(string qqfile)
{
    string uploadFolder = "FileUploads";
    string path = Server.MapPath(string.Format("~/{0}", uploadFolder));
    
    try
    {
        // To handle differences in FireFox, Chrome, Safari, Opera
        Stream stream = Request.Files.Count > 0 
            ? Request.Files[0].InputStream 
            : Request.InputStream;
 
        string filePath = Request.Files.Count > 0
            ? Path.Combine(path, System.IO.Path.GetFileName(Request.Files[0].FileName))
            : Path.Combine(path, qqfile);
 
        var buffer = new byte[stream.Length];
        stream.Read(buffer, 0, buffer.Length);
        System.IO.File.WriteAllBytes(filePath, buffer);
 
        return Json(new { success = true }, "text/html");
    }
    catch (Exception ex)
    {
        return Json(new { error = ex.Message }, "text/html");
    }
}

而後端的 Action() 處理中就是直接使用 Request.Files 來取得上傳的檔案(因為 IE 與非 IE 瀏覽器於前端處理會有所差異,所以後端程式就會有兩種不同的處理方式)。

 

看到這裡也是文章的結尾了,也許大家心理會想說,看完這篇後,是我要讓大家知道可以不用 HttpPostedFileBase 而改用 Request.Files 來取得上傳的檔案嗎?

絕對不是!

會說明這樣的處理方式是要讓大家知道在無法使用 HttpPostedFileBase 的情況下,我們可以改用 Request.Files,而這樣的情況是少數的,於後端 Controller Action() 方法裡處理上傳檔案還是要優先使用 HttpPostedFileBase,HttpPostedFileBase 類別的屬性已經包含了上傳檔案的大小(ContentLngth)、上傳檔案的 MIME 內容類型(ContentType)、上傳檔案的完整名稱(FileName)、上傳檔案的 Stream 物件以備讀取檔案內容(InputStream),如果是使用 Request.Files 的話,上述的內容都必須要另外再去做處理後才能取得。

MSDN - HttpPostedFileBase 類別

這就好比為何在 ASP.NET MVC 的後端要取得前端表單所傳送過來的值是要使用 FormCollection 而非 Request.Form,這是一樣的道理,在黑暗執行緒的一篇文章裡就有說到,可以參考黑大所提出的兩點考量:便於單元測試、保留客製擴充彈性。

黑暗執行緒 - 用FormCollection取代Request.Form,Why?

 

以上

沒有留言:

張貼留言

提醒

千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。

最近的留言