65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 ASP.NET MVC 和 OpenXML API 流式传输 Excel 文件。

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2013 年 10 月 11 日

CPOL

19分钟阅读

viewsIcon

17695

我过去几周一直埋头于 Scrum,有一段时间没有更新我的博客了,所以我想如果我想保持你的

我过去几周一直埋头于 Scrum,有一段时间没有更新我的博客了,所以我想如果我要保持你的兴趣,我应该给你一些真正有用的东西。我们一直在从 Web Forms 过渡到 ASP.NET MVC,我们有一个需求,需要将网格内容从我们的一个 ASP.NET MVC 页面导出到 Excel 电子表格,并提示用户打开或保存。

经过一番挖掘,我对移植旧解决方案的想法并不满意,所以我做了一些研究。打开 Excel 电子表格并不是什么新鲜事,但我自己有一些要求。

  • 我想从我的 ASP.NET MVC 控制器调用该函数。
  • 我想从我的模型中发送一个数据集合。
  • 我想尽量减少硬盘 I/O。
  • 我想添加自定义标题。
  • 我想给我的 Excel 文件一个有意义的名称。
  • 我想给我的电子表格一个有意义的名称,比如与用户从中导出的页面名称匹配。
  • 我希望能够扩展该类以添加自定义样式。
  • 我想为特定类型的数据格式化列
  • 我希望 Excel 立即识别文件。我们不能让任何“无法识别的文件类型”提示屏幕困扰我们的客户。
  • 最后,我想将它与 Web Forms 一起使用。

很明显,通过将文件保存到内存流可以最大限度地减少 I/O,但我的其他要求呢?

我将引导您完成以下各节中的步骤并提供源代码。

第 1 步 扩展控制器。 Steve Sanderson 的博客有一篇关于扩展控制器并将 Excel 文件作为动作结果返回的优秀文章。 Phil Haack 也有一个关于该主题的优秀帖子,你可以在此处找到: Steve 的示例使用 DataContext 和 XML 流式传输文件,但 Excel 不识别文件并提示您一个恼人的屏幕。 Excel 最终会打开文件,看起来很完美,但我真的想消除这个提示。我还想能够从模型中传入自己的数据并本地化标题和数据。我大量借鉴了 Steve 的教程,并稍作修改。那么,我如何让 Excel 正常工作呢?

我必须学习一些 Excel 兼容性才能使事情正常运行,所以我从 Office Developer's Kit 开始,追溯到 Excel 97。如果你有几个小时或一个空闲的周末,我建议你阅读 BIFF8。虽然 BIFF8 在 Office Developer's SDK 中有很好的文档,而且我最终确实成功地以 BIFF8 流式传输了文件,但我发现它在添加附加功能时几乎无法理解,坦率地说,我不想成为我小组中 Excel 电子表格知识的守护者,这最终会发生。这个解决方案需要扩展,因为我与许多其他开发人员合作,随着项目的增长,他们将有更复杂的需求。我偶然发现了 Microsoft 的 OpenXML API API 和 James Westgate 的 OOXML 扩展,然后我就开始运行了。有关 OpenXml 和文件格式的更多信息此处

获取 Open XML SDK

获取 OOXML API

所以总结一下,我将扩展我的 ASP.NET MVC 控制器,以便我的所有控制器都可以访问该功能,我将从我的模型中发送数据,接下来我将创建一个带样式标题的自定义电子表格和工作簿,最后我将为 Excel 文件和工作表指定一个有意义的名称。

 

步骤 1。 扩展控制器。

如果你读过 Steve 或 Phil 的博客(我鼓励你先阅读这些博客),这看起来像是很标准的东西,没有什么神奇的事情发生。

 

namespace YourNamespaceGoesHere.Controllers.Extensions

{

using System.Linq;

using System.Web.Mvc;

using YourNamespaceGoesHere.Controllers.Results;

 

/// <summary>

/// Excel 控制器扩展类。

/// </summary>

public static class ExcelControllerExtensions

   {

/// <summary>

/// 控制器扩展:返回一个 Excel 结果构造函数,用于从行返回值。

/// </summary>

/// <param>此控制器。</param>

/// <param>Excel 文件名。</param>

/// <param>Excel 工作表名称:默认:sheet1。</param>

/// <param>Excel 行值。</param>

/// <returns>动作结果。</returns>

public static ActionResult Excel(this Controller controller, string fileName, string excelWorkSheetName, IQueryable rows)

      {

return new ExcelResult(fileName, excelWorkSheetName, rows);

      }

 

/// <summary>

/// 控制器扩展:Excel 结果构造函数,用于从行和标题返回值。

/// </summary>

/// <param>此控制器。</param>

/// <param>Excel 文件名。</param>

/// <param>Excel 工作表名称:默认:sheet1。</param>

/// <param>Excel 行值。</param>

/// <param>Excel 标题值。</param>

/// <returns>动作结果。</returns>

public static ActionResult Excel(this Controller controller, string fileName, string excelWorkSheetName, IQueryable rows, string[] headers)

      {

return new ExcelResult(fileName, excelWorkSheetName, rows, headers);

      }

 

/// <summary>

/// 控制器扩展:Excel 结果构造函数,用于从行、标题和行键返回值。

/// </summary>

/// <param>此控制器。</param>

/// <param>Excel 文件名。</param>

/// <param>Excel 工作表名称:默认:sheet1。</param>

/// <param>Excel 行值。</param>

/// <param>Excel 标题值。</param>

/// <param>行集合的键值。</param>

/// <returns>动作结果。</returns>

public static ActionResult Excel(this Controller controller, string fileName, string excelWorkSheetName, IQueryable rows, string[] headers, string[] rowKeys)

      {

return new ExcelResult(fileName, excelWorkSheetName, rows, headers, rowKeys);

      }

   }

}

 

我有 3 个重载。

  • 重载 1 接受 Excel 文件名和一个 IQueryable 行集合。
    • 重载 2 接受 Excel 文件名、工作表名、IQueryable 行集合和标题数组。
    • 此方法允许单独传入标题,但它们必须与行的键匹配。
    • 重载 3 接受 Excel 文件名、工作表名、IQueryable 行集合、标题数组和行键集合。
      • 此方法允许您为行使用不同的标题名称。

 

步骤 3 扩展动作结果。此类存储构造时传入的值。如果您检查此类,您会发现它覆盖了 ExcecuteResult 函数,并添加了创建 Excel 文件并将其保存到内存流的功能。这是一个非常巧妙的技术,我特意将文件流式传输的功能与文件创建的功能分开,以便我的 Excel 类可以在非 MVC 应用程序中重用。

namespace YourNamespaceGoesHere.Controllers.Results

{

using System;

using System.IO;

using System.Linq;

using System.Web;

using System.Web.Mvc;

using YourNamespaceGoesHere.Controllers.ControllerExtensions;

 

/// <summary>

/// Excel 结果类

/// </summary>

public class ExcelResult : ActionResult

   {

/// <summary>

/// 文件名。

/// </summary>

private string excelFileName;

 

/// <summary>

/// 工作表名称。

/// </summary>

private string excelWorkSheetName;

 

/// <summary>

/// Excel 行数据。

/// </summary>

private IQueryable rowData;

 

/// <summary>

/// Excel 标题数据。

/// </summary>

private string[] headerData = null;

 

/// <summary>

/// 行数据键。

/// </summary>

private string[] rowPointers = null;

 

/// <summary>

/// 动作结果:Excel 结果构造函数,用于从行返回值。

/// </summary>

/// <param>Excel 文件名。</param>

/// <param>Excel 工作表名称:默认:sheet1。</param>

/// <param>Excel 行值。</param>

public ExcelResult(string fileName, string workSheetName, IQueryable rows)

: this(fileName, workSheetName, rows, null, null)

      {

      }

 

/// <summary>

 

namespace YourNamespaceGoesHere.Controllers.Results

{

using System;

using System.IO;

using System.Linq;

using System.Web;

using System.Web.Mvc;

using YourNamespaceGoesHere.Controllers.ControllerExtensions;

 

/// <summary>

/// Excel 结果类

/// </summary>

public class ExcelResult : ActionResult

   {

/// <summary>

/// 文件名。

/// </summary>

private string excelFileName;

 

/// <summary>

/// 工作表名称。

/// </summary>

private string excelWorkSheetName;

 

/// <summary>

/// Excel 行数据。

/// </summary>

private IQueryable rowData;

 

/// <summary>

/// Excel 标题数据。

/// </summary>

private string[] headerData = null;

 

/// <summary>

/// 行数据键。

/// </summary>

private string[] rowPointers = null;

 

/// <summary>

/// 动作结果:Excel 结果构造函数,用于从行返回值。

/// </summary>

/// <param>Excel 文件名。</param>

/// <param>Excel 工作表名称:默认:sheet1。</param>

/// <param>Excel 行值。</param>

public ExcelResult(string fileName, string workSheetName, IQueryable rows)

: this(fileName, workSheetName, rows, null, null)

      {

      }

 

/// <summary>

/// 动作结果:Excel 结果构造函数,用于从行和标题返回值。

/// </summary>

/// <param>Excel 文件名。</param>

/// <param>Excel 工作表名称:默认:sheet1。</param>

/// <param>Excel 行值。</param>

/// <param>Excel 标题值。</param>

public ExcelResult(string fileName, string workSheetName, IQueryable rows, string[] headers)

: this(fileName, workSheetName, rows, headers, null)

      {

      }

 

/// <summary>

/// 动作结果:Excel 结果构造函数,用于从行、标题和行键返回值。

/// </summary>

/// <param>Excel 文件名。</param>

/// <param>Excel 工作表名称:默认:sheet1。</param>

/// <param>Excel 行值。</param>

/// <param>Excel 标题值。</param>

/// <param>行集合的键值。</param>

public ExcelResult(string fileName, string workSheetName, IQueryable rows, string[] headers, string[] rowKeys)

      {

this.rowData = rows;

this.excelFileName = fileName;

this.excelWorkSheetName = workSheetName;

this.headerData = headers;

this.rowPointers = rowKeys;

      }

 

/// <summary>

/// 获取文件名的值。

/// </summary>

public string ExcelFileName

      {

get { return this.excelFileName; }

      }

 

/// <summary>

/// 获取文件名的值。

/// </summary>

public string ExcelWorkSheetName

      {

get { return this.excelWorkSheetName; }

      }

 

/// <summary>

/// 获取行的值。

/// </summary>

public IQueryable ExcelRowData

      {

get { return this.rowData; }

      }

 

/// <summary>

/// 执行 Excel 结果。

/// </summary>

/// <param>控制器上下文。</param>

public override void ExecuteResult(ControllerContext context)

      {

MemoryStream stream = ExcelDocument.Create(this.excelFileName, this.excelWorkSheetName, this.rowData, this.headerData, this.rowPointers);

WriteStream(stream, this.excelFileName);

      }

 

/// <summary>

/// 将内存流写入浏览器。

/// </summary>

/// <param>内存流。</param>

/// <param>Excel 文件名。</param>

private static void WriteStream(MemoryStream memoryStream, string excelFileName)

      {

HttpContext context = HttpContext.Current;

context.Response.Clear();

context.Response.AddHeader("content-disposition", String.Format("attachment;filename={0}", excelFileName));

memoryStream.WriteTo(context.Response.OutputStream);

 

memoryStream.Close();

context.Response.End();

      }

   }

}

 

 

最后创建 Excel 文件。关于静态方法的一点说明。我想确保我的 Excel 文档是线程安全的,所以我创建了静态方法,代价是使其不容易模拟。您需要自己权衡这个决定。

 

此类型的任何公共静态(Visual Basic 中的 **Shared**)成员都是线程安全的。不保证任何实例成员都是线程安全的。

 

 

 

namespace YourNamespaceGoesHere.Controllers.ControllerExtensions

{

using System;

using System.IO;

using System.Linq;

using DocumentFormat.OpenXml;

using DocumentFormat.OpenXml.Extensions;

using DocumentFormat.OpenXml.Packaging;

using DocumentFormat.OpenXml.Spreadsheet;

 

/// <summary>

/// Excel 文档。

/// </summary>

public static class ExcelDocument

   {

/// <summary>

/// 默认电子表格名称。

/// </summary>

private const string DefaultSheetName = "Sheet1";

 

/// <summary>

/// 创建要流式传输的 Excel 文档。

/// </summary>

/// <param>Excel 文件名。</param>

/// <param>Excel 工作表名称:默认:sheet1。</param>

/// <param>要写入的行数据。</param>

/// <param>标题数据。</param>

/// <param>行指针。</param>

/// <returns>内存流。</returns>

public static MemoryStream Create(string documentName, string excelWorkSheetName, IQueryable rowData, string[] headerData, string[] rowPointers)

      {

return CreateSpreadSheet(documentName, excelWorkSheetName, rowData, headerData, rowPointers, null);

      }

 

/// <summary>

/// 创建电子表格。

/// </summary>

/// <param>Excel 文件名。</param>

/// <param>Excel 工作表名称:默认:sheet1。</param>

/// <param>要写入的行数据。</param>

/// <param>标题数据。</param>

/// <param>行指针。</param>

/// <param>样式表。</param>

/// <returns>内存流。</returns>

private static MemoryStream CreateSpreadSheet(string documentName, string excelWorkSheetName, IQueryable rowData, string[] headerData, string[] rowPointers, Stylesheet styleSheet)

      {

int rowNum = 0;

int colNum = 0;

int maxWidth = 0;

int minCol = 1;

int maxCol = rowPointers == null ? minCol : rowPointers.Length;

maxCol = maxCol == 1 && headerData == null ? 1 : headerData.Length;

 

MemoryStream xmlStream = SpreadsheetReader.Create();

SpreadsheetDocument spreadSheet = SpreadsheetDocument.Open(xmlStream, true);

 

SetSheetName(excelWorkSheetName, spreadSheet);

 

if (styleSheet == null)

         {

SetStyleSheet(spreadSheet);

         }

else

         {

spreadSheet.WorkbookPart.WorkbookStylesPart.Stylesheet = styleSheet;

spreadSheet.WorkbookPart.WorkbookStylesPart.Stylesheet.Save();

         }

 

WorksheetPart worksheetPart = SpreadsheetReader.GetWorksheetPartByName(spreadSheet, excelWorkSheetName);

 

WriteHeaders(headerData, out rowNum, out colNum, out maxWidth, spreadSheet, worksheetPart);

AddCellWidthStyles(Convert.ToUInt32(minCol), Convert.ToUInt32(maxCol), maxWidth, spreadSheet, worksheetPart);

 

if (rowPointers == null || rowPointers.Length == 0)

         {

WriteRowsFromHeaders(rowData, headerData, rowNum, out maxWidth, spreadSheet, worksheetPart);

         }

else

         {

WriteRowsFromKeys(rowData, rowPointers, rowNum, out maxWidth, spreadSheet, worksheetPart);

         }

 

// 保存到内存流

SpreadsheetWriter.Save(spreadSheet);

spreadSheet.Close();

spreadSheet.Dispose();

return xmlStream;

      }

 

/// <summary>

/// 设置电子表格的名称。

/// </summary>

/// <param>电子表格名称。</param>

/// <param>电子表格。</param>

private static void SetSheetName(string excelSpreadSheetName, SpreadsheetDocument spreadSheet)

      {

excelSpreadSheetName = excelSpreadSheetName ?? DefaultSheetName;

 

Sheet ss = spreadSheet.WorkbookPart.Workbook.Descendants<Sheet>().Where(s => s.Name == DefaultSheetName).SingleOrDefault<Sheet>();

ss.Name = excelSpreadSheetName;

      }

 

/// <summary>

/// 添加单元格宽度样式。

/// </summary>

/// <param>最小列索引。</param>

/// <param>最大列索引。</param>

/// <param>最大列宽。</param>

/// <param>电子表格。</param>

/// <param>工作表。</param>

private static void AddCellWidthStyles(uint minCol, uint maxCol, int maxWidth, SpreadsheetDocument spreadSheet, WorksheetPart workSheetPart)

      {

Columns cols = new Columns(new Column() { CustomWidth = true, Min = minCol, Max = maxCol, Width = maxWidth, BestFit = false });

 

workSheetPart.Worksheet.InsertBefore<Columns>(cols, workSheetPart.Worksheet.GetFirstChild<SheetData>());

      }

 

/// <summary>

/// 设置样式表。

// 注意:此处设置样式而不是传入样式,可确保所有工作表都具有共同的用户界面设计。

/// </summary>

/// <param>要更改的电子表格。</param>

private static void SetStyleSheet(SpreadsheetDocument spreadSheet)

      {

// 注意:此处设置样式而不是传入样式,可确保所有工作表都具有共同的用户界面设计。

Stylesheet styleSheet = spreadSheet.WorkbookPart.WorkbookStylesPart.Stylesheet;

 

styleSheet.Fonts.AppendChild(

new Font(new FontSize() { Val = 11 }, new Color() { Rgb = "FFFFFF" }, new FontName() { Val = "Arial" }));

 

styleSheet.Fills.AppendChild(new Fill()

         {

PatternFill = new PatternFill()

            {

PatternType = PatternValues.Solid,

BackgroundColor = new BackgroundColor() { Rgb = "D8D8D8" }

            }

         });

 

spreadSheet.WorkbookPart.WorkbookStylesPart.Stylesheet.Save();

      }

 

/// <summary>

/// 保存工作表标题的样式。

/// </summary>

/// <param>单元格位置。</param>

/// <param>要更改的电子表格。</param>

/// <param>要更改的工作表。</param>

private static void SeatHeaderStyle(string cellLocation, SpreadsheetDocument spreadSheet, WorksheetPart workSheetPart)

      {

Stylesheet styleSheet = spreadSheet.WorkbookPart.WorkbookStylesPart.Stylesheet;

Cell cell = workSheetPart.Worksheet.Descendants<Cell>().Where(c => c.CellReference == cellLocation).FirstOrDefault();

 

if (cell == null)

         {

throw new ArgumentNullException("Cell not found");

         }

 

cell.SetAttribute(new OpenXmlAttribute("", "s", "", "1"));

OpenXmlAttribute cellStyleAttribute = cell.GetAttribute("s", "");

CellFormats cellFormats = spreadSheet.WorkbookPart.WorkbookStylesPart.Stylesheet.CellFormats;

 

// 选择第一个单元格格式。

CellFormat cellFormat = (CellFormat)cellFormats.ElementAt(0);

 

CellFormat cf = new CellFormat(cellFormat.OuterXml);

cf.FontId = styleSheet.Fonts.Count;

cf.FillId = styleSheet.Fills.Count;

 

cellFormats.AppendChild(cf);

 

int a = (int)styleSheet.CellFormats.Count.Value;

 

cell.SetAttribute(cellStyleAttribute);

 

cell.StyleIndex = styleSheet.CellFormats.Count;

 

workSheetPart.Worksheet.Save();

      }

 

/// <summary>

/// 替换特殊字符。

/// </summary>

/// <param>要输入的值。</param>

/// <returns>替换特殊字符后的值。</returns>

private static string ReplaceSpecialCharacters(string value)

      {

value = value.Replace("’", "'");

value = value.Replace("“", "\"");

value = value.Replace("”", "\"");

value = value.Replace("–", "-");

value = value.Replace("…", "...");

return value;

      }

 

/// <summary>

/// 将值写入电子表格。

/// </summary>

/// <param>行列表值。</param>

/// <param>要写入的值。</param>

/// <param>要写入的电子表格。</param>

/// <param>要写入的工作表。</param>

private static void WriteValues(string cellLocation, string strValue, SpreadsheetDocument spreadSheet, WorksheetPart workSheet)

      {

WorksheetWriter workSheetWriter = new WorksheetWriter(spreadSheet, workSheet);

 

int intValue = 0;

if (strValue.Contains("$"))

         {

strValue = strValue.Replace("$", "");

strValue = strValue.Replace(",", "");

 

workSheetWriter.PasteValue(cellLocation, strValue, CellValues.Number);

         }

else if (int.TryParse(strValue, out intValue))

         {

workSheetWriter.PasteValue(cellLocation, strValue, CellValues.Number);

         }

else if (string.IsNullOrEmpty(strValue))

         {

workSheetWriter.PasteText(cellLocation, strValue);

         }

else

         {

workSheetWriter.PasteText(cellLocation, strValue);

         }

      }

 

/// <summary>

/// 写入电子表格的 Excel 行。

/// </summary>

/// <param>Excel 行值。</param>

/// <param>Excel 行键值。</param>

/// <param>行号。</param>

/// <param>最大宽度。</param>

/// <param>要写入的电子表格。</param>

/// <param>要写入的工作表。</param>

private static void WriteRowsFromKeys(IQueryable rowData, string[] rowDataKeys, int rowNum, out int maxWidth, SpreadsheetDocument spreadSheet, WorksheetPart workSheet)

      {

maxWidth = 0;

 

foreach (object row in rowData)

         {

int colNum = 0;

foreach (string rowKey in rowDataKeys)

            {

string strValue = row.GetType().GetProperty(rowKey).GetValue(row, null).ToString();

strValue = ReplaceSpecialCharacters(strValue);

maxWidth = strValue.Length > maxWidth ? strValue.Length : maxWidth;

 

string cellLocation = string.Format("{0}{1}", GetColumnLetter(colNum.ToString()), rowNum);

ExcelDocument.WriteValues(cellLocation, strValue, spreadSheet, workSheet);

 

colNum++;

            }

 

rowNum++;

         }

      }

 

/// <summary>

/// 将列号转换为字母数字值。

/// </summary>

/// <param>列号。</param>

/// <returns>数字的 ASCII 值。</returns>

private static string GetColumnLetter(string colNumber)

      {

if (string.IsNullOrEmpty(colNumber))

         {

throw new ArgumentNullException(colNumber);

         }

 

string colName = null;

 

try

         {

for (int i = 0; i < colNumber.Length; i++)

            {

string colValue = colNumber.Substring(i, 1);

 

int asc = Convert.ToInt16(colValue) + 65;

 

colName += Convert.ToChar(asc);

            }

         }

finally

         {

colName = colName ?? "A";

         }

 

return colName;

      }

 

/// <summary>

/// 从标题写入行的值。

/// </summary>

/// <param>Excel 行值。</param>

/// <param>Excel 标题值。</param>

/// <param>行号。</param>

/// <param>最大宽度。</param>

/// <param>要写入的电子表格。</param>

/// <param>要写入的工作表。</param>

private static void WriteRowsFromHeaders(IQueryable rowData, string[] headerData, int rowNum, out int maxWidth, SpreadsheetDocument spreadSheet, WorksheetPart workSheet)

      {

WorksheetWriter workSheetWriter = new WorksheetWriter(spreadSheet, workSheet);

maxWidth = 0;

 

foreach (object row in rowData)

         {

int colNum = 0;

foreach (string header in headerData)

            {

string strValue = row.GetType().GetProperty(header).GetValue(row, null).ToString();

strValue = ReplaceSpecialCharacters(strValue);

maxWidth = strValue.Length > maxWidth ? strValue.Length : maxWidth;

 

string cellLocation = string.Format("{0}{1}", GetColumnLetter(colNum.ToString()), rowNum);

 

ExcelDocument.WriteValues(cellLocation, strValue, spreadSheet, workSheet);

colNum++;

            }

 

rowNum++;

         }

      }

 

/// <summary>

/// 写入电子表格的 Excel 标题。

/// </summary>

/// <param>Excel 标题值。</param>

/// <param>行号。</param>

/// <param>列号。</param>

/// <param>最大列宽</param>

/// <param>要写入的最大列宽。</param>

/// <param>要写入的工作表。</param>

private static void WriteHeaders(string[] headerData, out int rowNum, out int colNum, out int maxWidth, SpreadsheetDocument spreadSheet, WorksheetPart workSheet)

      {

rowNum = 1;

colNum = 0;

maxWidth = 0;

 

foreach (string header in headerData)

         {

string strValue = ReplaceSpecialCharacters(header);

 

string cellLocation = string.Format("{0}{1}", GetColumnLetter(colNum.ToString()), rowNum);

maxWidth = strValue.Length > maxWidth ? strValue.Length : maxWidth;

ExcelDocument.WriteValues(cellLocation, strValue, spreadSheet, workSheet);

SeatHeaderStyle(cellLocation, spreadSheet, workSheet);

colNum++;

         }

 

rowNum++;

      }

   }

}

 

 

我希望你觉得这很有用。我不会在这里详细介绍 OpenXml API。这都是可运行的代码,所以我实际上建议你自己逐步调试代码并熟悉 Microsoft 的 OpenXml 开发人员网站。 http://msdn.microsoft.com/en-us/office/bb738430.aspx

 

在我的博客上阅读更多

此致,

 

-c

© . All rights reserved.