Excel的便捷使得其在非开发人员的办公中非常流行,而Excel确实也提供了很多有用的功能。很多时候我们还需要以Excel为数据源来进行处理或者将Excel作为模板来生成一些报表。在Open XML SDK没出来之前,我们大多采用引用Office类库的方法来做处理,但这样的操作显得很麻烦。而Open XML SDK的出现,让我们可以以一个很自然的方式来处理这些数据,本文我们就一起了解一下如何使用Open XML SDK来通过LINQ to XML的方式操作Excel。

这些代码是建立在Open XML SDK CTP 2基础上的,使用前请先下载Open XML Format SDK 2.0。点击这里下载。SDK默认会安装在C:\Program Files (x86)\Open XML Format SDK\V2.0 (64bit)目录下,lib子目录下的DocumentFormat.OpenXml.dll必须被引用到项目中。

Excel也罢,word也罢,他们都是通过Open XML的标准来组织特定标记的。其实,你只要理解这些标记的含义,你可以不用任何工具自己来解析XML得到你想要的内容和格式。而Open XML SDK提供给我们的是更统一的解析方式。通过下边传统的DOM解析,你可以得到一个Excel的worksheet集合。
  1. public static List<String> GetSheets(String strFileName)

  2. {

  3.     //  Fill this collection with a list of all the sheets.

  4.     List<String> sheets = new List<String>();



  5.     using (SpreadsheetDocument xlPackage = SpreadsheetDocument.Open(strFileName, false))

  6.     {

  7.         WorkbookPart workbook = xlPackage.WorkbookPart;

  8.         Stream workbookstr = workbook.GetStream();

  9.         XmlDocument doc = new XmlDocument();

  10.         doc.Load(workbookstr);



  11.         XmlNamespaceManager nsManager = new XmlNamespaceManager(doc.NameTable);

  12.         nsManager.AddNamespace("default", doc.DocumentElement.NamespaceURI);

  13.         XmlNodeList nodelist = doc.SelectNodes("//default:sheets/default:sheet", nsManager);



  14.         foreach (XmlNode node in nodelist)

  15.         {

  16.             String sheetName = String.Empty;

  17.             sheetName = node.Attributes["name"].Value;

  18.             sheets.Add(sheetName);

  19.         }

  20.     }



  21.     return sheets;

  22. }
复制代码
对于每一个工作簿而言,Open XML Format SDK将它以不同对象之间的关系来组织。在下边的图中你可以看到这些内嵌的对象间的关系。需要说明的是,他们都代表在Excel中不同的应用,例如Columns,你可以通过它得到你的工作簿中的列。TablePart则列举了在工作簿中以Table(可以理解为以数据表,具有过滤,排序,汇总等功能)方式展现的数据区域。

附件: 2008123111562157.jpg

而我们可能更关心的是如何来得到行、列单元格内的值。当然,worksheet除了这些对象(关系)集合外,它也通过直观的行(Row)、列(Cell)来组织内容区域。通过LINQ to XML我们可以很容易的通过Descendents来得到。
  1. IEnumerable<Sheet> sheets = document.WorkbookPart.Workbook.Descendants<Sheet>().Where(s => s.Name == strSheet);

  2. if (sheets.Count() == 0)

  3. {

  4.     // The specified worksheet does not exist.

  5.     return null;

  6. }



  7. WorksheetPart worksheetPart = (WorksheetPart)document.WorkbookPart.GetPartById(sheets.First().Id);

  8. Worksheet worksheet = worksheetPart.Worksheet;



  9. //Ignore row header

  10. IEnumerable<Row> rows = worksheet.Descendants<Row>();

  11. foreach (Row row in rows)

  12. {

  13.     foreach (Cell cell in row)

  14.     {

  15.     ……

  16. }

  17. }
复制代码
能够对Row和Cell进行遍历,这就容易多了。你可以很容易的取出某一行某一列,或者所有列的数据并输出(当然,同等的道理,你也可以写入),操作起来是不是很方便呢?但不要忘记了,你很难得到cell的值:cell.InnerText并不是在任何时候都有效的。对于s类型的列(SharedStringTable类型),它的值是存储在子元素内的。
  1. public static String GetValue(Cell cell, SharedStringTablePart stringTablePart)

  2. {

  3.     if (cell.ChildElements.Count == 0)

  4.         return null;

  5.     //get cell value

  6.     String value = cell.CellValue.InnerText;

  7.     //Look up real value from shared string table

  8.     if ((cell.DataType != null) && (cell.DataType == CellValues.SharedString))

  9.         value = stringTablePart.SharedStringTable

  10.         .ChildElements[Int32.Parse(value)]

  11.         .InnerText;

  12.     return value;

  13. }
复制代码
这时,你可以通过String columnValue = GetValue(cell, tablePart);的方式来取得单元格值。tablePart就是WorkbookPart.SharedStringTablePart,它存储了所有以SharedStringTable方式存储数据的子元素。

再扩展一下如果你想将一个Excel的工作簿以强类型展示,那该如何做呢?列明,每一个行代表一个对象元素,通过反射来来对对象赋值。请注意在工作簿中并不是每个列都能和你的类属性对应的,所以必须判断。当然,你也可以通过中间元素来产生映射扩展。
  1. //get SharedStringTablePart to get the cell value.

  2. SharedStringTablePart tablePart = document.WorkbookPart.SharedStringTablePart;

  3. //Column headers

  4. String[] cellHeaders = null;

  5. String[] cellValues = null;



  6. //Ignore row header

  7. IEnumerable<Row> rows = worksheet.Descendants<Row>();

  8. foreach (Row row in rows)

  9. {

  10.     if (row.RowIndex == 1)

  11.     {

  12.         cellHeaders = new String[row.Count()];

  13.     }

  14.     cellValues = new String[row.Count()];

  15.     int i = 0;



  16.     foreach (Cell cell in row)

  17.     {                       

  18.         String columnValue = GetValue(cell, tablePart);



  19.         //The first row is header

  20.         if (row.RowIndex == 1)

  21.         {

  22.             cellHeaders = columnValue;

  23.         }

  24.         else

  25.         {

  26.             cellValues = columnValue;

  27.         }

  28.         i++;

  29.     }

  30.     if (row.RowIndex > 1)

  31.     {

  32.         products.Add(ProductConverter.Convert(cellValues, cellHeaders));

  33.     }

  34. }
复制代码
对最终的单元格值集合到Product对象的转换,我们通过ProductConverter类来完成。在这里,你可以通过反射来完成,但枚举出所有你可能用到的类型是你不得不面对的问题。
  1. foreach (PropertyInfo pi in product.GetType().GetProperties())

  2. {

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

  4.     {

  5.         if (pi.Name.Equals(cellHeader, StringComparison.OrdinalIgnoreCase))

  6.         {

  7.             //get property type

  8.             String propertyType = pi.PropertyType.Name;

  9.             switch (propertyType)

  10.             {

  11.                 case "Int32":

  12.                     pi.SetValue(product, int.Parse(cellValues), null);

  13.                     break;

  14.                 case "DateTime":

  15.                     pi.SetValue(product, System.DateTime.Parse(cellValues), null);

  16.                     break;

  17.                 case "Decimal":

  18.                     pi.SetValue(product, Decimal.Parse(cellValues), null);

  19.                     break;

  20.                 case "Double":

  21.                     pi.SetValue(product, Double.Parse(cellValues), null);

  22.                     break;

  23.                 case "String":

  24.                     pi.SetValue(product, cellValues, null);

  25.                     break;

  26.             }

  27.             break;

  28.         }

  29.     }

  30. }
复制代码
通过将数据展现到UI上,你可以验证你的工作 是否成功:
  1. List<Product> products = SpreadSheetFunction.GetProducts(strFileName, "Products");

  2. this.dataGridView1.DataSource = products;
复制代码
附件: 2008123111563945.jpg

其实对于Excel中的Table可能更有意思。因为你可以通过它来实现过滤,排序,汇总,你会感觉它特别方便(起码比Reporting Service来得快多了)。我们会再介绍如何通过更简单的办法来对Excel Table操作。(文/Allan.
TOP