C# 实现数据导出到 Excel 文件功能的实现方案

1. 引言

在日常的 Web 或桌面应用开发中,将数据导出为 Excel 文件是一项非常普遍且实用的需求。无论是生成报表、数据备份,还是为用户提供可离线编辑的数据,Excel 格式因其通用性和易用性而备受青睐。在 C# 开发中,我们常常面临两种典型的数据源:ASP.NET WebForms 中的 GridView 控件 和通用的 DataTable 对象。针对这两种数据源,又有多种技术方案可以实现导出功能。

本文将深入探讨两种主流且高效的实现方案:

  1. WebForms 原生 Response 导出 :利用 ASP.NETHttpResponse 对象,直接将格式化的文本输出为 CSV 或简易的 HTML 表格,并伪装成 Excel 文件。这种方法简单快捷,无需依赖第三方库,适合快速实现和导出结构简单的数据。
  2. 使用 NPOI 库导出标准 xlsx 文件 :NPOI 是一个强大的 .NET 开源库,用于读写 Microsoft Office 格式文件(如 Excel、Word)。使用 NPOI 可以生成标准的 .xlsx 文件,支持复杂的格式设置、多工作表、公式、图表等高级功能,适合对文件格式和样式有较高要求的场景。

我们将分别针对 GridViewDataTable 这两种数据形式,展示如何使用上述两种方案进行实现,并提供完整的、可直接运行的代码示例。

2. 环境准备与项目结构

在开始编码之前,我们需要确保开发环境已就绪。

2.1 开发环境

  • IDE: Visual Studio 2022 或更高版本。
  • .NET 框架: .NET Framework 4.7.2+ 或 .NET Core/.NET 6+。本文示例基于 .NET Framework 的 ASP.NET WebForms 项目,但核心逻辑(尤其是 NPOI 部分)在 .NET Core 中同样适用。
  • 目标项目类型: ASP.NET Web Forms 应用程序(用于演示 GridView 和 Response 导出)。

2.2 安装 NPOI 库

对于使用 NPOI 的方案,需要通过 NuGet 包管理器安装 NPOI。

安装方法(在 Visual Studio 中):

  1. 在解决方案资源管理器中,右键单击您的项目。
  2. 选择"管理 NuGet 程序包..."。
  3. 在"浏览"选项卡中,搜索"NPOI"。
  4. 选择由 dotnetcorenissl-lab 维护的 NPOI 包,点击"安装"。

或者,在程序包管理器控制台中运行以下命令:

powershell 复制代码
Install-Package NPOI

2.3 示例项目结构概览

为了清晰演示,我们假设一个简单的 WebForms 项目结构:

复制代码
ExportToExcelDemo/
├── Default.aspx (包含 GridView 和导出按钮)
├── Default.aspx.cs (代码隐藏文件,包含导出逻辑)
├── Models/
│   └── DataHelper.cs (模拟生成 DataTable 数据)
└── 导出的 Excel 文件将由浏览器直接下载。

3. 方案一:WebForms 原生 Response 导出

此方案的核心是利用 HttpResponse 对象,将数据内容以特定格式(如 CSV)写入输出流,并设置 HTTP 头信息,使浏览器将其识别为 Excel 文件并触发下载。这种方法本质上生成的是文本文件,但 .xls.xlsx 后缀和 Content-Type 头会"欺骗"Excel 打开它。

3.1 导出 GridView 数据

GridView 控件本身具有 RenderControl 方法,可以将其渲染为 HTML。我们可以利用这一点,将 GridView 的 HTML 表格输出为文件。

实现步骤:

  1. 清除当前页面的所有输出。
  2. 设置 ResponseContentType"application/vnd.ms-excel""application/octet-stream"
  3. 设置 Content-Disposition 头,指定下载的文件名。
  4. 将 GridView 控件渲染到 HtmlTextWriter,并写入 Response.Output
  5. 结束响应。

代码示例 (Default.aspx.cs):

csharp 复制代码
using System.IO;
using System.Web.UI;

protected void btnExportGridViewToExcel_Click(object sender, EventArgs e)
{
    // 确保 GridView 已绑定数据
    if (GridView1.Rows.Count > 0)
    {
        // 准备响应
        Response.Clear();
        Response.Buffer = true;
        // 设置文件类型和文件名
        Response.AddHeader("content-disposition", "attachment;filename=GridView_Export.xls");
        Response.Charset = "";
        Response.ContentType = "application/vnd.ms-excel";

        // 使用 StringWriter 和 HtmlTextWriter 来捕获 GridView 的渲染输出
        using (StringWriter sw = new StringWriter())
        {
            using (HtmlTextWriter hw = new HtmlTextWriter(sw))
            {
                // 渲染 GridView 到 HtmlTextWriter
                GridView1.RenderControl(hw);
                // 输出到响应流
                Response.Output.Write(sw.ToString());
            }
        }
        Response.Flush();
        Response.End();
    }
    else
    {
        // 处理无数据的情况
        ClientScript.RegisterStartupScript(this.GetType(), "alert", "alert('没有数据可导出!');", true);
    }
}

// 必须重写此方法,允许页面上的服务器控件在导出时被渲染
public override void VerifyRenderingInServerForm(Control control)
{
    // 必须重写以跳过"控件必须放在具有 runat=server 的窗体标记内"的验证
    // 在导出页面内容时,此验证不必要
}

说明:

  • VerifyRenderingInServerForm 方法的重写是必须的,否则在调用 GridView1.RenderControl 时会引发异常。
  • 此方法生成的文件实际上是 HTML,但扩展名为 .xls。现代版本的 Excel 可以很好地打开和显示这种文件,但可能提示"文件格式与扩展名不匹配"。

3.2 导出 DataTable 数据

对于 DataTable,我们通常将其转换为 CSV (Comma-Separated Values) 格式,这是一种更通用、更轻量的纯文本表格格式。

实现步骤:

  1. 获取或生成 DataTable。
  2. 设置 Response 头信息(同 GridView 导出)。
  3. 遍历 DataTable 的列,输出列名作为 CSV 标题行。
  4. 遍历 DataTable 的行,将每个单元格的值用逗号连接,注意处理包含逗号或换行符的值(通常用双引号包裹)。
  5. 将构建好的 CSV 字符串写入 Response.Output

代码示例 (Default.aspx.cs):

csharp 复制代码
protected void btnExportDataTableToExcel_Click(object sender, EventArgs e)
{
    // 假设有一个获取 DataTable 的方法
    System.Data.DataTable dt = GetSampleDataTable();

    if (dt != null && dt.Rows.Count > 0)
    {
        Response.Clear();
        Response.Buffer = true;
        Response.AddHeader("content-disposition", "attachment;filename=DataTable_Export.csv");
        Response.Charset = "";
        // CSV 通常使用 text/csv,但使用 excel 类型更通用
        Response.ContentType = "application/vnd.ms-excel";

        // 创建 StringWriter 用于构建 CSV 内容
        using (StringWriter sw = new StringWriter())
        {
            // 写入列标题
            for (int i = 0; i < dt.Columns.Count; i++)
            {
                sw.Write(dt.Columns[i].ColumnName);
                if (i < dt.Columns.Count - 1)
                {
                    sw.Write(",");
                }
            }
            sw.Write(sw.NewLine);

            // 写入数据行
            foreach (System.Data.DataRow row in dt.Rows)
            {
                for (int i = 0; i < dt.Columns.Count; i++)
                {
                    string cellValue = row[i].ToString();
                    // 处理可能包含逗号或换行符的值
                    if (cellValue.Contains(",") || cellValue.Contains("\"") || cellValue.Contains("\n"))
                    {
                        cellValue = string.Format("\"{0}\"", cellValue.Replace("\"", "\"\""));
                    }
                    sw.Write(cellValue);
                    if (i < dt.Columns.Count - 1)
                    {
                        sw.Write(",");
                    }
                }
                sw.Write(sw.NewLine);
            }

            // 输出到响应流
            Response.Output.Write(sw.ToString());
        }
        Response.Flush();
        Response.End();
    }
    else
    {
        ClientScript.RegisterStartupScript(this.GetType(), "alert", "alert('没有数据可导出!');", true);
    }
}

private System.Data.DataTable GetSampleDataTable()
{
    System.Data.DataTable dt = new System.Data.DataTable();
    dt.Columns.Add("ID", typeof(int));
    dt.Columns.Add("Name", typeof(string));
    dt.Columns.Add("Email", typeof(string));
    dt.Columns.Add("JoinDate", typeof(DateTime));

    dt.Rows.Add(1, "张三", "zhangsan@example.com", new DateTime(2023, 1, 15));
    dt.Rows.Add(2, "李四", "lisi@example.com", new DateTime(2023, 3, 22));
    dt.Rows.Add(3, "王五", "wangwu@example.com", new DateTime(2023, 5, 10));
    return dt;
}

方案一总结:

  • 优点:实现简单,无需外部依赖,适合快速导出。
  • 缺点:生成的不是真正的 Excel 文件,功能有限(无格式、多工作表等),数据复杂时(如包含特殊字符)容易出错。

4. 方案二:使用 NPOI 导出标准 xlsx 文件

NPOI 提供了完整的 Excel 文件操作能力。我们将使用 NPOI.XSSF.UserModel 命名空间下的类来创建 .xlsx 格式的工作簿。

4.1 导出 GridView 数据

思路是遍历 GridView 的行和单元格,将数据写入 NPOI 的 ISheet 对象。

实现步骤:

  1. 创建 XSSFWorkbook 和工作表 ISheet
  2. 可选:创建样式(如字体、边框、对齐方式)。
  3. 遍历 GridView 的标题行(GridView.HeaderRow),创建 IRowICell,并设置单元格值和样式。
  4. 遍历 GridView 的数据行(GridView.Rows),将每个单元格的文本写入对应的 Excel 单元格。
  5. 可选:遍历 GridView 的脚注行(GridView.FooterRow)。
  6. 将工作簿写入 MemoryStream
  7. 设置 Response 头,将内存流的内容输出。

代码示例 (Default.aspx.cs):

csharp 复制代码
using NPOI.XSSF.UserModel; // 用于 .xlsx
using NPOI.SS.UserModel;
using System.IO;

protected void btnExportGridViewWithNPOI_Click(object sender, EventArgs e)
{
    if (GridView1.Rows.Count > 0)
    {
        // 1. 创建工作簿和工作表
        IWorkbook workbook = new XSSFWorkbook();
        ISheet sheet = workbook.CreateSheet("Sheet1");

        // 2. 可选:创建标题行样式
        ICellStyle headerStyle = workbook.CreateCellStyle();
        IFont headerFont = workbook.CreateFont();
        headerFont.IsBold = true;
        headerStyle.SetFont(headerFont);
        headerStyle.BorderBottom = BorderStyle.Thin;
        headerStyle.BorderLeft = BorderStyle.Thin;
        headerStyle.BorderRight = BorderStyle.Thin;
        headerStyle.BorderTop = BorderStyle.Thin;

        // 3. 创建标题行
        IRow headerRow = sheet.CreateRow(0);
        for (int i = 0; i < GridView1.Columns.Count; i++)
        {
            ICell cell = headerRow.CreateCell(i);
            cell.SetCellValue(GridView1.Columns[i].HeaderText);
            cell.CellStyle = headerStyle;
            // 自动调整列宽(近似)
            sheet.AutoSizeColumn(i);
        }

        // 4. 填充数据行
        for (int i = 0; i < GridView1.Rows.Count; i++)
        {
            IRow dataRow = sheet.CreateRow(i + 1); // +1 因为标题行占用了第0行
            GridViewRow gridViewRow = GridView1.Rows[i];
            for (int j = 0; j < GridView1.Columns.Count; j++)
            {
                // 获取单元格文本。注意:如果 GridView 列是模板字段,可能需要 FindControl
                string cellText = gridViewRow.Cells[j].Text;
                // 清理 HTML 编码(如 &amp;)
                cellText = System.Web.HttpUtility.HtmlDecode(cellText);
                dataRow.CreateCell(j).SetCellValue(cellText);
            }
        }

        // 5. 写入内存流并输出
        using (MemoryStream ms = new MemoryStream())
        {
            workbook.Write(ms);
            Response.Clear();
            Response.Buffer = true;
            Response.AddHeader("content-disposition", "attachment;filename=GridView_NPOI_Export.xlsx");
            Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
            Response.BinaryWrite(ms.ToArray());
        }
        Response.Flush();
        Response.End();
    }
}

4.2 导出 DataTable 数据

这是 NPOI 更自然的用法,因为 DataTable 的结构与 Excel 工作表高度相似。

实现步骤:

  1. 获取 DataTable。
  2. 创建 XSSFWorkbookISheet
  3. 创建标题行,从 DataTable 的 Columns 集合获取列名。
  4. 遍历 DataTable 的 Rows 集合,填充数据。
  5. 处理不同类型的数据(如日期、数字),NPOI 的 SetCellValue 方法有重载可以处理 DateTimedouble
  6. 写入内存流并输出到 Response

代码示例 (Default.aspx.cs):

csharp 复制代码
protected void btnExportDataTableWithNPOI_Click(object sender, EventArgs e)
{
    System.Data.DataTable dt = GetSampleDataTable();

    if (dt != null && dt.Rows.Count > 0)
    {
        IWorkbook workbook = new XSSFWorkbook();
        ISheet sheet = workbook.CreateSheet("Data");

        // 创建标题行
        IRow headerRow = sheet.CreateRow(0);
        for (int i = 0; i < dt.Columns.Count; i++)
        {
            headerRow.CreateCell(i).SetCellValue(dt.Columns[i].ColumnName);
        }

        // 创建数据行
        for (int i = 0; i < dt.Rows.Count; i++)
        {
            IRow dataRow = sheet.CreateRow(i + 1);
            for (int j = 0; j < dt.Columns.Count; j++)
            {
                ICell cell = dataRow.CreateCell(j);
                object cellValue = dt.Rows[i][j];
                // 根据列的数据类型设置单元格值
                if (cellValue is DateTime)
                {
                    cell.SetCellValue((DateTime)cellValue);
                    // 可选:设置日期格式
                    ICellStyle dateStyle = workbook.CreateCellStyle();
                    IDataFormat format = workbook.CreateDataFormat();
                    dateStyle.DataFormat = format.GetFormat("yyyy-mm-dd");
                    cell.CellStyle = dateStyle;
                }
                else if (IsNumeric(cellValue))
                {
                    cell.SetCellValue(Convert.ToDouble(cellValue));
                }
                else
                {
                    cell.SetCellValue(cellValue.ToString());
                }
            }
        }

        // 自动调整列宽
        for (int i = 0; i < dt.Columns.Count; i++)
        {
            sheet.AutoSizeColumn(i);
        }

        using (MemoryStream ms = new MemoryStream())
        {
            workbook.Write(ms);
            Response.Clear();
            Response.Buffer = true;
            Response.AddHeader("content-disposition", "attachment;filename=DataTable_NPOI_Export.xlsx");
            Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
            Response.BinaryWrite(ms.ToArray());
        }
        Response.Flush();
        Response.End();
    }
}

// 辅助方法:判断是否为数值类型
private bool IsNumeric(object value)
{
    return value is int || value is long || value is float || value is double || value is decimal;
}

方案二总结:

  • 优点 :生成真正的、标准的 .xlsx 文件,兼容性好。支持丰富的格式、样式、公式、多工作表等高级功能。数据处理更可靠。
  • 缺点:需要引入第三方库 (NPOI),代码量相对方案一稍多。

5. 方案对比与选择建议

特性 WebForms 原生 Response 导出 NPOI 库导出
文件格式 伪 Excel (HTML/CSV) 标准 Excel (.xlsx/.xls)
依赖项 无,仅需 .NET Framework 需安装 NPOI NuGet 包
实现复杂度 简单 中等
功能支持 基础表格数据,无格式 完整 Excel 功能(格式、公式、图表等)
文件兼容性 尚可,Excel 会提示格式警告 优秀,完全兼容
性能 高(直接输出文本) 较高(内存流操作)
适用场景 快速导出、简单报表、内部使用 正式报表、需要格式、复杂数据、对外分发