2013年3月15日 星期五

ASP.NET MVC - 檔案上傳的基本操作

做網站系統多半都會有檔案上傳的需求,而我也在 2012 年用了四篇文章的篇幅介紹如何使用「file-uploader」這個前端套件來完成一個檔案上傳的功能,

ASP.NET MVC上傳檔案,使用file-uploader : 基本操作
ASP.NET MVC上傳檔案,使用file-uploader : 進階操作 Part.1
ASP.NET MVC上傳檔案,使用file-uploader : 進階操作 Part.2
ASP.NET MVC上傳檔案,使用file-uploader : 進階操作 Part.3

不過這一系列的文章有使用「file-uploader」這個前端套件而且在後端的 Controller Action 操作也比較進階,並不適合給初學者參考,所以這邊整理一下有關 ASP.NET MVC 的檔案上傳基本操作方法。

 


單一檔案上傳

這個操作是最簡單也最為基本的操作,以下為 View 的內容,

image

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

再來就是 Controller 的 Action 方法,

image

public ActionResult Upload()
{
    return View();
}
 
[HttpPost]
public ActionResult Upload(HttpPostedFileBase file)
{
    if (file.ContentLength > 0)
    {
        var fileName = Path.GetFileName(file.FileName);
        var path = Path.Combine(Server.MapPath("~/FileUploads"), fileName);
        file.SaveAs(path);
    }
    return RedirectToAction("Upload");
}

MSDN - HttpPostedFileBase 類別

在 Upload 的 Action() 裡,我們會以 HttpPostedFileBase 類別來接前端所上傳的檔案,而接收檔案的參數名稱為「file」,所以在前端 View 表單中所使用的 file input tag 的 Name 也必須要為「file」,如此後端才能收到上傳的檔案,

image

接著我們下個中斷點來觀察 Action 方法,

在 Action 內可以對 file 進行判斷是否有上傳檔案,當沒有上傳檔案時 file 為 null,

image

上面的程式並沒有對於「未上傳檔案」的狀況做另外處理,所以當 file 為 null 時就會出現 exception,

image

所以可以先進行是否有檔案上傳的判斷,

image

而當有檔案上傳時,就可以在 file 中取得相關的資訊,其中 ContentLength 就是檔案大小,

image

上傳的檔案是存放到我們所指定的位置裡,

image

 


多個檔案上傳

而要處理多個檔案上傳該怎麼做呢?

View

在 View 的表單中放了三個 File Input Tag,各自的 id 都不同,id 對於傳到後端 controller 並不會有影響,有影響的是 name,所以三個 File Input Tag 的 name 都是一樣的,

image

Controller

後端 controller Action 方法的接收參數的型別使用的是 IEnumerable<HttpPostedFileBase>,跟上傳單一個檔案一樣,後端 action 方法接收的參數名稱要跟 View 表單中上傳檔案欄位所使用的 name 一致,

image

而 Action 方法內在以 foreach 方式一一的對上傳檔案做處理

image

觀察上傳檔案的執行

我們在 View 有放上三個 File Input Tag,但是故意第二個上傳檔案的欄位不選擇檔案,所以可以在下圖裡看到在 files 的第二個 HttpPostedFileBase 為 null,

image

image

 


為什麼是用 HttpPostedFileBase 而不是從 FormCollection 取得上傳的檔案

首先要先認識一下 FormCollection:MSDN - FormCollection 類別

FormCollection 類別是繼承 NameValueCollection 類別,是以索引鍵或是所引來存取值,而前端表單有其他 input tag 時,當 POST 表單資料到後端的 Action 方法,我們在 FormCollection 是使用 Input Tag 欄位的 Name 來取得相對應的值,而雖然 File Input 也是存在於表單當中,卻無法在 FormCollection 中取得上傳檔案的值,如下所示,

image

MSDN - NameValueConnection 類別

我前面有講到 FormCollection 是繼承 NameValueCollection 類別,而 NameValueCollection 是 String 索引鍵和 String 值的集合,表單裡所上傳的檔案並不是 String,而是檔案,不同於表單其它 input tag 的內容值可以用 String 表示。

image

在 ASP.NET MVC 裡對於檔案上傳有另外預設的 value provider「HttpFileCollectionValueProvider」,

MSDN - HttpFileCollectionValueProvider 類別

這個不太好解釋,當網頁含有檔案上傳的表單 POST 到 Controller 時,有關檔案的處理會交由 HttpFileCollectionValueProvider,而這個 Value Provider 內就會從 ControllerContext 的 HttpContext Request File 裡去找檔案,當有找到檔案後就會把檔案以 HttpPostedFileBase 類別放到 HttpPostedFileBase[] 裡,最後再回到 Controller Action 方法,接著我們再去取得檔案,關於這一段可以直接看以下的原始檔內容:

// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace System.Web.Mvc
{
    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);
        }
    }
}

所以當檔案上傳後在 Controller 的 Action 方法去處理時,就必須要使用 HttpPostedFileBase 來取得檔案;有關以上 HttpFileCollectionValueProvider 的原始碼以及其他類別原始碼有興趣的朋友可以自行去下載 ASP.NET MVC 原始碼來研究。

 


這篇先介紹簡單的單一檔案與多個檔案上傳的應用操作,並且在後面說明了為何在 FormCollection 裡無法取得上傳檔案的值而為什麼要使用 HttpPostedFileBase 才能取得上傳檔案的原因,希望能夠幫助對此有疑問的朋友。

 

參考連結:

Phil Haack - Uploading a File (Or Files) With ASP.NET MVC

延伸連結:

CodePlex -  ASP.NET WebStack (下載 ASP.NET MVC 4 原始碼)

 

以上

沒有留言:

張貼留言

提醒

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

最近的留言