寫網頁的同學們應該都有遇過這種需求?只要是矩陣式網頁輸入介面(如下圖),幾乎都會遇到使用者許願:「我能不能先在 Excel 敲好再用匯入的?」

說實在話,如果我是使用者也會覺得這是好用介面的必備條件。網頁介面再怎麼厲害,跟 Excel 永遠不會在同一個量級,加上許多使用者終日與 Excel 為伍,要求他們放著熟練順手的工具不用,硬要在網頁重敲一遍,很難不引起抱怨。

如此實用的需求,硬是推辭不做也說不過去,只會顯得我們功力不足。所幸,借助好用的 ClosedXML程式庫,要在 ASP.NET MVC 寫出 Excel 轉成網頁輸入並不難,這篇文章就來簡單示範一下。

假設網頁如上圖有 4 x 4 個輸入欄位要填寫,我先準備一個 Excel 預先填好內容當實驗材料:

前端程式長這樣:

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Excel 匯入示範</title>
    <style>
        td input {
            width: 80px;
        }
    </style>
</head>
<body>
    <div>
        <div>請輸入資料</div>
        <table>
            <tr>
                <td><input id="C00" /></td>
                <td><input id="C01" /></td>
                <td><input id="C02" /></td>
                <td><input id="C03" /></td>
            </tr>
            <tr>
                <td><input id="C10" /></td>
                <td><input id="C11" /></td>
                <td><input id="C12" /></td>
                <td><input id="C13" /></td>
            </tr>
            <tr>
                <td><input id="C20" /></td>
                <td><input id="C21" /></td>
                <td><input id="C22" /></td>
                <td><input id="C23" /></td>
            </tr>
        </table>
        <form action="@Url.Content("~/Home/ParseAsCsv?callback=parent.fillCsv")" method="POST"
              enctype="multipart/form-data" target="resultFrame">
            <button type="submit" id="btnReadExcel">從Excel匯入</button>
            <input type="file" name="excelFile" />
            <script>
                function fillCsv(csv) {
                    var rows = csv.split('\n');
                    //TODO: 未考慮筆數不吻合的情況
                    for (var r = 0; r < rows.length; r++) {
                        var cells = rows[r].split('\t');
                        for (var c = 0; c < cells.length; c++) {
                            var inp = document.getElementById("C" + r + c);
                            if (inp) inp.value = cells[c];
                        }
                    }
                }
            </script>
        </form>
        <iframe name="resultFrame" style="display:none;"></iframe>
    </div>
</body>
</html>

我在最下方加了一個 <form >,enctype 設 "mutilpar/form-data",搭配 <input type="file" >,走標準的網頁檔案上傳。伺服器端讀取 Excel 檔後用 ClosedXML 解析,將資料轉成 CSV 傳回前端(當然,也可考慮轉成二維字串陣列 JSON,但我個人偏好 CSV,通用性高且較直覺好偵錯),用一小段 JavaScript 解析 CSV 將資料逐欄填入對映欄位。另外,我還用了 form target 指向隱形 iframe 技巧,讓它有 AJAX 更新效果。

後端的程式碼也不複雜,ParseAsCsv Action 接收 IEnumerable<HttpPostedFileBase> 取得 Excel 內容,以 ClosedXML 解析後轉成 CSV (為省略處理內含逗號的情況,我採用 "\t" 分隔,但此處先不考慮欄位內包換行符號的案例),接著將 CSV 字串內容交給前端指定的回呼 JavaScript 函數處理。由於 POST 傳回網頁被包在 iframe,函數名稱前方要加上 parent。

using ClosedXML.Excel;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace Demo.Controllers
{
    public class HomeController : Controller
    {
        // GET: Home
        public ActionResult Index()
        {
            return View();
        }

        /// <summary>
        /// 上傳Excel解析為CSV
        /// </summary>
        /// <param name="excelFile">上傳檔案</param>
        /// <param name="callback">處理結果之 JavaScript 函數名稱</param>
        /// <returns></returns>
        public ActionResult ParseAsCsv(IEnumerable<HttpPostedFileBase> excelFile, string callback)
        {
            try
            {
                if (excelFile == null || excelFile.First() == null) throw new ApplicationException("未選取檔案或檔案上傳失敗");
                if (excelFile.Count() != 1) throw new ApplicationException("請上傳單一檔案");
                var file = excelFile.First();
                if (Path.GetExtension(file.FileName) != ".xlsx") throw new ApplicationException("請使用Excel 2007(.xlsx)格式");
                var stream = file.InputStream;
                XLWorkbook wb = new XLWorkbook(stream);
                if (wb.Worksheets.Count > 1)
                    throw new ApplicationException("Excel檔包含多個工作表");
                var csv = 
                    string.Join("\n",
                        wb.Worksheets.First().RowsUsed().Select(row =>
                            string.Join("\t", 
                                row.Cells(1, row.LastCellUsed(false).Address.ColumnNumber)
                                .Select(cell => cell.GetValue<string>()).ToArray()
                            )).ToArray());
                return Content($@"<script>
{callback}({JsonConvert.SerializeObject(csv)});
</script>", "text/html");
            }
            catch (Exception ex)
            {
                return Content($"<script>alert({JsonConvert.SerializeObject(ex.Message)})</script>", "text/html");
            }
        }
    }
}

廢話不多說,請看示範:

An example of using ClosedXML to import Excel as webpage input fields in ASP.NET MVC.


Comments

Be the first to post a comment

Post a comment