ASP.NET MVC Export CSV (StringBuilder & DataTables)

2022-02-08

說明 ASP.NET MVC 如何將各式資料來源匯出為 CSV 格式資料,比較 StringBuilder 建立字串的原生 C# 解決方案以及使用 DataTables 的 Zero Dev 解決方案 🙂

logo

StringBuilder

匯出為 CSV 使用的是 StringBuilder 組合字串的方式。

首先是從 DBContext 取得相關的資料,可以搭配 Select 的方式篩選特定的資料欄位。

var data = db.Customer
    .Select(i => new
    {
        i.FirstName,
        i.LastName,
        i.CompanyName
    })
    .ToList();

接著透過 StringBuilder 來建構 CSV 內容,藉由 Reflection 的方式可以迭代所有的 Row 匿名類型的屬性,免去逐一輸入的困擾。

using System.Reflection;
using System.Text;

StringBuilder sb = new StringBuilder();
sb.AppendLine("FirstName,LastName,CompanyName");

foreach (var row in data)
{
    foreach (PropertyInfo prop in row.GetType().GetProperties())
    {
        var propertyValue = prop.GetValue(row, null);

        var cellValue =
            propertyValue == null ?
                "," : "\"" + prop.GetValue(row, null).ToString() + "\"" + ',';

        sb.Append(cellValue);
    }
    sb.Append("\r\n");
}

使用 File 的方式回傳 ActionResult,其中 StringBuilder 的內容為 Bytes 必須藉由 Encoding.GetBytes 來進行編碼。

這邊選擇 Default 的編碼方式,會預設回應 ANSI 編碼的 CSV,如果希望回應 UTF-8 編碼的 CSV 可以在另行調整。但 Excel 預設會以 ANSI 的方式解碼,會先看到亂碼必須要再自行調整編碼方式,稍微不方便。

return File(
        Encoding.Default.GetBytes(
            sb.ToString()),
            "text/csv",
            string.Format("Export-{0}.csv", DateTime.Now.ToString("yyyyMMdd-HHmmss")
        ));

DataTables.js

使用 DataTables 的方式僅需要藉由 JS Object 來設定 Library 使用就可以輕鬆進行資料的匯出,除了 CSV 外,Excel、PDF 或者是 Print 的方式都支援,同時提供使用者自行篩選資料的功能,能夠減少許多繁瑣的開發工作。

需要安裝的 Library,使用 cdnjs 搭配 libman.json 進行管理。

libman.json

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "[email protected]",
      "destination": "lib/datatables.net/"
    },
    {
      "library": "[email protected]",
      "destination": "lib/datatables.net-bs/"
    },
    {
      "library": "[email protected]",
      "destination": "lib/jszip/"
    },
    {
      "library": "[email protected]",
      "destination": "lib/pdfmake/"
    },
    {
      "library": "[email protected]",
      "destination": "lib/datatables-buttons/"
    },
    {
      "library": "[email protected]",
      "destination": "lib/datatables.net-buttons-bs/"
    },
    {
      "library": "[email protected]",
      "destination": "lib/datatables.net-buttons-dt",
      "files": [
        "buttons.dataTables.css",
        "buttons.dataTables.min.css"
      ]
    }
  ]
}

view.cshtml / style sections

<link href="~/lib/datatables.net-bs/dataTables.bootstrap.css" rel="stylesheet" />
<link href="~/lib/datatables.net-buttons-dt/buttons.dataTables.min.css" rel="stylesheet" />

<style>
    .dataTables_info {
        float: left;
    }

    table.dataTable {
        margin-top: 2rem !important;
        margin-bottom: 2rem !important;
    }
</style>

view.cshtml

<h2>DataTables</h2>

<table class="table table-bordered" id="dataTable">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.FirstName)
            </th>
            ...
        </tr>
    </thead>
    <tbody>
    @foreach (var item in Model)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.FirstName)
            </td>
            ...
        </tr>
    }
    </tbody>
</table>

view.cshtml / section scripts

@section scripts {
    ...
}

下列的 Script 都是放在 section scripts 當中:

@* DataTables Cores *@
<script src="~/lib/datatables.net/jquery.dataTables.min.js"></script>
<script src="~/lib/datatables.net-bs/dataTables.bootstrap.min.js"></script>

@* DataTables Buttons *@
<script src="~/lib/datatables-buttons/js/dataTables.buttons.min.js"></script>
<script src="~/lib/datatables.net-buttons-bs/buttons.bootstrap.min.js"></script>

@* Basic *@
<script src="~/lib/datatables-buttons/js/buttons.html5.min.js"></script>

@* Excel *@
<script src="~/lib/jszip/jszip.js"></script>

@* Print *@
<script src="~/lib/datatables-buttons/js/buttons.print.min.js"></script>

@* PDF *@
<script src="~/lib/pdfmake/pdfmake.min.js"></script>
<script src="~/lib/pdfmake/vfs_fonts.min.js"></script>

關於 DataTables Init 上實際的呼叫函式。

$(document).ready(function () {
    $('#dataTable').DataTable({
        "searching": true,
        "pageLength": 50,
        "bLengthChange": false,
        "order": [[0, "desc"]], // Default Order
        "columnDefs": [
            { "targets": 0, "orderable": true },
            { "targets": 1, "orderable": false },
            { "targets": 1, "searchable": false },
        ],
        "language": {
            "lengthMenu": "Display _MENU_ records per page",
            "zeroRecords": "沒有任何查詢結果,請調整搜尋條件後再執行。",
            "info": "搜尋共有 _TOTAL_ 個項目",
            "infoEmpty": "",
            "infoFiltered": "(filtered from _MAX_ total records)",
        },
        "dom": "Blfrtip",
        "buttons": [
            'copy',
            'excel',
            'csv',
            'pdf',
            'print'
        ]
    });
});

需要注意的 dom 一定要加入在 DataTables 的啟動函式當中,否則 Buttons 不會正常顯示。

關於 DataTables 的 dom 可以參考 dom,關於 DataTables buttons 可以參考 buttons

待解 Bugs

使用 PDF 匯出的文件中文標題無法正常顯示 😥

參考資料

將資料匯出成 CSV

DataTables Export Examples

相關連結

ASP.NET MVC 從無到有打造一個應用系統

Visual Studio 入門教學