I see people trying to output in CSV and then running into problems with Excel misinterpreting the cell datatype formatting.
I see people wanting to create an Excel Workbook with more than one Worksheet.
So, I present this! A little standalone class that lets you
- create an Excel document (XMLSS format)
- add as many Worksheets as you like by simply chucking DataTables at it
- send it to the browser for download
Usage an an MVC Controller Action:
public void ExportData() { DataTable dtYourData = YourApp.GetYourDataTable(); ExcelWorkbookGenerator exGen = new ExcelWorkbookGenerator(); exGen.AddWorksheet("YourWorksheetTitle", dtYourData); exGen.SendToBrowser("YourSuggestedFilename"); }
Code:
using System; using System.Collections.Generic; using System.Data; using System.Data.SqlClient; using System.IO; using System.Xml; using System.Web; namespace DaddyCode.Utilities { /// <summary> /// ExcelWorkbookGenerator : Generate Excel XML - compatible documents from DataTables. /// </summary> /// <remarks> /// Author: James McCormack, DaddyCode Ltd /// </remarks> public class ExcelWorkbookGenerator { private class Worksheet { public string Title = ""; public DataTable Data = null; public Worksheet(string title, DataTable dataTable) { this.Title = title; this.Data = dataTable; } } private List<Worksheet> Worksheets = new List<Worksheet>(); /// <summary> /// Add a new Worksheet to the Workbook, based on a DataTable that you provide /// </summary> /// <param name="title"></param> /// <param name="dataTable"></param> public void AddWorksheet(string title, DataTable dataTable) { Worksheets.Add(new Worksheet(title, dataTable)); } /// <summary> /// Send the current Workbook to the Web Browser to view or save the file /// </summary> /// <param name="suggestedFileName"></param> public void SendToBrowser(string suggestedFileName) { HttpContext.Current.Response.ClearContent(); HttpContext.Current.Response.ContentType = "application/vnd.ms-excel"; HttpContext.Current.Response.AddHeader("content-disposition", "attachment; filename=" + suggestedFileName); HttpContext.Current.Response.Write(getWorkbookXML()); HttpContext.Current.Response.End(); } /// <summary> /// Generate an Excel-compliant XML Workbook /// </summary> /// <returns></returns> private string getWorkbookXML() { XmlDocument xDoc = new XmlDocument(); xDoc.AppendChild(xDoc.CreateNode(XmlNodeType.XmlDeclaration, null, null)); string strCustomNamespace = "urn:schemas-microsoft-com:office:spreadsheet"; XmlElement root = xDoc.CreateElement("Workbook"); root.SetAttribute("xmlns", strCustomNamespace); xDoc.AppendChild(root); XmlElement styles = xDoc.CreateElement("Styles"); XmlElement styleBold = xDoc.CreateElement("Style"); XmlElement font = xDoc.CreateElement("Font"); XmlAttribute xStyleBoldID = xDoc.CreateAttribute("dc", "ID", strCustomNamespace); xStyleBoldID.Value = "dc1"; styleBold.Attributes.Append(xStyleBoldID); XmlAttribute xFontWeight = xDoc.CreateAttribute("dc", "Bold", strCustomNamespace); xFontWeight.Value = "1"; font.Attributes.Append(xFontWeight); styleBold.AppendChild(font); styles.AppendChild(styleBold); XmlElement styleDateTime = xDoc.CreateElement("Style"); XmlElement numberFormat = xDoc.CreateElement("NumberFormat"); XmlAttribute xStyleDateTimeID = xDoc.CreateAttribute("dc", "ID", strCustomNamespace); xStyleDateTimeID.Value = "dcDateTime"; styleDateTime.Attributes.Append(xStyleDateTimeID); XmlAttribute xStyleNumberFormat = xDoc.CreateAttribute("dc", "Format", strCustomNamespace); xStyleNumberFormat.Value = "General Date"; numberFormat.Attributes.Append(xStyleNumberFormat); styleDateTime.AppendChild(numberFormat); styles.AppendChild(styleDateTime); root.AppendChild(styles); // Populate worksheets foreach (Worksheet wSheet in Worksheets) { XmlElement worksheet = xDoc.CreateElement("Worksheet"); XmlAttribute xSheetTitle = xDoc.CreateAttribute("dc", "Name", strCustomNamespace); xSheetTitle.Value = System.Text.RegularExpressions.Regex.Replace(wSheet.Title, "[^a-z0-9 -]", "", System.Text.RegularExpressions.RegexOptions.IgnoreCase); worksheet.Attributes.Append(xSheetTitle); XmlElement table = xDoc.CreateElement("Table"); // Populate header row XmlElement header = xDoc.CreateElement("Row"); XmlAttribute xHeaderStyle = xDoc.CreateAttribute("dc", "StyleID", strCustomNamespace); xHeaderStyle.Value = "dc1"; header.Attributes.Append(xHeaderStyle); foreach (DataColumn col in wSheet.Data.Columns) { XmlElement headerCell = xDoc.CreateElement("Cell"); XmlElement headerData = xDoc.CreateElement("Data"); headerData.InnerText = col.ColumnName; XmlAttribute xHeaderDataType = xDoc.CreateAttribute("dc", "Type", strCustomNamespace); xHeaderDataType.Value = "String"; headerData.Attributes.Append(xHeaderDataType); headerCell.AppendChild(headerData); header.AppendChild(headerCell); } table.AppendChild(header); // Populate data rows foreach (DataRow drData in wSheet.Data.Rows) { XmlElement row = xDoc.CreateElement("Row"); foreach (DataColumn col in wSheet.Data.Columns) { XmlElement cell = xDoc.CreateElement("Cell"); XmlElement cellData = xDoc.CreateElement("Data"); XmlAttribute xCellDataType = xDoc.CreateAttribute("dc", "Type", strCustomNamespace); if (drData[col.ColumnName] == DBNull.Value) { cellData.InnerText = ""; xCellDataType.Value = "String"; } else { switch (col.DataType.Name) { case "Single": case "Double": case "Decimal": case "Int16": case "Int32": case "Int64": cellData.InnerText = drData[col.ColumnName].ToString(); xCellDataType.Value = "Number"; break; case "DateTime": XmlAttribute xCellStyleID = xDoc.CreateAttribute("dc", "StyleID", strCustomNamespace); xCellStyleID.Value = "dcDateTime"; cell.Attributes.Append(xCellStyleID); if (drData[col.ColumnName] != null && drData[col.ColumnName] != DBNull.Value && (DateTime)drData[col.ColumnName] != DateTime.MinValue) { cellData.InnerText = ((DateTime)drData[col.ColumnName]).ToString("o"); // ISO 8601 DateTime String Format } xCellDataType.Value = "DateTime"; break; case "Boolean": cellData.InnerText = (bool)drData[col.ColumnName] ? "1" : "0"; xCellDataType.Value = "Boolean"; break; default: cellData.InnerText = drData[col.ColumnName].ToString(); // XmlElement.InnerText escapes reserved XML characters automatically xCellDataType.Value = "String"; break; } } cellData.Attributes.Append(xCellDataType); cell.AppendChild(cellData); row.AppendChild(cell); } table.AppendChild(row); } worksheet.AppendChild(table); root.AppendChild(worksheet); } StringWriter swOut = new StringWriter(); xDoc.Save(swOut); return swOut.ToString(); } } }
If you're reading this and know a better way to do this sort of thing - PLEASE LET ME KNOW. I was driven to this because of the crap documentation on the web. I only achieved this limited success by reverse-engineering an existing Excel doc and latterly discovering the MS XML Spreadsheet Reference. Why they don't tell you that the XMLSS DateTime format is ISO 8601, I don't know...