使用 Open XML SDK 2.0 生成 Excel 2010 工作簿

**摘要:**了解如何使用 Open XML SDK 2.0 处理 Microsoft Excel 2010 工作簿。

上次修改时间: 2012年1月5日

适用范围: Excel 2010 | Office 2010 | Open XML | SharePoint Server 2010 | VBA

**发布时间:**2011 年 4 月

**供稿人:**Steve Hansen,Grid Logic

目录

  • Open XML 文件格式介绍

  • Excel 文件一窥

  • 以编程方式处理 Open XML 文件

  • 使用 Open XML SDK 2.0 处理工作簿

  • 结论

  • 其他资源

  • 关于作者

下载示例代码(该链接可能指向英文页面)

Open XML 文件格式介绍

Open XML 是以文档为中心的核心 Office 应用程序的开放文件格式。Open XML 旨在取代使用 Microsoft Office 应用程序定义的二进制格式编码的现有字处理文档、演示文稿和电子表格。Open XML 文件格式具有许多优势。其中一个优势是 Open XML 文件格式可确保所有理解该文件格式的程序均可访问文档中包含的数据。这可以帮助组织确保今天创建的文档在未来仍然可用。另一个优势是它简化了无法安装 Office 客户端应用程序的服务器环境或其他环境中文档的创建和处理。

顾名思义,Open XML 文件使用 XML 进行表示。不过,Open XML 文档并不是使用单个大型 XML 文件进行表示,它实际上使用称为部件的相关文件的集合进行表示,这些部件存储在包中并压缩成 ZIP 存档。Open XML 文档包使用开放打包约定 (OPC) 规范进行编译,该规范是一项容器文件技术,用于存储共同组成单个实体的 XML 和非 XML 文件的组合。

Excel 文件一窥

初步了解一切是如何协同作业的最佳方法之一就是打开一个工作簿文件,然后查看各个部分。要研究 Microsoft Excel 2010 工作簿包的各个部件,只需将文件扩展名从 .xlsx 更改为 .zip 即可。例如,考虑图 1 和图 2 所示的工作簿。

图 1. 简单工作簿

简单工作簿

此工作簿包含两个工作表:图 1 显示了包含年度销售额的工作表,而图 2 显示了包含一个简单图表的工作表。

图 2. 工作簿中的基本图表

工作簿中的基本图表

通过将此工作簿的名称从 Simple Sales Example.xlsx 更改为 Simple Sales Example.zip,您便可以使用 Windows 资源管理器查看文件容器或包中部件的结构。

图 3. 简单工作簿的部件结构

简单工作簿的部件结构

图 3 显示了包中主要的文件夹以及存储在工作表文件夹中的各个部件。继续深入剖析一下,图 4 提供了名为 sheet1.xml 的部件中 XML 的概览。

图 4. 工作表部件内的 XML 示例

工作表部分中的 XML 示例

图 4 所示的 XML 提供了 Excel 呈现图 1 所示工作表所需的必要信息。例如,sheetData 节点中存在行节点。至少有一个单元格不是为空的每一行都有一个行节点。然后,在每一行内,每一个非空单元格都有一个节点。

请注意,图 1 所示单元格 C3 包含粗体值 2008。而单元格 C4 包含值 182,但使用的是不包含粗体的默认格式设置。图 4 显示了这些单元格的 XML 表示形式。特别是单元格 C3 的 XML 如下面的示例所示。

      <c r="C3" s="1">
        <v>2008</v>
      </c>

为了保持 Open XML 文件大小尽可能紧凑,许多 XML 节点和属性都采用非常短的名称。在之前的片段中,c 表示一个单元格,此特定单元格指定两个属性:r(引用)和 s(样式索引)。引用属性指定单元格的位置引用。

样式索引是对用于设置单元格格式的样式的引用。样式在样式部件 (styles.xml) 中定义,样式部件位于 xl 文件夹(参见图 3 中的 xl 文件夹)。以下示例显示了单元格 C3 的 XML 和单元格 C4 的 XML 的比较。

      <c r="C4">
        <v>182</v>
      </c>

由于单元格 C4 使用默认的格式设置,因此您无需指定样式索引属性的值。在下文中,您将更加详细地了解如何在 Open XML 文档中使用样式索引。

尽管了解更多有关 Open XML 文件格式的细节非常有用,但是本文的真正目的在于演示如何使用 Open XML SDK 2.0 for Microsoft Office 以编程的方式处理 Open XML 文档,具体而言就是 Excel 工作簿。

以编程方式处理 Open XML 文件

以编程方式创建或处理 Open XML 文档的一个方法就是使用下面的高级模式:

  1. 打开/创建 Open XML 包

  2. 打开/创建包部件

  3. 解析您需要处理的部件中的 XML

  4. 根据需要处理 XML

  5. 保存部件

  6. 重新打包文档

除了第三步和第四步,其他步骤都能使用 System.IO.Packaging 命名空间中的类轻松实现。这些类旨在方便处理 Open XML 包以及与高级部件处理关联的任务。

此过程最难的部分就是第四步,即处理 XML。对于这一部分,开发人员务必 深入理解许多繁琐细节才能成功处理 Open XML 文件格式的许多细微变化。例如,前面您了解到单元格的格式设置信息并未与单元格一起存储。事实上,格式设置详细信息在另外一个文档部件中被定义为样式,与该样式关联的样式索引才是 Excel 在单元格中存储的内容。

即使对 Open XML 规范有全面的了解,许多开发人员也不希望以编程方式处理如此原始的 XML。这正是 Open XML SDK 2.0 应蕴而生的原因。

开发 Open XML SDK 2.0 的目的是简化 Open XML 包和包中的基础 Open XML 架构元素的处理。Open XML SDK 2.0 封装了开发人员对 Open XML 包执行的许多常见任务,这样您就可以不用处理原始 XML,而是可以使用可提供许多设计时优势(例如 IntelliSense 支持和类型安全开发体验)的 .NET 类。

使用 Open XML SDK 2.0 处理工作簿

为了演示使用 Open XML SDK 2.0 处理 Excel 工作簿的过程,本文演练了如何构建报告生成器。假设您为一家名为 Contoso 的证券经纪公司工作。Contoso 的 ASP.NET 网站允许客户登录并联机查看各种项目组合报告。但是,常见的用户请求却是希望能够在 Excel 中查看或下载报告,以便他们能够执行其他临时的项目组合分析。

备注

为了方便您试用此代码,下面的示例构建了一个基于控制台的应用程序。同时,此示例中使用的技术将与 ASP.NET 网站 100% 兼容。在此示例中对 Microsoft Excel 绝对没有任何要求。

所需的结果是一个使客户端能够生成 Excel 项目组合报告的过程。通常有两种方法可实现此过程。一种方法是从头开始生成所有文档。对于包含极少或根本不包含任何格式的简单工作簿,很适合使用这种方法。第二种方法是创建使用模板 的文档,这种方法较为常用。请注意,这里提到的 Word 模板不是真的 Excel 模板 (*.xltx)。而是指使用包含最终工作簿所需的所有必要格式设置、图表等信息的工作簿 (*.xlsx)。要使用该模板,此过程的第一步就是复制模板文件。然后添加与要为其建构报告的客户关联的数据。

图 5. 项目组合报告的示例

项目组合报告的示例

设置项目

若要创建项目组合报告生成器,请打开 Microsoft Visual Studio 2010 并创建一个名为 PortfolioReportGenerator 的新控制台应用程序。

备注

若要下载示例 C# 和 Visual Basic .NET 项目,请单击下载代码示例(该链接可能指向英文页面)

图 6. 创建项目组合报告生成器解决方案

创建项目组合报告生成器解决方案

接下来,向项目中添加两个类:PortfolioReport 和 Portfolio。PortfolioReport 类是使用 Open XML SDK 2.0 执行所有文档处理的关键类。Portfolio 类实际上是一个数据结构,包含表示客户项目组合所必需的属性。

备注

本文中将详细介绍 Portfolio 类。它是一个包含一些测试数据的数据容器,并且不包含与 Open XML 或 Open XML SDK 2.0 相关的代码。

在您编写任何代码之前,任何涉及 Open XML 和 Open XML SDK 2.0 的项目的第一步都是向项目中添加所需的引用。需要两个特定的引用:DocumentFormat.OpenXml 和 WindowsBase。

DocumentFormat.OpenXml 包含随 Open XML SDK 2.0 安装的类。如果您在安装 Open XML SDK 2.0 之后找不到此引用,则可以通过浏览找到它。默认情况下,它位于 C:\Program Files (x86)\Open XML SDK\V2.0\lib\ 中。仅当您打算使用 Open XML SDK 2.0 时才需要此引用。如果您希望通过调整原始 XML 来处理 Open XML 文档,则不需要此引用。

WindowsBase 包括 System.IO.Packaging 命名空间中的类。所有 Open XML 项目都需要此引用,不管您是否使用 Open XML SDK 2.0。System.IO.Packaging 命名空间中的类提供打开 Open XML 包的功能。此外,还有一些类允许您处理(添加、删除、编辑)Open XML 包中的部件。

此时,您的项目应类似于图 7。

图 7. 初始项目设置后的项目组合报告生成器项目

设置后的项目组合报告生成器项目

初始化项目组合报告

如上所述,报告生成过程包括创建报告模板的副本和向报告中添加数据。报告模板是一个预先设置了格式的 Excel 工作簿,名为 PortfolioReport.xlsx。向执行此过程的 PortfolioReport 类中添加一个构造函数。为了复制文件,您还必须导入 System.IO 命名空间。添加 System.IO 命名空间时,请添加与 Open XML SDK 2.0 相关的命名空间。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
using DocumentFormat.OpenXml;

namespace PortfolioReportGenerator
{
    class PortfolioReport
    {
        string path = "c:\\example\\";
        string templateName = "PortfolioReport.xlsx";

        public PortfolioReport(string client)
        {
            string newFileName = path + client + ".xlsx";
            CopyFile(path + templateName, newFileName);
        }

        private string CopyFile(string source, string dest)
        {
            string result = "Copied file";
            try
            {
                // Overwrites existing files
                File.Copy(source, dest, true);
            }
            catch (Exception ex)
            {
                result = ex.Message;
            }
            return result;
        }
    }
}

请注意,PortfolioReport 构造函数需要一个参数,用于表示将要为其生成报告的客户。

为了避免向方法传递参数或持续不断地重新打开文档并提取工作簿部件,可将两个类范围的私有变量添加到 PortfolioReport 类。同样,再添加一个类范围的私有变量以存放对当前 Portfolio 对象的引用,该对象的数据正在用于生成报告。这些变量准备就绪后,就可以在 PortfolioReport 构造函数中对它们进行初始化,如下面的示例所示。

        string path = "c:\\example\\";
        string templateName = "PortfolioReport.xlsx";

        WorkbookPart wbPart = null;
        SpreadsheetDocument document = null;
        Portfolio portfolio = null;

        public PortfolioReport(string client)
        {
            string newFileName = path + client + ".xlsx";
            CopyFile(path + templateName, newFileName);
            document = SpreadsheetDocument.Open(newFileName, true);
            wbPart = document.WorkbookPart;
            portfolio = new Portfolio(client);
        }

此代码段突显了使用 Open XML SDK 2.0 打开文档和提取部件是多么容易。在 PortfolioReport 构造函数中,使用 SpreadsheetDocument 类的 Open 方法打开工作簿文件。SpreadsheetDocument 是 DocumentFormat.OpenXml.Packaging 命名空间的一部分。SpreadsheetDocument 通过名为 WorkbookPart 的属性方便地访问文档包中的工作簿部件。在整个过程进行到这个时候,报告生成器已经:

  1. 创建了 PortfolioReport.xlsx 文件的副本

  2. 使用客户的名称命名了副本

  3. 打开了客户报告进行编辑

  4. 提取了工作簿部件

使用 Open XML SDK 修改工作表单元格值

完成报告生成器需要解决的主要任务是了解如何使用 Open XML SDK 2.0 修改 Excel 工作簿中的值。结合使用 Excel 对象模型和 Microsoft Visual Basic for Applications (VBA) 或 .NET,更改单元格的值会非常容易。要更改某个单元格(Excel 对象模型中的 Range 对象)的值,请修改 Value 属性的值。例如,要将名为"Sales"(销售额)的工作表中 B4 单元格的值更改为 250,您可以使用下面的语句:

ThisWorkbook.Worksheets("Sales").Range("B4").Value = 250

Open XML SDK 2.0 的工作方式则有些不同。一个最大的不同之外就在于,通过使用 Excel 对象模型,您可以处理工作表中的所有单元格,不管该单元格是否有内容。换句话说,只要使用对象模型,工作表中的所有单元格就都存在。当使用 Open XML 时,对象则不存在。这是默认情况。如果某个单元格没有值,它就不存在。如果您从指定文件格式的角度看来,就会觉得这非常合理。为了保持文件的大小尽可能的小,将只保存相关信息。例如,回过头来再看看图 4,观察 sheetData 下面的第一个行节点。第一行开始于第 3 行,跳过了第 1 行和第 2 行。这是因为前两行的所有单元格都为空。同样,请注意,第一个行节点(第 3 行)中,第一个单元格的地址为 C3。这是因为 A3 和 B3 为空。

因为您无法确定某个单元格是否存在于 Open XML 文档中,所以您必须首先检查它是否存在,如果不存在,则将其添加到文件中。下面的示例显示了一个可以执行此功能的名为 InsertCellInWorksheet 的方法,另外还列出了一些其他方法。将这些方法添加到 PortfolioReport 类中。

备注

Microsoft 提供许多常见 Open XML SDK 2.0 任务的代码示例。更好的是,这些示例可用作您在 Visual Studio 2010 中使用的代码示例。本文中的某些代码就以这些代码示例为基础。您可以在此下载示例代码(该链接可能指向英文页面)

        // Given a Worksheet and an address (like "AZ254"), either return a 
        // cell reference, or create the cell reference and return it.
        private Cell InsertCellInWorksheet(Worksheet ws, string addressName)
        {
            SheetData sheetData = ws.GetFirstChild<SheetData>();
            Cell cell = null;

            UInt32 rowNumber = GetRowIndex(addressName);
            Row row = GetRow(sheetData, rowNumber);

            // If the cell you need already exists, return it.
            // If there is not a cell with the specified column name, insert one.  
            Cell refCell = row.Elements<Cell>().
                Where(c => c.CellReference.Value == addressName).FirstOrDefault();
            if (refCell != null)
            {
                cell = refCell;
            }
            else
            {
                cell = CreateCell(row, addressName);
            }
            return cell;
        }
        
        // Add a cell with the specified address to a row.
        private Cell CreateCell(Row row, String address)
        {
            Cell cellResult;
            Cell refCell = null;

            // Cells must be in sequential order according to CellReference. 
            // Determine where to insert the new cell.
            foreach (Cell cell in row.Elements<Cell>())
            {
                if (string.Compare(cell.CellReference.Value, address, true) > 0)
                {
                    refCell = cell;
                    break;
                }
            }

            cellResult = new Cell();
            cellResult.CellReference = address;

            row.InsertBefore(cellResult, refCell);
            return cellResult;
        }

        // Return the row at the specified rowIndex located within
        // the sheet data passed in via wsData. If the row does not
        // exist, create it.
        private Row GetRow(SheetData wsData, UInt32 rowIndex)
        {
            var row = wsData.Elements<Row>().
            Where(r => r.RowIndex.Value == rowIndex).FirstOrDefault();
            if (row == null)
            {
                row = new Row();
                row.RowIndex = rowIndex;
                wsData.Append(row);
            }
            return row;
        }

        // Given an Excel address such as E5 or AB128, GetRowIndex
        // parses the address and returns the row index.
        private UInt32 GetRowIndex(string address)
        {
            string rowPart;
            UInt32 l;
            UInt32 result = 0;

            for (int i = 0; i < address.Length; i++)
            {
                if (UInt32.TryParse(address.Substring(i, 1), out l))
                {
                    rowPart = address.Substring(i, address.Length - i);
                    if (UInt32.TryParse(rowPart, out l))
                    {
                        result = l;
                        break;
                    }
                }
            }
            return result;
        }

使用 Excel 对象模型和处理 Open XML 文档的另一个不同之处在于,当您使用 Excel 对象模型时,您向单元格或范围提供的值的数据类型无关紧要。而使用 Open XML 更改单元格的值时,具体过程根据值的数据类型而异。对于数字值,该过程与使用 Excel 对象模型有点类似。存在一个与 Open XML SDK 2.0 中名为 CellValue 的 Cell 对象关联的属性。您可以使用该属性为单元格赋予数字值。

在单元格中存储字符串或文本的方式有所不同。Excel 并不是直接将文本存储在单元格中,而是将它存储在一个称为共享字符串表 的地方。共享字符串表只不过是工作簿中所有唯一字符串的列表,其中每个唯一的字符串都与一个索引关联。要将单元格与字符串关联,单元格只需存放对字符串索引的引用即可,而不需要存放字符串本身。当您将单元格的值更改为字符串时,首先需要查看该字符串是否位于共享字符串表中。如果位于该表中,您只需查找共享字符串索引并将它存储在单元格中即可。如果字符串不在共享字符串表中,那么您需要添加该字符串,检索其字符串索引,然后将字符串索引存储在单元格中。下面的示例显示了一个用于更改单元格值的名为 UpdateValue 的方法,以及一个用于更新共享字符串表的 InsertSharedStringItem 方法。

        public bool UpdateValue(string sheetName, string addressName, string value, 
                                UInt32Value styleIndex, bool isString)
        {
            // Assume failure.
            bool updated = false;

            Sheet sheet = wbPart.Workbook.Descendants<Sheet>().Where(
                (s) => s.Name == sheetName).FirstOrDefault();

            if (sheet != null)
            {
                Worksheet ws = ((WorksheetPart)(wbPart.GetPartById(sheet.Id))).Worksheet;
                Cell cell = InsertCellInWorksheet(ws, addressName);

                if (isString)
                {
                    // Either retrieve the index of an existing string,
                    // or insert the string into the shared string table
                    // and get the index of the new item.
                    int stringIndex = InsertSharedStringItem(wbPart, value);

                    cell.CellValue = new CellValue(stringIndex.ToString());
                    cell.DataType = new EnumValue<CellValues>(CellValues.SharedString);
                }
                else
                {
                    cell.CellValue = new CellValue(value);
                    cell.DataType = new EnumValue<CellValues>(CellValues.Number);
                }

                if (styleIndex > 0)
                    cell.StyleIndex = styleIndex;
                
                // Save the worksheet.
                ws.Save();
                updated = true;
            }

            return updated;
        }

        // Given the main workbook part, and a text value, insert the text into 
        // the shared string table. Create the table if necessary. If the value 
        // already exists, return its index. If it doesn't exist, insert it and 
        // return its new index.
        private int InsertSharedStringItem(WorkbookPart wbPart, string value)
        {
            int index = 0;
            bool found = false;
            var stringTablePart = wbPart
                .GetPartsOfType<SharedStringTablePart>().FirstOrDefault();

            // If the shared string table is missing, something's wrong.
            // Just return the index that you found in the cell.
            // Otherwise, look up the correct text in the table.
            if (stringTablePart == null)
            {
                // Create it.
                stringTablePart = wbPart.AddNewPart<SharedStringTablePart>();
            }

            var stringTable = stringTablePart.SharedStringTable;
            if (stringTable == null)
            {
                stringTable = new SharedStringTable();
            }

            // Iterate through all the items in the SharedStringTable. 
            // If the text already exists, return its index.
            foreach (SharedStringItem item in stringTable.Elements<SharedStringItem>())
            {
                if (item.InnerText == value)
                {
                    found = true;
                    break;
                }
                index += 1;
            }

            if (!found)
            {
                stringTable.AppendChild(new SharedStringItem(new Text(value)));
                stringTable.Save();
            }

            return index;
        }

前面代码示例中一个值得关注的地方就是单元格的格式处理。如上文所述,单元格的格式并未存储在单元格节点中。实际上,单元格存储的是指向另一个部件 (styles.xml) 中定义的样式的样式索引。当通过 VBA 或 .NET 使用本文档中演示的模板模式和 Excel 对象模型时,您通常会将所需的格式应用于由一个或多个单元格组成的范围。当您以编程方式向工作簿中添加数据时,您在该范围内应用的所有格式都将如实地得到应用。

由于 Open XML 文件仅包含有关含有数据的单元格的信息,任何时候向文件中添加新单元格时,只要单元格需要任何格式,您就必须更新样式索引。因此,UpdateValue 方法接受 styleIndex 参数,以指示向单元格应用哪个样式索引。如果您传递零值,则不会设置样式索引,单元格将使用 Excel 的默认格式。

确定每个单元格的相应样式索引的一个简单方法就是,根据需要设置工作簿模板文件的格式,然后在 XML 模式下打开相应的工作簿部件(如图 4 所示)并观察设置格式的单元格的样式索引。

当前面代码列表中的方法准备就绪后,生成报告的过程现在只不过是获取项目组合数据并反复调用 UpdateValue 以创建报告的过程。事实上,如果您添加必要的代码来执行此过程,一切似乎都很顺利,只有一个问题除外,即任何包含公式(该公式引用的单元格的值通过 Open XML 处理进行了更改)的单元格无法显示正确结果。这是因为 Excel 会缓存单元格内公式的结果。由于 Excel 认为它缓存了正确的值,因此它不重新计算单元格。即使您打开自动计算或者按 F9 强制执行手动重新计算,Excel 也不重新计算单元格。

此问题的解决方案是从这些单元格删除缓存的值,以便 Excel 会在 Excel 中打开文件时立即重新计算值。将下面示例中显示的 RemoveCellValue 方法添加到 PortfolioReport 类中可提供此功能。

        // This method is used to force a recalculation of cells containing formulas. The
        // CellValue has a cached value of the evaluated formula. This
        // prevents Excel from recalculating the cell even if 
        // calculation is set to automatic.
        private bool RemoveCellValue(string sheetName, string addressName)
        {
            bool returnValue = false;

            Sheet sheet = wbPart.Workbook.Descendants<Sheet>().
                Where(s => s.Name == sheetName).FirstOrDefault();
            if (sheet != null)
            {
                Worksheet ws = ((WorksheetPart)(wbPart.GetPartById(sheet.Id))).Worksheet;
                Cell cell = InsertCellInWorksheet(ws, addressName);

                // If there is a cell value, remove it to force a recalculation
                // on this cell.
                if (cell.CellValue != null)
                {
                    cell.CellValue.Remove();
                }
                
                // Save the worksheet.
                ws.Save();
                returnValue = true;
            }

            return returnValue;
        }

要完成 PortfolioReport 类,请将下面示例中显示的 CreateReport 方法添加到 PortfolioReport 类中。它使用 CreateReport 方法 UpdateValue 将项目组合信息放入所需单元格。在更新了全部的所需单元格后,它对每个需要重新计算的单元格调用 RemoveCellValue。最后,CreateReport 对 SpreadsheetDocument 调用 Close 方法以保存所有更改并关闭文件。

        // Create a new Portfolio report
        public void CreateReport()
        {
            string wsName = "Portfolio Summary";

            UpdateValue(wsName, "J2", "Prepared for " + portfolio.Name, 0, true);
            UpdateValue(wsName, "J3", "Account # " + 
                        portfolio.AccountNumber.ToString(), 0, true);
            UpdateValue(wsName, "D9", portfolio.BeginningValueQTR.ToString(), 0, false);
            UpdateValue(wsName, "E9", portfolio.BeginningValueYTD.ToString(), 0, false);
            UpdateValue(wsName, "D11", portfolio.ContributionsQTR.ToString(), 0, false);
            UpdateValue(wsName, "E11", portfolio.ContributionsYTD.ToString(), 0, false);
            UpdateValue(wsName, "D12", portfolio.WithdrawalsQTR.ToString(), 0, false);
            UpdateValue(wsName, "E12", portfolio.WithdrawalsYTD.ToString(), 0, false);
            UpdateValue(wsName, "D13", portfolio.DistributionsQTR.ToString(), 0, false);
            UpdateValue(wsName, "E13", portfolio.DistributionsYTD.ToString(), 0, false);
            UpdateValue(wsName, "D14", portfolio.FeesQTR.ToString(), 0, false);
            UpdateValue(wsName, "E14", portfolio.FeesYTD.ToString(), 0, false);
            UpdateValue(wsName, "D15", portfolio.GainLossQTR.ToString(), 0, false);
            UpdateValue(wsName, "E15", portfolio.GainLossYTD.ToString(), 0, false);

            int row = 7;
            wsName = "Portfolio Holdings";

            UpdateValue(wsName, "J2", "Prepared for " + portfolio.Name, 0, true);
            UpdateValue(wsName, "J3", "Account # " + 
                        portfolio.AccountNumber.ToString(), 0, true);
            foreach (PortfolioItem item in portfolio.Holdings)
            {
                UpdateValue(wsName, "B" + row.ToString(), item.Description, 3, true);
                UpdateValue(wsName, "D" + row.ToString(), 
                            item.CurrentPrice.ToString(), 24, false);
                UpdateValue(wsName, "E" + row.ToString(), 
                            item.SharesHeld.ToString(), 27, false);
                UpdateValue(wsName, "F" + row.ToString(), 
                            item.MarketValue.ToString(), 24, false);
                UpdateValue(wsName, "G" + row.ToString(), 
                            item.Cost.ToString(), 24, false);
                UpdateValue(wsName, "H" + row.ToString(), 
                            item.High52Week.ToString(), 28, false);
                UpdateValue(wsName, "I" + row.ToString(), 
                            item.Low52Week.ToString(), 28, false);
                UpdateValue(wsName, "J" + row.ToString(), item.Ticker, 11, true);
                row++;
            }

            // Force re-calc when the workbook is opened
            this.RemoveCellValue("Portfolio Summary", "D17");
            this.RemoveCellValue("Portfolio Summary", "E17");

            // All done! Close and save the document.
            document.Close();
        }

使用 PortfolioReport 类

最后一步(假设您复制了 Portfolio 类的源代码)是将一些代码添加到 Program 类中的 Main 方法。修改 Main 方法以便它包含下面的示例显示的代码。请注意,Portfolio 类的源代码包括两个客户的示例数据:SteveKelly

        static void Main(string[] args)
        {
            PortfolioReport report = new PortfolioReport("Steve");
            report.CreateReport();
            report = new PortfolioReport("Kelly");
            report.CreateReport();
            Console.WriteLine("Reports created!");
            Console.WriteLine("Press ENTER to quit.");
            Console.ReadLine();
        }

运行此代码时,您会注意到文件的生成速度非常快。这非常适合高处理量的服务器应用场景。使用 Excel 对象模型实现相同结果的类似代码的性能则相差甚远,Open XML 方法要快很多很多。

结论

从 2007 Microsoft Office system 开始,以文档为中心的核心 Microsoft Office 应用程序从专有的二进制文件格式切换到 Open XML 文件格式。Open XML 文件格式是开放的、基于标准的 XML 文件格式。切换到 Open XML 文件格式为开发人员提供了新的开发机遇。不过,要想利用这些这些新的机遇,则需要花时间和精力了解 XML 规范和大量繁琐的原始 XML 处理。

Open XML SDK 2.0 通过将 Open XML 规范的许多详细信息封装在用于处理 Open XML 文档的易于使用的类库中,来帮助缩短开发技术的学习曲线。除了缩短学习曲线,Open XML SDK 2.0 还通过提供设计时功能(例如 IntelliSense 支持和类型安全开发体验)帮助提高开发人员的效率。

本文演示了如何使用 Open XML SDK 2.0 建构项目组合报告生成器。此练习演示了常见的解决方案模式和面向 Excel 的常见任务(例如打开工作簿、引用工作表、检索单元格和更新单元格的值)的方法。

其他资源

若要查找有关本文中讨论的主题的详细信息,请参阅以下资源。

关于作者

Steve Hansen 是 Grid Logic 的创始人,这是一家位于明尼苏达州的咨询公司,专门从事商业智能和信息工作者解决方案方面的业务。作为一名开发人员,同时还经常受邀为各种技术大会撰写文章和发表演讲,Steve 是 Visual Studio 领域的 Microsoft MVP。一面是代码老手,一面是金融奇才;Steve 还取得明尼苏达大学金融专业的 MBA 学位。