ASP.NET MVC - 在 MVC 使用ASP.NET Web Form的方法下載檔案

前言

原文參考:Trigger file downloads with a MVC 3 controller action

在 ASP.NET MVC 中,一般是使用 FileResult 來回傳檔案資料流給 Client,以進行檔案下載等動作。這我們在「ASP.NET MVC - 單一檔案與多檔案上傳及下載管理」最後一部份及「ASP.NET MVC - 檔案下載使用非主鍵,例如Name, GUID」都能清楚看到。

開頭原文是使用 ASP.NET Web Form 的 Response.BinaryWrite() 的方法,就像「透過資料庫上傳下載檔案」的使用一樣,算是ASP.NET Web From + ASP.NET MVC 二合一下載方法,因為原文為 C#,所以我改寫為 Visual Basic。

那為什麼我要特別轉介紹這個 ASP.NET Web Form 下載方法,如何在 ASP.NET MVC 裡使用?原因很簡單,因為我們會碰到一個「阿豆仔」不會碰到的問題,什麼問題?你有沒有下載過「中文檔案名稱」的檔案?例如,「產品規格.pdf」、「全系列產品.xls」、「產品升級說明.doc」…,你可以試試看,在原始 ASP.NET MVC 中使用 FileResult 來讓使用者下載中文檔案名稱的檔案,你會發現檔案名稱除 Firefox 3.6 (註1) 之外,其他瀏覽器都會是亂碼。原因是 ASP.NET MVC 走的規格太新 ( RFC2231 ),瀏覽器來不及實作。不過,現在瀏覽器更新速度很快,就算最近的新版本瀏覽器都已經支援此規格,但我們無法保證使用者的瀏覽器版本,還是會產生亂碼問題。

註1:Will保哥所著:「ASP.NET MVC 2 開發實戰」,第6章第143~148頁,有針對ASP.NET MVC 的中文檔案名稱下載的討論。有興趣者請自行參考。

那怎麼辦,如果你真的有這種需求?那就必須回歸使用 ASP.NET Web Form 的下載方法,這樣我們就能確保下載的檔案名稱不管是中文還是英文都能很正常顯示及下載。不過再提醒,除非萬不得以,我是強烈建議檔案名稱還是使用英文來命名

在 MVC 實作 ASP.NET Web Form 下載

Step 1:我們先在根目錄新增一個 Extensions 目錄,然後新增一個 GetFileData.vb Class。

Imports System.Runtime.CompilerServices 
Imports System.IO 
public Module GetFileData 
    ''' <summary>
    ''' 擴充方法:確認檔案是否存在,存在將檔案讀出回傳
    ''' </summary>
    ''' <param name="fileName">檔案名稱</param>
    ''' <param name="filePath">檔案路徑</param>
    <Extension()>
    Function GetFileData(fileName As String , filePath As String) As Byte()
         
        Dim fullFilePath = String.Format("{0}/{1}", filePath, fileName )
        If Not System.IO.File.Exists(fullFilePath ) then 
            Throw New FileNotFoundException("檔案不存在!", fileName )
        End If

        Return System.IO.File.ReadAllBytes(fullFilePath)
    End Function
End Module

Step 2:在 Extensions 目錄中新增一個 FileDownloadResult.vb 類別,這裡我們有個重大任務,我們自己寫一個回傳類別,以 ASP.NET Web Form 的方法啟動檔案下載的動作。

''' <summary>
''' Inherits ContentResult, then Overrides ExecuteResult
''' </summary>
Public Class FileDownloadResult
    Inherits ContentResult

    Private _fileName As String
    Private _fileData As Byte()

    Sub New(fileName As String, fileData As Byte())
        _fileName = fileName
        _fileData = fileData
    End Sub

    ''' <summary>
    ''' 覆寫執行結果( ExecuteResult )
    ''' </summary>
    Public Overrides Sub ExecuteResult(context As System.Web.Mvc.ControllerContext)
        If String.IsNullOrEmpty(Me._fileName) Then
            Throw New Exception("需要檔案名稱!")
        End If

        If Me._fileData Is Nothing Then
            Throw New Exception("檔案必須有內容!")
        End If

        ' 使用 ASP.NET Web Form 方法進行檔案資料流輸出
        Dim contentDisposition = String.Format("attachment; filename={0}", Me._fileName)
        ' 加入 AddHeader,需要指定 Content-Disposition, 重點是檔案名稱
        context.HttpContext.Response.AddHeader("Content-Disposition", contentDisposition)
        ' 指定MIME,這個地方可以加強,可依檔案類型給正確的MIME。
        ' 為尊重原作者,我就不修改了。
        ContentType = "application/force-download"
        ' 輸出檔案
        context.HttpContext.Response.BinaryWrite(Me._fileData)
    End Sub
End Class

這裡我們也學到可以繼承 ActionResult 的類別,然後修改成自己想要功能。

Step 3:新增一個 Controller \ FilesController.vb 來進行檔案搜尋及下載的動作。

Public Class filesController
    Inherits System.Web.Mvc.Controller

    ''' <summary>
    ''' 注意,回傳類別是FileDownloadResult
    ''' </summary>
    ''' <param name="file">檔案名稱</param>
    Function Download(file As String) As FileDownloadResult
        Try
            ' file.GetFileData() 是我們前面撰寫的擴充方法
            ' 確認檔案是否存在,存在,將檔案讀出回傳
             ' 我們的目錄設定在根目錄下的 ~/Files
            Dim fileData = file.GetFileData(Server.MapPath("~/Files"))
            ' 進行檔案下載的動作
            ' 因為會觸發我們覆寫 ExecuteResult 方法
            Return New FileDownloadResult(file, fileData)
        Catch ex As Exception
            Throw New HttpException(404, "找不到檔案!")
        End Try
    End Function
End Class

Step 4:在相關 View ,撰寫測試下載的 ActionLink()

我們的目錄設定在根目錄下的 ~/Files,請自行準備好相關測試檔案。

<div>
    <ul>
        <li><%: Html.ActionLink("Sales", "Download", New With {.Controller = "Files", .file="Sales.7z"}) %></li>
        <li><%: Html.ActionLink("產品型錄", "Download", New With {.Controller = "Files", .file="產品型錄.7z"}) %></li>
        <li><%: Html.ActionLink("Opps!", "Download", New With {.Controller = "Files", .file="Opps.7z"}) %></li>
        <li><%: Html.ActionLink("產品規格", "Download", New With {.Controller = "Files", .file="產品規格.7z"}) %></li>
    </ul>
</div>

結論

你可以參考前面「ASP.NET MVC - 單一檔案與多檔案上傳及下載管理」來修改此範例,增加資料庫管理的功能,那會更棒。或是提供一個「中介」的 Action 去判斷使用者的瀏覽器,如果是支援 RFC2231 瀏覽器 ( 這部份就留給各位去測試,或是有人測試願意回報給我,我也會非常感謝 ),就使用ASP.NET MVC 的 FileResult 去下載,不然就使用 ASP.NET Web Form 的方法去下載。

如果你在讓使用者下載檔案時,需要處理中文檔案名稱的問題,才會建議使用 ASP.NET Web Form 的方法,這也讓我們知道每個東西都有優點,不是最新就是最好,合適的地方用合適的方法,適才適用最重要。

透過幾篇的討論,現在我們使用 ASP.NET MVC 不管是上傳、下載、中文檔案名稱、英文檔案名稱,通通不是問題了,你應該可以很輕鬆處理。

參考

沒有留言:

張貼留言

感謝您的留言,如果我的文章你喜歡或對你有幫助,按個「讚」或「分享」它,我會很高興的。