2014年6月26日 星期四

ASP.NET MVC 匯入 Excel 簡單做 - Part.1 檔案上傳

做專案開發的時候常常會碰到這樣需求,就是 Excel 檔案的匯入與匯出,而做法的難易度與複雜度都會因為所要處理的匯出入資料格式與內容而異,所以在市場上就有許多的套件與解決方案,功能強大而且支援多樣性與客製化處理與操作的套件往往需要很高的授權費用,不過一分錢一分貨,但是唯有一句話永遠是真理……「免費的最貴」。

因為不想花錢買第三方套件來解決,所以就用土法煉鋼的方式來兜出一個解決方法,通常都要耗費相當多的時間,而且很多方法都是 Google Copy Paste 而來的,所以就會常常出現問題。

那麼這一篇也是一樣,看來也會是讓人 Google Copy Paste 的一篇文章,不過這一篇只會提供簡單做法的解決方式,針對單一個 Excel 檔案的單一 WorkSheet,然後匯入單一的 Table 裡,希望大家能夠了解做一個 Excel 匯入功能的基本流程與方法。

 


進入主題就不再廢話多說,要做一個 Excel 匯入功能會有以下幾個功能:檔案上傳、讀取 Excel 資料,存入資料庫表格。

有關讀取 Excel 資料的功能將會使用「Linq to Excel」,相關資訊可以在以下的連結裡做更進一步的了解:

Linq to Excel

NuGet Gallery | LinqToExcel

讀取 Excel 你還在用 NPOI 嗎?快來試試 LinqToExcel | demo小鋪

 

還有一個功能就是檔案上傳,文章範例所使用的是 ASP.NET MVC 5 的網站預設範本,所以前端 UI 介面就是使用 Bootstrap,而有關檔案上傳的使用者介面,我會使用 Jasny Bootstrap 裡所提供的 Upload 功能與介面,這是一套基於 Twitter Bootstrap 的擴充功能,所以還是在網站裡會使用到 Bootstrap,

Jasny Bootstrap

Jasny Bootstrap - File Input

 

那麼在 ASP.NET MVC 的檔案上傳就會使用我之前在部落格所發表的文章裡所介紹的方式,

mrkt 的程式學習筆記: ASP.NET MVC - 檔案上傳的基本操作

mrkt 的程式學習筆記: ASP.NET MVC - 使用 jQuery Form Plugin 做檔案上傳

 

那麼要上傳的 Excel 資料,我這邊是使用台灣各縣市、鄉鎮市區的郵遞區號,

image

 

開發環境:Visual Studio 2013 Update2, LocalDB, ASP.NET MVC 5.1, ADO.NET Entity Framework 6.1

 

建立網站專案

建立好一個基本的 ASP.NET MVC 5 網站專案

image

增加 LocalDB,並且新增一個 TaiwanZipCode 的表格與欄位

image

再來新增 ADO.NET 實體資料模型(Database First)

image

 

新增 ZipCodeController.cs,因為全部的郵遞區號資料也有三百多筆,所以加入 PagedList.Mvc 套件,

image

新增 ZipCode 的 Index.cshtml

image

 

檔案上傳

接著我們要做檔案上傳的功能,在前端會使用到一開始有說過 Jasny Bootstrap 與 jQuery Form Plugin,所以分別前往網站然後下載相關檔案,

Jasny Bootstrap

jQuery Form Plugin

image

加入 Jasny Bootstrap 之後要修改 BundleConfig.cs 的內容

image

檔案上傳之後要存在某個位置,所以在專案裡建立一個「FileUpload」資料夾,並且在 Web.Config 的 AppSettings 裡新增設定,

image

再來我們要將檔案上傳的使用者介面用 Bootatrsp Modal 的方式呈現,所以把檔案上傳的部份另外建立一個 Partial View 來放置,讓 Index.cshtml 可以單純一點,然後 Partila View 裡的檔案上傳為一個 Form,檔案上傳所使用的 Action 方法為 Upload,

image

_UploadFile.cshtml

<!-- UploadModal -->
<div class="modal fade" id="UploadModal" tabindex="-1" role="dialog" aria-labelledby="UploadModalLabel" aria-hidden="true">
    <form id="UploadForm" action="@Url.Action("Upload", "ZipCode")" method="post" enctype="multipart/form-data">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
                    <h4 class="modal-title" id="UploadModalLabel">上傳並匯入資料</h4>
                </div>
                <div class="modal-body">
 
                    <div class="fileinput fileinput-new input-group" data-provides="fileinput">
                        <div class="form-control" data-trigger="fileinput">
                            <i class="glyphicon glyphicon-file fileinput-exists"></i>
                            <span class="fileinput-filename"></span>
                        </div>
                        <span class="input-group-addon btn btn-default btn-file">
                            <span class="fileinput-new">選擇檔案</span>
                            <span class="fileinput-exists">更換檔案</span>
                            <input type="file" name="file" id="file" />
                        </span>
                        <a href="#" class="input-group-addon btn btn-default fileinput-exists" data-dismiss="fileinput">移除檔案</a>
                    </div>
 
 
                </div>
                <div class="modal-footer">
                    <input type="button" id="ButtonCancel" class="btn btn-default" data-dismiss="modal" value="取消">
                    <input type="submit" id="ButtonSubmit" name="ButtonSubmit" class="btn btn-primary" value="上傳檔案">
                </div>
            </div>
        </div>
    </form>
</div>

接著回過頭來修改 Index.cshtml 的內容

image

在 Index.cshtml 的最下面加入 Partial View「_UploadFile.cshtml」的使用

image

前端的工程還沒完成,不過先跳到 ZipCodeController 裡建立檔案上傳的 Server 端的程式,

private string fileSavedPath = WebConfigurationManager.AppSettings["UploadPath"];
 
[HttpPost]
public ActionResult Upload(HttpPostedFileBase file)
{
    JObject jo = new JObject();
    string result = string.Empty;
 
    if (file == null)
    {
        jo.Add("Result", false);
        jo.Add("Msg", "請上傳檔案!");
        result = JsonConvert.SerializeObject(jo);
        return Content(result, "application/json");
    }
    if (file.ContentLength <= 0)
    {
        jo.Add("Result", false);
        jo.Add("Msg", "請上傳正確的檔案.");
        result = JsonConvert.SerializeObject(jo);
        return Content(result, "application/json");
    }
 
    string fileExtName = Path.GetExtension(file.FileName).ToLower();
 
    if (!fileExtName.Equals(".xls", StringComparison.OrdinalIgnoreCase)
        &&
        !fileExtName.Equals(".xlsx", StringComparison.OrdinalIgnoreCase))
    {
        jo.Add("Result", false);
        jo.Add("Msg", "請上傳 .xls 或 .xlsx 格式的檔案");
        result = JsonConvert.SerializeObject(jo);
        return Content(result, "application/json");
    }
 
    try
    {
        var uploadResult = this.FileUploadHandler(file);
 
        jo.Add("Result", !string.IsNullOrWhiteSpace(uploadResult));
        jo.Add("Msg", !string.IsNullOrWhiteSpace(uploadResult) ? uploadResult : "");
 
        result = JsonConvert.SerializeObject(jo);
    }
    catch (Exception ex)
    {
        jo.Add("Result", false);
        jo.Add("Msg", ex.Message);
        result = JsonConvert.SerializeObject(jo);
    }
    return Content(result, "application/json");
}
 
/// <summary>
/// Files the upload handler.
/// </summary>
/// <param name="file">The file.</param>
/// <returns></returns>
/// <exception cref="System.ArgumentNullException">file;上傳失敗:沒有檔案!</exception>
/// <exception cref="System.InvalidOperationException">上傳失敗:檔案沒有內容!</exception>
private string FileUploadHandler(HttpPostedFileBase file)
{
    string result;
 
    if (file == null)
    {
        throw new ArgumentNullException("file", "上傳失敗:沒有檔案!");
    }
    if (file.ContentLength <= 0)
    {
        throw new InvalidOperationException("上傳失敗:檔案沒有內容!");
    }
 
    try
    {
        string virtualBaseFilePath = Url.Content(fileSavedPath);
        string filePath = HttpContext.Server.MapPath(virtualBaseFilePath);
 
        if (!Directory.Exists(filePath))
        {
            Directory.CreateDirectory(filePath);
        }
 
        string newFileName = string.Concat(
            DateTime.Now.ToString("yyyyMMddHHmmssfff"),
            Path.GetExtension(file.FileName).ToLower());
 
        string fullFilePath = Path.Combine(Server.MapPath(fileSavedPath), newFileName);
        file.SaveAs(fullFilePath);
 
        result = newFileName;
    }
    catch (Exception ex)
    {
        throw;
    }
    return result;
}

完成檔案上傳的 Server 端程式之後,接著再繼續回到前端,在前端程式的部份,我非常不喜歡直接在 View 裡面去寫 Javascript Code,我只會在 View 裡面去寫執行 Javascript 程式的入口,讓 View 的內容可以單純一些,避免前端程式與 View 裡面的 Razor 程式混雜,當前端程式與伺服器端程式分開處理,

image

對了,檔案上傳後的成功或失敗訊息的顯示,我是使用 Bootbox.js 這個套件,然後 project.js, project.extends.js 與 project.ZipCode.js 這三個 Javascript 檔案是我所建立的,

project.ZipCode.js

//================================================================================================
/// <reference path="_references.js" /> Ps.如果你要新增給IntelliSense用的js檔案, 請加在該檔案中
//================================================================================================
 
(function (app) {
    //===========================================================================================
    var current = app.ZipCode = {};
    //===========================================================================================
 
    jQuery.extend(app.ZipCode,
    {
        Initialize: function (actionUrls) {
            /// <summary>
            /// 初始化函式
            /// </summary>
            /// <param name="actionUrls"></param>
 
            jQuery.extend(project.ActionUrls, actionUrls);
 
            //上傳檔案事件處理
            current.UploadEventHandler();
        },
 
        UploadEventHandler: function () {
            /// <summary>
            /// 上傳匯入資料
            /// </summary>
 
            $("#UploadForm").ajaxForm({
                iframe: true,
                dataType: "json",
                success: function (result) {
                    $("#UploadForm").resetForm();
                    if (!result.Result) {
                        project.AlertErrorMessage("錯誤", result.Msg);
                    }
                    else {
                        $('#ResultContent').html(result.Msg);
                        project.ShowMessage("訊息", "檔案上傳完成");
                    }
                },
                error: function (xhr, textStatus, errorThrown) {
                    $("#UploadForm").resetForm();
                    project.AlertErrorMessage("錯誤", "檔案上傳錯誤");
                }
            });
        }
 
    });
})
(project);

 

就目前為止所完成的檔案上傳功能畫面,

image

image

image

image

image

image

image

在檔案上傳完成之後就要進行讀取 Excel 資料並匯入的處理,而這些動作就留待下一篇繼續說明。

下一篇「ASP.NET MVC 匯入 Excel 簡單做 - Part.2 匯入資料

 

以上

16 則留言:

  1. 請問一下為什麼我的訊息是在UPDATE頁面顯示 而且只有字串 EX:{"Result":false,"Msg":"請上傳檔案!"}

    回覆刪除
    回覆
    1. Hello, 你好
      因為我不知道你的程式是如何寫的,所以可以參考我所提供的專案原始碼,
      我在 GitHub 上面有提供完整的專案,以下的文章裡有說明怎麼取得檔案:
      「ASP.NET MVC Excel 匯入與匯出 - 簡單做 @ GitHub」
      http://kevintsengtw.blogspot.tw/2014/06/aspnet-mvc-excel-github.html#.U7-OVfmSwlQ

      刪除
    2. 猜測你是直接把這篇文章的「project.ZipCode.js」程式給直接複製並且建立,
      你可以看看 project.ZipCode.js 程式碼的上方那一張圖片,
      除了 project.ZipCode.js 這個檔案之外,其實還有 project.js 與 project.extends.js 這兩個 js 檔案,
      有關這兩個檔案,可以參考我前面所提到的 GitHub

      刪除
  2. 不好意思請問一下 我實作已可上傳 但程式吃不到project.ZipCode.js檔 雖然非xls檔案無法成功上傳 但無法針對非xls檔案呈現錯誤訊息或上傳成功訊息(已在index.cshtml引入project.ZipCode.js) 該如何解決

    謝謝您詳細的指導

    回覆刪除
    回覆
    1. Hello, 你好
      因為你所提供的訊息並無法讓我知道你所做的程式內容(簡言之,我看不到你做的東西),
      不過我有提供這個範例最後完成的完整專案原始檔,所以就請你下載原始檔之後再自行比對,
      專案原始檔是可以執行的。

      「ASP.NET MVC Excel 匯入與匯出 - 簡單做 @ GitHub」
      http://kevintsengtw.blogspot.tw/2014/06/aspnet-mvc-excel-github.html#.VECECPmSweQ

      刪除
    2. 您好:
      我是完全照您上面的教學步驟做的,實作出來的結果和上面那位大大一樣,似乎出來的結果只是一個json檔案呈現的錯誤訊息{"Result":false,"Msg":"請上傳檔案!"}無法呈現文章倒數第五張圖片呈現的訊息,那樣的訊息呈現是否和bootbox.js有關?
      如何讓該上傳頁面送出後reflash到倒數第五張圖片的呈現結果。
      我也比對您的ASP.NET MVC Excel 匯入與匯出 - 簡單做 @ GitHub 程式碼完全一樣

      刪除
    3. 除了 project.ZipCode.js 之外,也還有 project.js 和 project.extends.js 兩個檔案也需要,
      project.AlertErrorMessage() 與 project.ShowMessage() 這兩個都是再將 bootbox 包裝後的 function,
      其實是很簡單的訊息顯示功能,如果真的無法正常顯示訊息的話,其實就直接使用 javascript 原本的 alrert function 就可以。

      要還是將放在 GitHub 上面的原始碼抓下來執行一次並且做比對,因為文章不可能將所有專案裡的程式都拿出來逐一說明,
      所以各位所遇到的應該就是少了文章所沒有提到的 project.js 與 project.extends.js 這兩個檔案,
      所以再檢視我所提供的原始檔內容,並且直接執行我的原始檔。
      https://github.com/kevintsengtw/MVC-Excel-Import-Export

      刪除
    4. 好的,謝謝您
      我有將project.js 和 project.extends.js 等相關檔案都從您的專案中複製進來,無奈依舊無法成功,可惜無法實作出這個漂亮的功能,只好改以 javascript 的方式來呈現。

      謝謝您所提供的資訊

      刪除
    5. 我記得好像我當初寫完這系列文章之後,bootbox 有更新了版本,好像我原來在 project.js 所包裝的用法就無法是用於新版本,試試看將我原始碼的 bootbox.js 與相關的 css 檔案複製一份到你所建立的專案,應該就可以解決。
      其實根本的解決知道就是要請你自行修改 project.js 裡面對於 bootbox 所包裝的方法,或者就是直接在 project.ZipCode.js 裡使用 bootbox 所提供的原始方法。
      只是有關 alert 的一些 jQuery plugin 有很多,別執著於 bootbox,例如 SweetAlert 也是一個蠻漂亮的 alert 套件「https://github.com/t4t5/sweetalert」,或是可以看看其他的相關套件也可以「http://jquery-plugins.net/tag/alert」

      刪除
  3. 請問一下~我把您的 Code 佈署到 IIS 上面,透過別台電腦開起網頁(IE/google/firefox都一樣)會出現"錯誤。處理您的要求時發生錯誤。",這大概會是什麼原因造成的?或是您有沒有建議的 IIS 佈署 MVC 的範例可以介紹一下,再麻煩您了,感謝。

    回覆刪除
  4. 環境狀況如下:
    SQL Server 2012
    DB資料連結已經都設定好。
    Windows Server 2012 R2
    啟動 iis,設定基本上沒有特別調整,
    驗證調整為 ASP.NET 模擬:開啓、Windows驗證:開啟。
    在 iis 本機上是可以正常執行的,但透過網路連線就會出現錯誤。
    不知道 iis 是否有哪裡需要為 MVC 做特別的設定。

    回覆刪除
  5. Windows Server 2012R2
    SQL Server 2012
    Visual studio 2015
    DB連結已經都設定好,iis 只有設定驗證:Asp.net模擬、windows驗證、匿名關閉,其餘的沒有特別設定。
    在 iis 本機上面可以正常運行,透過網路就會出現錯誤訊息。

    回覆刪除
    回覆
    1. 想辦法去 log 錯誤,看看錯誤的詳細地方與原因為何

      刪除
    2. 後續的討論請使用左方的「詢問與建議」將你的問題與詳細錯誤內容或相關程式提供給我

      刪除
    3. 看看是不是這個問題
      http://blog.miniasp.com/post/2014/05/10/Could-not-load-file-or-assembly-LinqToExcel-or-one-of-its-dependencies.aspx

      刪除
    4. https://dotblogs.com.tw/maduka/archive/2011/09/14/36150.aspx

      刪除

提醒

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