2014年9月29日 星期一

ASP.NET MVC 匯出 Excel - 讓使用者挑選要匯出的資料欄位 Part.1

這一篇會用簡單且直接的方式來處理這個「讓使用者挑選要匯出的資料欄位」功能需求,先以簡單的方式來做出這個功能,之所以會說直接則是沒有經過太多的進階處理,只有稍微使用一些技巧與方式,把這一個功能需求給做出來,之後再進而修改並做出一個比較進階的解決方法。

 


要讓使用者在前端的檢視頁面裡讓他們可以自己挑選要匯出的資料欄位,首先就是要整理出有哪些資料是可以被匯出的,這邊先不用想得太深、太進階,我以簡單的方式來處理,要讓使用者能夠理解欄位名稱,最好的做法就是直接顯示欄位的描述,畢竟直接以 Customer 類別的屬性名稱顯示給使用者看,使用者一定不會明白的,所以要有個欄位名稱與欄位描述的對應,所以我這邊就先以 Dictionary<string, string> 來做處理。

建立一個 ExportDataHelper 類別,主要用來處理匯出資料欄位的操作,

image

在 CustomerController 的 Index action 方法內,增加取得 Customer 匯出資料欄位的內容,然後使用 SelectListItem 做為容器,最後放到 ViewBag 裡提供給 View 來使用,

image

前端的部分,原本給使用者自行輸入匯出檔案名稱的地方,在下方增加匯出資料欄位的顯示,

image

其實這個 ExportData Dialog 的區塊已經佔了 Index.cshtml 一大部分,建議是可以將這一個部分抽出來並且轉為 Partial View,

~/Views/Customer/_ExportDataDialog.cshtml

<div id="ExportDataDialog" class="modal fade" tabindex="-1" data-width="600px" data-height="500px" style="display: none;">
    <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
        <h4 id="ExportDataDialog">匯出會員資料</h4>
    </div>
    <div class="modal-body" style="margin-left: 5px">
        <div class="row">
            <div class="form-group">
                <div class="col-lg-12">
                    <label for="ExportFileName">匯出資料檔名(可不填)</label>
                    <input type="text" id="ExportFileName" name="ExportFileName" class="form-control" placeholder="可輸入中文, 不含副檔名." />
                </div>
            </div>
        </div>
        <div class="row">
            <div class="form-group">
                <div class="col-lg-12">
                    <label>
                        <strong>匯出欄位</strong> (
                        <a id="SelectAllColumns" style="cursor: pointer;">
                            <span class="glyphicon glyphicon-ok-sign"></span> 選取全部欄位
                        </a>
                        <a id="UnselectAllColumns" style="cursor: pointer;">
                            <span class=" glyphicon glyphicon-remove-sign"></span> 不選取全部欄位
                        </a> )
                    </label>
                </div>
            </div>
        </div>
        <div class="row">
            <div class="col-lg-12">
                <div class="well">
                    @foreach (var item in (List<SelectListItem>)ViewBag.ExportColumns)
                    {
                        <div class="form-group">
                            <label class="checkbox">
                                <input type="checkbox" id="Checkbox_ExportColumns" name="Checkbox_ExportColumns" value="@(item.Value)" checked="checked" />
                                @(item.Text)
                            </label>
                        </div>
                    }
                </div>
            </div>
        </div>
    </div>
    <div class="modal-footer">
        <button id="ButtonExecuteExport" class="btn btn-primary">匯出資料</button>
        <button id="ButtonCancel" class="btn" data-dismiss="modal" aria-hidden="true">取消</button>
    </div>
</div>

~/Views/Customer/Index.cshtml

@model IEnumerable<BlogDemo.Models.Customer>
 
@{
    ViewBag.Title = "Customer - List";
}
 
<h2>Customer - List</h2>
 
<div class="well">
    <button class="btn btn-primary" id="ButtonExport" name="ButtonExport">
        匯出資料
    </button>
</div>
 
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.CompanyName)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.ContactName)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.ContactTitle)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Address)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.City)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Region)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.PostalCode)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Country)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Phone)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Fax)
        </th>
    </tr>
 
    @foreach (var item in Model)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.CompanyName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ContactName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ContactTitle)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Address)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.City)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Region)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.PostalCode)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Country)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Phone)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Fax)
            </td>
        </tr>
    }
 
</table>
 
<!-- ExportData Dialog -->     
 @Html.Partial("_ExportDataDialog")
<!-- ExportData Dialog --> 
 
@section scripts
{
    <script src="~/Scripts/Customer-Export.js"></script>
}

以下是到目前為止的檔案與目錄結構內容

image

顯示匯出資料欄位的結果,預設是全部的欄位都為選取的狀態,

image

接下來是前端頁面的操作處理,先針對「選取全部欄位」「不選取全部欄位」以及取得已選取的匯出資料項目,最後就是要再將已選取的匯出欄位給傳回後端,

~/Scripts/Customer-Export.js

;
(function (windows) {
    if (typeof (jQuery) === 'undefined') { alert('jQuery Library NotFound.'); return; }
 
    var HasData = 'False';
 
    $(function () {
 
        //顯示匯出選項視窗
        $('#ButtonExport').click(function () {
            $('#ExportDataDialog').modal('show');
        });
 
        $('#SelectAllColumns').unbind('click').click(function () {
            //選取全部欄位
            $('input:checkbox[name=Checkbox_ExportColumns]').prop('checked', 'checked');
        });
 
        $('#UnselectAllColumns').unbind('click').click(function () {
            //不選取全部欄位
            $('input:checkbox[name=Checkbox_ExportColumns]').removeAttr('checked');
        });
 
        //匯出資料
        $('#ButtonExecuteExport').click(function () {
            //匯出 Excel 檔名
            var exportFileName = $.trim($('#ExportFileName').val());
 
            //匯出的資料欄位
            var selectedColumns = $('input:checkbox[name=Checkbox_ExportColumns]:checked').map(function () {
                return $(this).val();
            }).get().join(',');
 
            if (selectedColumns.length == 0) {
                alert("必須選取匯出資料的欄位.");
                return false;
            }
 
            ExportData(exportFileName, selectedColumns);
        });
 
    });
 
    function ExportData(exportFileName, selectedColumns) {
        /// <summary>
        /// 資料匯出
        /// </summary>
 
        $.ajax({
            type: 'post',
            url: Router.action('Customer', 'HasData'),
            dataType: 'json',
            cache: false,
            async: false,
            success: function (data) {
                if (data.Msg) {
                    HasData = data.Msg;
                    if (HasData == 'False') {
                        alert("尚未建立任何資料, 無法匯出資料.");
                    }
                    else {
                        window.location = exportFileName.length == 0
                            ? Router.action('Customer', 'Export', { selectedColumns: selectedColumns })
                            : Router.action('Customer', 'Export', { fileName: exportFileName, selectedColumns: selectedColumns });
 
                        $('#ExportFileName').val('');
                        $('#ExportDataDialog').modal('hide');
                    }
                }
            },
            error: function (xhr, textStatus, errorThrown) {
                alert("資料匯出錯誤");
            }
        });
    }
})
(window);

 

CustomerController.cs

回到 CustomerController 的 Export action,多了一個參數「selectedColumns」這是要接那些已選取的匯出欄位值,而我們在後端處理資料的匯出時,從資料庫裡取出的資料還是取得全部欄位的資料內容,只是最後要匯出為 Excel 前再去把沒有被選取的欄位資料給移除,

image

 

ExportDataHelper - GetRemoveColumnNames

取得未被選取的欄位,這些未被選取的欄位資料將會先被移除之後再匯出為 Excel,

image

 

ExportExcelResult.cs

而先前已經建立好的 ExportExcelResult 類別也要增加移除未被選取欄位的資料處理,

using System.Collections.Generic;
using ClosedXML.Excel;
using System;
using System.Data;
using System.IO;
using System.Text;
using System.Web;
using System.Web.Mvc;
 
namespace BlogDemo.Infrastructure.ActionResults
{
    public class ExportExcelResult : ActionResult
    {
        public string SheetName { get; set; }
 
        public string FileName { get; set; }
 
        public DataTable ExportData { get; set; }
 
        public List<string> RemoveColumnNames { get; set; }
 
        public ExportExcelResult()
        {
 
        }
 
        public override void ExecuteResult(ControllerContext context)
        {
            if (ExportData == null)
            {
                throw new InvalidDataException("ExportData");
            }
            if (string.IsNullOrWhiteSpace(this.SheetName))
            {
                this.SheetName = "Sheet1";
            }
            if (string.IsNullOrWhiteSpace(this.FileName))
            {
                this.FileName = string.Concat(
                    "ExportData_",
                    DateTime.Now.ToString("yyyyMMddHHmmss"),
                    ".xlsx");
            }
 
            if (RemoveColumnNames != null && RemoveColumnNames.Count > 0)
            {
                foreach (var item in this.RemoveColumnNames)
                {
                    this.ExportData.Columns.Remove(item);
                }
            }
 
            this.ExportExcelEventHandler(context);
        }
 
        /// <summary>
        /// Exports the excel event handler.
        /// </summary>
        /// <param name="context">The context.</param>
        private void ExportExcelEventHandler(ControllerContext context)
        {
            try
            {
                var workbook = new XLWorkbook();
 
                if (this.ExportData != null)
                {
                    context.HttpContext.Response.Clear();
 
                    // 編碼
                    context.HttpContext.Response.ContentEncoding = Encoding.UTF8;
 
                    // 設定網頁ContentType
                    context.HttpContext.Response.ContentType =
                        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
 
                    // 匯出檔名
                    var browser = context.HttpContext.Request.Browser.Browser;
                    var exportFileName = browser.Equals("Firefox", StringComparison.OrdinalIgnoreCase)
                        ? this.FileName
                        : HttpUtility.UrlEncode(this.FileName, Encoding.UTF8);
 
                    context.HttpContext.Response.AddHeader(
                        "Content-Disposition",
                        string.Format("attachment;filename={0}", exportFileName));
 
                    // Add all DataTables in the DataSet as a worksheets
                    workbook.Worksheets.Add(this.ExportData, this.SheetName);
 
                    using (var memoryStream = new MemoryStream())
                    {
                        workbook.SaveAs(memoryStream);
                        memoryStream.WriteTo(context.HttpContext.Response.OutputStream);
                        memoryStream.Close();
                    }
                }
                workbook.Dispose();
            }
            catch
            {
                throw;
            }
        }
    }
 
}

 

執行結果

預設狀況為全部欄位都被選取(匯出所有欄位的資料)

image

如果全部欄位都沒有選取,直接按下「匯出資料」,會有訊息提示,

image

選取要匯出的資料欄位

image

匯出結果

image

 

看起來好像已經解決了功能需求的目標,但是這樣的做法合適嗎?

以下面這張圖所示的內容,就可以知道這樣的做法並不恰當,

image

難道系統每增加一個要匯出資料的類別時就要再去新增這個 XxxxxExportColumns 的靜態方法,而這個方法只是要去決定類別裡的那些欄位可以被匯出、以及這些欄位的描述為何,包含到匯出資料的順序等,其實還是可以用一個比較合適的方法來處理,而這一個部分就再下一篇文章裡做說明。

 

以上

3 則留言:

  1. 比較不了解的是 為什麼 "選取全部欄位" 和 "不選取全部欄位" 要先 unbind click 在 click

    回覆刪除
    回覆
    1. 純粹只是為了防止重複 bind 而已
      那一段程式是從我之前的專案抓出來的,因為之前的專案在選取會出資料欄位的部分是用 AJAX 去讀取 Partial View 的內容
      因為曾經出現過重複 bind event 的狀況,所以一進來就先做 unbind 之後再做 bind 處理.

      刪除

提醒

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

最近的留言