mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-13 17:23:02 -05:00
965 lines
39 KiB
C#
965 lines
39 KiB
C#
//
|
|
// Copyright (c) Microsoft. All rights reserved.
|
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
//
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Data.SqlTypes;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using System.Xml;
|
|
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
|
|
|
|
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
|
|
{
|
|
// A xlsx file is a zip with specific folder structure.
|
|
// http://www.ecma-international.org/publications/standards/Ecma-376.htm
|
|
|
|
// The page number in the comments are based on
|
|
// ECMA-376, Fifth Edition, Part 1 - Fundamentals And Markup Language Reference
|
|
|
|
// Page 75, SpreadsheetML package structure
|
|
// |- [Content_Types].xml
|
|
// |- _rels
|
|
// |- .rels
|
|
// |- xl
|
|
// |- workbook.xml
|
|
// |- styles.xml
|
|
// |- _rels
|
|
// |- workbook.xml.rels
|
|
// |- worksheets
|
|
// |- sheet1.xml
|
|
|
|
/// <summary>
|
|
/// A helper class for write xlsx file base on ECMA-376. It tries to be minimal,
|
|
/// both in implementation and runtime allocation.
|
|
/// </summary>
|
|
/// <example>
|
|
/// This sample shows how to use the class
|
|
/// <code>
|
|
/// public class TestClass
|
|
/// {
|
|
/// public static int Main()
|
|
/// {
|
|
/// using (Stream stream = File.Create("test.xlsx"))
|
|
/// using (var helper = new SaveAsExcelFileStreamWriterHelper(stream, false))
|
|
/// using (var sheet = helper.AddSheet())
|
|
/// {
|
|
/// sheet.AddRow();
|
|
/// sheet.AddCell("string");
|
|
/// }
|
|
/// }
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
|
|
internal sealed class SaveAsExcelFileStreamWriterHelper : IDisposable
|
|
{
|
|
/// <summary>
|
|
/// Present a Excel sheet
|
|
/// </summary>
|
|
public sealed class ExcelSheet : IDisposable
|
|
{
|
|
// The excel epoch is 1/1/1900, but it has 1/0/1900 and 2/29/1900
|
|
// which is equal to set the epoch back two days to 12/30/1899
|
|
// new DateTime(1899,12,30).Ticks
|
|
private const long ExcelEpochTick = 599264352000000000L;
|
|
|
|
// Excel can not use date before 1/0/1900 and
|
|
// date before 3/1/1900 is wrong, off by 1 because of 2/29/1900
|
|
// thus, for any date before 3/1/1900, use string for date
|
|
// new DateTime(1900,3,1).Ticks
|
|
private const long ExcelDateCutoffTick = 599317056000000000L;
|
|
|
|
// new TimeSpan(24,0,0).Ticks
|
|
private const long TicksPerDay = 864000000000L;
|
|
|
|
// Digit pixel width for 11 point Calibri
|
|
private const float FontPixelWidth = 7;
|
|
|
|
private XmlWriter writer;
|
|
private ReferenceManager referenceManager;
|
|
private bool hasOpenRowTag;
|
|
|
|
private readonly int columnCount;
|
|
private bool autoFilterColumns;
|
|
private bool hasStartedSheetData;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the ExcelSheet class.
|
|
/// </summary>
|
|
/// <param name="writer">XmlWriter to write the sheet data</param>
|
|
/// <param name="columnCount">Number of columns in the new sheet</param>
|
|
internal ExcelSheet(XmlWriter writer, int columnCount)
|
|
{
|
|
this.writer = writer;
|
|
this.columnCount = columnCount;
|
|
|
|
writer.WriteStartDocument();
|
|
writer.WriteStartElement("worksheet", "http://schemas.openxmlformats.org/spreadsheetml/2006/main");
|
|
writer.WriteAttributeString("xmlns", "r", null, "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
|
|
|
|
referenceManager = new ReferenceManager(writer);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Start a new row
|
|
/// </summary>
|
|
public void AddRow()
|
|
{
|
|
// Write the open tag for sheetData if it hasn't been written yet
|
|
if (!hasStartedSheetData)
|
|
{
|
|
writer.WriteStartElement("sheetData");
|
|
hasStartedSheetData = true;
|
|
}
|
|
|
|
EndRowIfNeeded();
|
|
hasOpenRowTag = true;
|
|
|
|
referenceManager.AssureRowReference();
|
|
|
|
writer.WriteStartElement("row");
|
|
referenceManager.WriteAndIncreaseRowReference();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write a string cell
|
|
/// </summary>
|
|
/// <param name="value">String value to write</param>
|
|
/// <param name="bold">Whether the cell should be bold, defaults to false</param>
|
|
public void AddCell(string value, bool bold = false)
|
|
{
|
|
// string needs <c t="inlineStr"><is><t>string</t></is></c>
|
|
// This class uses inlineStr instead of more common shared string table
|
|
// to improve write performance and reduce implementation complexity
|
|
referenceManager.AssureColumnReference();
|
|
if (value == null)
|
|
{
|
|
AddCellEmpty();
|
|
return;
|
|
}
|
|
|
|
writer.WriteStartElement("c");
|
|
|
|
referenceManager.WriteAndIncreaseColumnReference();
|
|
|
|
writer.WriteAttributeString("t", "inlineStr");
|
|
|
|
// Write the style attribute set to the bold font if requested
|
|
if (bold)
|
|
{
|
|
writer.WriteAttributeString("s", "5");
|
|
}
|
|
|
|
writer.WriteStartElement("is");
|
|
writer.WriteStartElement("t");
|
|
writer.WriteValue(value);
|
|
writer.WriteEndElement(); // <t>
|
|
writer.WriteEndElement(); // <is>
|
|
|
|
writer.WriteEndElement(); // <c>
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write a object cell
|
|
/// </summary>
|
|
/// The program will try to output number/datetime, otherwise, call the ToString
|
|
/// <param name="dbCellValue">DbCellValue to write based on data type</param>
|
|
/// <param name="bold">Whether the cell should be bold, defaults to false</param>
|
|
public void AddCell(DbCellValue dbCellValue, bool bold = false)
|
|
{
|
|
object o = dbCellValue.RawObject;
|
|
if (dbCellValue.IsNull || o == null)
|
|
{
|
|
AddCellEmpty();
|
|
return;
|
|
}
|
|
switch (Type.GetTypeCode(o.GetType()))
|
|
{
|
|
case TypeCode.Boolean:
|
|
AddCell((bool)o);
|
|
break;
|
|
case TypeCode.Byte:
|
|
case TypeCode.Int16:
|
|
case TypeCode.Int32:
|
|
case TypeCode.Int64:
|
|
case TypeCode.Single:
|
|
case TypeCode.Double:
|
|
case TypeCode.Decimal:
|
|
AddCellBoxedNumber(o);
|
|
break;
|
|
case TypeCode.DateTime:
|
|
AddCell((DateTime)o);
|
|
break;
|
|
case TypeCode.String:
|
|
AddCell((string)o, bold);
|
|
break;
|
|
default:
|
|
if (o is TimeSpan span) //TimeSpan doesn't have TypeCode
|
|
{
|
|
AddCell(span);
|
|
}
|
|
// We need to handle SqlDecimal and SqlMoney types here because we can't convert them to .NET types due to different precisions in SQL Server and .NET.
|
|
else if (o is SqlDecimal || o is SqlMoney)
|
|
{
|
|
AddCellBoxedNumber(dbCellValue.DisplayValue);
|
|
}
|
|
else
|
|
{
|
|
AddCell(dbCellValue.DisplayValue, bold);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write a sheetView that freezes the top row. Must be called before any rows have been added.
|
|
/// </summary>
|
|
/// <exception cref="InvalidOperationException">Thrown if called after any rows have been added.</exception>
|
|
public void FreezeHeaderRow()
|
|
{
|
|
if (hasStartedSheetData)
|
|
{
|
|
throw new InvalidOperationException("Must be called before calling AddRow");
|
|
}
|
|
|
|
writer.WriteStartElement("sheetViews");
|
|
|
|
writer.WriteStartElement("sheetView");
|
|
writer.WriteAttributeString("tabSelected", "1");
|
|
writer.WriteAttributeString("workbookViewId", "0");
|
|
|
|
writer.WriteStartElement("pane");
|
|
writer.WriteAttributeString("ySplit", "1");
|
|
writer.WriteAttributeString("topLeftCell", "A2");
|
|
writer.WriteAttributeString("activePane", "bottomLeft");
|
|
writer.WriteAttributeString("state", "frozen");
|
|
writer.WriteEndElement(); // <pane>
|
|
|
|
writer.WriteStartElement("selection");
|
|
writer.WriteAttributeString("pane", "bottomLeft");
|
|
writer.WriteEndElement(); // <selection>
|
|
|
|
writer.WriteEndElement(); // <sheetView>
|
|
writer.WriteEndElement(); // <sheetViews>
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enable auto filtering, the XML will be written when closing the sheet later
|
|
/// </summary>
|
|
public void EnableAutoFilter()
|
|
{
|
|
autoFilterColumns = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write the columns widths. Must be called before any rows have been added.
|
|
/// </summary>
|
|
/// <param name="columnWidths">Array with the widths of each column.</param>
|
|
/// <exception cref="InvalidOperationException">Thrown if called after any rows have been added.</exception>
|
|
public void WriteColumnInformation(float[] columnWidths)
|
|
{
|
|
if (hasStartedSheetData)
|
|
{
|
|
throw new InvalidOperationException("Must be called before calling AddRow");
|
|
}
|
|
|
|
if (columnWidths.Length != this.columnCount)
|
|
{
|
|
throw new InvalidOperationException("Column count mismatch");
|
|
}
|
|
|
|
writer.WriteStartElement("cols");
|
|
|
|
for (int columnIndex = 0; columnIndex < columnWidths.Length; columnIndex++)
|
|
{
|
|
var columnWidth = Math.Truncate((columnWidths[columnIndex] + 15) / FontPixelWidth * 256) / 256;
|
|
|
|
writer.WriteStartElement("col");
|
|
writer.WriteAttributeString("min", (columnIndex + 1).ToString());
|
|
writer.WriteAttributeString("max", (columnIndex + 1).ToString());
|
|
writer.WriteAttributeString("width", columnWidth.ToString(CultureInfo.InvariantCulture));
|
|
writer.WriteAttributeString("bestFit", "1");
|
|
writer.WriteAttributeString("customWidth", "1");
|
|
writer.WriteEndElement(); // <col>
|
|
}
|
|
|
|
writer.WriteEndElement(); // <cols>
|
|
}
|
|
|
|
/// <summary>
|
|
/// Close the <row><sheetData><worksheet> tags and close the stream
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
EndRowIfNeeded();
|
|
writer.WriteEndElement(); // <sheetData>
|
|
|
|
// Write the auto filter XML if requested
|
|
if (autoFilterColumns)
|
|
{
|
|
writer.WriteStartElement("autoFilter");
|
|
writer.WriteAttributeString("ref", $"A1:{ReferenceManager.GetColumnName(this.columnCount)}1");
|
|
writer.WriteEndElement(); // <autoFilter>
|
|
}
|
|
|
|
writer.WriteEndElement(); // <worksheet>
|
|
writer.Dispose();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write a empty cell
|
|
/// </summary>
|
|
/// This only increases the internal bookmark and doesn't actually write out anything.
|
|
private void AddCellEmpty()
|
|
{
|
|
referenceManager.IncreaseColumnReference();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write a bool cell.
|
|
/// </summary>
|
|
/// <param name="value">Boolean value to write</param>
|
|
private void AddCell(bool value)
|
|
{
|
|
// Excel FALSE: <c r="A1" t="b"><v>0</v></c>
|
|
// Excel TRUE: <c r="A1" t="b"><v>1</v></c>
|
|
referenceManager.AssureColumnReference();
|
|
|
|
writer.WriteStartElement("c");
|
|
|
|
referenceManager.WriteAndIncreaseColumnReference();
|
|
|
|
writer.WriteAttributeString("t", "b");
|
|
|
|
writer.WriteStartElement("v");
|
|
if (value)
|
|
{
|
|
writer.WriteValue("1"); //use string to avoid convert
|
|
}
|
|
else
|
|
{
|
|
writer.WriteValue("0");
|
|
}
|
|
writer.WriteEndElement(); // <v>
|
|
|
|
writer.WriteEndElement(); // <c>
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write a TimeSpan cell.
|
|
/// </summary>
|
|
/// <param name="time">TimeSpan value to write</param>
|
|
private void AddCell(TimeSpan time)
|
|
{
|
|
referenceManager.AssureColumnReference();
|
|
double excelDate = (double)time.Ticks / (double)TicksPerDay;
|
|
// The default hh:mm:ss format do not support more than 24 hours
|
|
// For that case, use the format string [h]:mm:ss
|
|
if (time.Ticks >= TicksPerDay)
|
|
{
|
|
AddCellDateTimeInternal(excelDate, Style.TimeMoreThan24Hours);
|
|
}
|
|
else
|
|
{
|
|
AddCellDateTimeInternal(excelDate, Style.Time);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write a DateTime cell.
|
|
/// </summary>
|
|
/// <param name="dateTime">DateTime value to write</param>
|
|
/// <remark>
|
|
/// If the DateTime does not have date part, it will be written as datetime and show as time only
|
|
/// If the DateTime is before 1900-03-01, save as string because excel doesn't support them.
|
|
/// Otherwise, save as datetime, and if the time is 00:00:00, show as yyyy-MM-dd.
|
|
/// Show the datetime as yyyy-MM-dd HH:mm:ss if none of the previous situations
|
|
/// </remark>
|
|
private void AddCell(DateTime dateTime)
|
|
{
|
|
referenceManager.AssureColumnReference();
|
|
long ticks = dateTime.Ticks;
|
|
Style style = Style.DateTime;
|
|
double excelDate;
|
|
if (ticks < TicksPerDay) //date empty, time only
|
|
{
|
|
style = Style.Time;
|
|
excelDate = ((double)ticks) / (double)TicksPerDay;
|
|
}
|
|
else if (ticks < ExcelDateCutoffTick) //before excel cut-off, use string
|
|
{
|
|
AddCell(dateTime.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture));
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
if (ticks % TicksPerDay == 0) //time empty, date only
|
|
{
|
|
style = Style.Date;
|
|
}
|
|
excelDate = ((double)(ticks - ExcelEpochTick)) / (double)TicksPerDay;
|
|
}
|
|
AddCellDateTimeInternal(excelDate, style);
|
|
}
|
|
|
|
// number needs <c r="A1"><v>12.5</v></c>
|
|
private void AddCellBoxedNumber(object number)
|
|
{
|
|
referenceManager.AssureColumnReference();
|
|
|
|
writer.WriteStartElement("c");
|
|
|
|
referenceManager.WriteAndIncreaseColumnReference();
|
|
|
|
writer.WriteStartElement("v");
|
|
writer.WriteValue(number);
|
|
writer.WriteEndElement(); // <v>
|
|
|
|
writer.WriteEndElement(); // <c>
|
|
}
|
|
|
|
|
|
// datetime needs <c r="A1" s="2"><v>26012.451</v></c>
|
|
private void AddCellDateTimeInternal(double excelDate, Style style)
|
|
{
|
|
writer.WriteStartElement("c");
|
|
|
|
referenceManager.WriteAndIncreaseColumnReference();
|
|
|
|
writer.WriteStartAttribute("s");
|
|
writer.WriteValue((int)style);
|
|
writer.WriteEndAttribute();
|
|
|
|
writer.WriteStartElement("v");
|
|
writer.WriteValue(excelDate);
|
|
writer.WriteEndElement(); // <v>
|
|
|
|
writer.WriteEndElement(); // <c>
|
|
}
|
|
|
|
private void EndRowIfNeeded()
|
|
{
|
|
if (hasOpenRowTag)
|
|
{
|
|
writer.WriteEndElement(); // <row>
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper class to track the current cell reference.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// SpreadsheetML cell needs a reference attribute. (e.g. r="A1"). This class is used
|
|
/// to track the current cell reference.
|
|
/// </remarks>
|
|
internal class ReferenceManager
|
|
{
|
|
private int currColumn; // 0 is invalid, the first AddRow will set to 1
|
|
private int currRow = 1;
|
|
|
|
// In order to reduce allocation, current reference is saved in this array,
|
|
// and write to the XmlWriter through WriteChars.
|
|
// For example, when the reference has value AA15,
|
|
// The content of this array will be @AA15xxxxx, with currReferenceRowLength=2
|
|
// and currReferenceColumnLength=2
|
|
private char[] currReference = new char[3 + 7]; //maximal XFD1048576
|
|
private int currReferenceRowLength;
|
|
private int currReferenceColumnLength;
|
|
|
|
private XmlWriter writer;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the ReferenceManager class.
|
|
/// </summary>
|
|
/// <param name="writer">XmlWriter to write the reference attribute to.</param>
|
|
public ReferenceManager(XmlWriter writer)
|
|
{
|
|
this.writer = writer;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check that we have not write too many columns. (xlsx has a limit of 16384 columns)
|
|
/// </summary>
|
|
public void AssureColumnReference()
|
|
{
|
|
if (currColumn == 0)
|
|
{
|
|
throw new InvalidOperationException("AddRow must be called before AddCell");
|
|
|
|
}
|
|
if (currColumn > 16384)
|
|
{
|
|
throw new InvalidOperationException("max column number is 16384, see https://support.office.com/en-us/article/Excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write out the r="A1" attribute and increase the column number of internal bookmark
|
|
/// </summary>
|
|
public void WriteAndIncreaseColumnReference()
|
|
{
|
|
writer.WriteStartAttribute("r");
|
|
writer.WriteChars(currReference, 3 - currReferenceColumnLength, currReferenceRowLength + currReferenceColumnLength);
|
|
writer.WriteEndAttribute();
|
|
IncreaseColumnReference();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Increase the column of internal bookmark.
|
|
/// </summary>
|
|
public void IncreaseColumnReference()
|
|
{
|
|
// This function change the first three chars of currReference array
|
|
// The logic is simple, when a start a new row, the array is reset to @@A
|
|
// where @='A'-1. At each increase, check if the current reference is Z
|
|
// and move to AA if needed, since the maximal is 16384, or XFD, the code
|
|
// manipulates the array element directly instead of loop
|
|
char[] reference = currReference;
|
|
currColumn++;
|
|
if ('Z' == reference[2]++)
|
|
{
|
|
reference[2] = 'A';
|
|
if (currReferenceColumnLength < 2)
|
|
{
|
|
currReferenceColumnLength = 2;
|
|
}
|
|
if ('Z' == reference[1]++)
|
|
{
|
|
reference[0]++;
|
|
reference[1] = 'A';
|
|
currReferenceColumnLength = 3;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the column name (letters) from a column number
|
|
/// https://stackoverflow.com/questions/181596/how-to-convert-a-column-number-e-g-127-into-an-excel-column-e-g-aa
|
|
/// </summary>
|
|
/// <param name="columnNumber">The column number.</param>
|
|
/// <returns>The column name.</returns>
|
|
public static string GetColumnName(int columnNumber)
|
|
{
|
|
string columnName = "";
|
|
|
|
while (columnNumber > 0)
|
|
{
|
|
int modulo = (columnNumber - 1) % 26;
|
|
columnName = Convert.ToChar('A' + modulo) + columnName;
|
|
columnNumber = (columnNumber - modulo) / 26;
|
|
}
|
|
|
|
return columnName;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check that we have not write too many rows. (xlsx has a limit of 1048576 rows)
|
|
/// </summary>
|
|
public void AssureRowReference()
|
|
{
|
|
if (currRow > 1048576)
|
|
{
|
|
throw new InvalidOperationException("max row number is 1048576, see https://support.office.com/en-us/article/Excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3");
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// Write out the r="1" attribute and increase the row number of internal bookmark
|
|
/// </summary>
|
|
public void WriteAndIncreaseRowReference()
|
|
{
|
|
writer.WriteStartAttribute("r");
|
|
writer.WriteValue(currRow);
|
|
writer.WriteEndAttribute();
|
|
|
|
ResetColumnReference(); //This need to be called before the increase
|
|
|
|
currRow++;
|
|
}
|
|
|
|
// Reset the Column Reference
|
|
// This will reset the first three chars of currReference array to '@@A'
|
|
// and the rest to the array to the string presentation of the current row.
|
|
private void ResetColumnReference()
|
|
{
|
|
currColumn = 1;
|
|
currReference[0] = currReference[1] = (char)('A' - 1);
|
|
currReference[2] = 'A';
|
|
currReferenceColumnLength = 1;
|
|
|
|
string rowReference = XmlConvert.ToString(currRow);
|
|
currReferenceRowLength = rowReference.Length;
|
|
rowReference.CopyTo(0, currReference, 3, rowReference.Length);
|
|
}
|
|
}
|
|
|
|
private enum Style
|
|
{
|
|
Normal = 0,
|
|
Date = 1,
|
|
Time = 2,
|
|
DateTime = 3,
|
|
TimeMoreThan24Hours = 4,
|
|
}
|
|
|
|
private ZipArchive zipArchive;
|
|
private List<string> sheetNames = new List<string>();
|
|
private XmlWriterSettings writerSetting = new XmlWriterSettings()
|
|
{
|
|
CloseOutput = true,
|
|
};
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the SaveAsExcelFileStreamWriterHelper class.
|
|
/// </summary>
|
|
/// <param name="stream">The input or output stream.</param>
|
|
public SaveAsExcelFileStreamWriterHelper(Stream stream)
|
|
{
|
|
zipArchive = new ZipArchive(stream, ZipArchiveMode.Create, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the SaveAsExcelFileStreamWriterHelper class.
|
|
/// </summary>
|
|
/// <param name="stream">The input or output stream.</param>
|
|
/// <param name="leaveOpen">true to leave the stream open after the
|
|
/// SaveAsExcelFileStreamWriterHelper object is disposed; otherwise, false.</param>
|
|
public SaveAsExcelFileStreamWriterHelper(Stream stream, bool leaveOpen)
|
|
{
|
|
zipArchive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add sheet inside the Xlsx file.
|
|
/// </summary>
|
|
/// <param name="sheetName">Sheet name</param>
|
|
/// <returns>ExcelSheet for writing the sheet content</returns>
|
|
/// <remarks>
|
|
/// When the sheetName is null, sheet1,sheet2,..., will be used.
|
|
/// The following characters are not allowed in the sheetName
|
|
/// '\', '/','*','[',']',':','?'
|
|
/// </remarks>
|
|
public ExcelSheet AddSheet(string sheetName, int columnCount)
|
|
{
|
|
string sheetFileName = "sheet" + (sheetNames.Count + 1);
|
|
sheetName ??= sheetFileName;
|
|
EnsureValidSheetName(sheetName);
|
|
|
|
sheetNames.Add(sheetName);
|
|
XmlWriter sheetWriter = AddEntry($"xl/worksheets/{sheetFileName}.xml");
|
|
return new ExcelSheet(sheetWriter, columnCount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write out the rest of the xlsx files and release the resources used by the current instance
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
WriteMinimalTemplate();
|
|
zipArchive.Dispose();
|
|
}
|
|
|
|
|
|
private XmlWriter AddEntry(string entryName)
|
|
{
|
|
ZipArchiveEntry entry = zipArchive.CreateEntry(entryName, CompressionLevel.Fastest);
|
|
return XmlWriter.Create(entry.Open(), writerSetting);
|
|
}
|
|
|
|
//ECMA-376 page 75
|
|
private void WriteMinimalTemplate()
|
|
{
|
|
WriteTopRel();
|
|
WriteWorkbook();
|
|
WriteStyle();
|
|
WriteContentType();
|
|
WriteWorkbookRel();
|
|
}
|
|
|
|
/// <summary>
|
|
/// write [Content_Types].xml
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This file need to describe all the files in the zip.
|
|
/// </remarks>
|
|
private void WriteContentType()
|
|
{
|
|
using (XmlWriter xw = AddEntry("[Content_Types].xml"))
|
|
{
|
|
xw.WriteStartDocument();
|
|
xw.WriteStartElement("Types", "http://schemas.openxmlformats.org/package/2006/content-types");
|
|
|
|
xw.WriteStartElement("Default");
|
|
xw.WriteAttributeString("Extension", "rels");
|
|
xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-package.relationships+xml");
|
|
xw.WriteEndElement(); // <Default>
|
|
|
|
xw.WriteStartElement("Override");
|
|
xw.WriteAttributeString("PartName", "/xl/workbook.xml");
|
|
xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml");
|
|
xw.WriteEndElement(); // <Override>
|
|
|
|
xw.WriteStartElement("Override");
|
|
xw.WriteAttributeString("PartName", "/xl/styles.xml");
|
|
xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml");
|
|
xw.WriteEndElement(); // <Override>
|
|
|
|
for (int i = 1; i <= sheetNames.Count; ++i)
|
|
{
|
|
xw.WriteStartElement("Override");
|
|
xw.WriteAttributeString("PartName", "/xl/worksheets/sheet" + i + ".xml");
|
|
xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml");
|
|
xw.WriteEndElement(); // <Override>
|
|
}
|
|
xw.WriteEndElement(); // <Types>
|
|
xw.WriteEndDocument();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write _rels/.rels. This file only need to reference main workbook
|
|
/// </summary>
|
|
private void WriteTopRel()
|
|
{
|
|
using (XmlWriter xw = AddEntry("_rels/.rels"))
|
|
{
|
|
xw.WriteStartDocument();
|
|
|
|
xw.WriteStartElement("Relationships", "http://schemas.openxmlformats.org/package/2006/relationships");
|
|
|
|
xw.WriteStartElement("Relationship");
|
|
xw.WriteAttributeString("Id", "rId1");
|
|
xw.WriteAttributeString("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument");
|
|
xw.WriteAttributeString("Target", "xl/workbook.xml");
|
|
xw.WriteEndElement(); // <Relationship>
|
|
|
|
xw.WriteEndElement(); // <Relationships>
|
|
|
|
xw.WriteEndDocument();
|
|
}
|
|
}
|
|
|
|
private static char[] invalidSheetNameCharacters = new char[]
|
|
{
|
|
'\\', '/','*','[',']',':','?'
|
|
};
|
|
private void EnsureValidSheetName(string sheetName)
|
|
{
|
|
if (sheetName.IndexOfAny(invalidSheetNameCharacters) != -1)
|
|
{
|
|
throw new ArgumentException($"Invalid sheetname: sheetName");
|
|
}
|
|
if (sheetNames.IndexOf(sheetName) != -1)
|
|
{
|
|
throw new ArgumentException($"Duplicate sheetName: {sheetName}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write xl/workbook.xml. This file will references the sheets through ids in xl/_rels/workbook.xml.rels
|
|
/// </summary>
|
|
private void WriteWorkbook()
|
|
{
|
|
using (XmlWriter xw = AddEntry("xl/workbook.xml"))
|
|
{
|
|
xw.WriteStartDocument();
|
|
xw.WriteStartElement("workbook", "http://schemas.openxmlformats.org/spreadsheetml/2006/main");
|
|
xw.WriteAttributeString("xmlns", "r", null, "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
|
|
xw.WriteStartElement("sheets");
|
|
for (int i = 1; i <= sheetNames.Count; i++)
|
|
{
|
|
xw.WriteStartElement("sheet");
|
|
xw.WriteAttributeString("name", sheetNames[i - 1]);
|
|
xw.WriteAttributeString("sheetId", i.ToString());
|
|
xw.WriteAttributeString("r", "id", null, "rId" + i);
|
|
xw.WriteEndElement(); // <sheet>
|
|
}
|
|
xw.WriteEndDocument();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write xl/_rels/workbook.xml.rels. This file will have the paths of the style and sheets.
|
|
/// </summary>
|
|
private void WriteWorkbookRel()
|
|
{
|
|
using (XmlWriter xw = AddEntry("xl/_rels/workbook.xml.rels"))
|
|
{
|
|
xw.WriteStartDocument();
|
|
xw.WriteStartElement("Relationships", "http://schemas.openxmlformats.org/package/2006/relationships");
|
|
|
|
xw.WriteStartElement("Relationship");
|
|
xw.WriteAttributeString("Id", "rId0");
|
|
xw.WriteAttributeString("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles");
|
|
xw.WriteAttributeString("Target", "styles.xml");
|
|
xw.WriteEndElement(); // <Relationship>
|
|
|
|
for (int i = 1; i <= sheetNames.Count; i++)
|
|
{
|
|
xw.WriteStartElement("Relationship");
|
|
xw.WriteAttributeString("Id", "rId" + i);
|
|
xw.WriteAttributeString("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet");
|
|
xw.WriteAttributeString("Target", "worksheets/sheet" + i + ".xml");
|
|
xw.WriteEndElement(); // <Relationship>
|
|
}
|
|
xw.WriteEndElement(); // <Relationships>
|
|
xw.WriteEndDocument();
|
|
}
|
|
}
|
|
|
|
// Write the xl/styles.xml
|
|
private void WriteStyle()
|
|
{
|
|
// the style 0 is used for general case, style 1 for date, style 2 for time and style 3 for datetime see Enum Style
|
|
// reference chain: (index start with 0)
|
|
// <c>(in sheet1.xml) --> (by s) <cellXfs> --> (by xfId) <cellStyleXfs>
|
|
// --> (by numFmtId) <numFmts>
|
|
// that is <c s="1"></c> will reference the second element of <cellXfs> <xf numFmtId=""162"" xfId=""0"" applyNumberFormat=""1""/>
|
|
// then, this xf reference numFmt by name and get formatCode "hh:mm:ss"
|
|
|
|
using (XmlWriter xw = AddEntry("xl/styles.xml"))
|
|
{
|
|
xw.WriteStartElement("styleSheet", "http://schemas.openxmlformats.org/spreadsheetml/2006/main");
|
|
|
|
xw.WriteStartElement("numFmts");
|
|
xw.WriteAttributeString("count", "4");
|
|
xw.WriteStartElement("numFmt");
|
|
xw.WriteAttributeString("numFmtId", "166");
|
|
xw.WriteAttributeString("formatCode", "yyyy-mm-dd");
|
|
xw.WriteEndElement(); // <numFmt>
|
|
xw.WriteStartElement("numFmt");
|
|
xw.WriteAttributeString("numFmtId", "167");
|
|
xw.WriteAttributeString("formatCode", "hh:mm:ss");
|
|
xw.WriteEndElement(); // <numFmt>
|
|
xw.WriteStartElement("numFmt");
|
|
xw.WriteAttributeString("numFmtId", "168");
|
|
xw.WriteAttributeString("formatCode", "yyyy-mm-dd hh:mm:ss");
|
|
xw.WriteEndElement(); // <numFmt>
|
|
xw.WriteStartElement("numFmt");
|
|
xw.WriteAttributeString("numFmtId", "169");
|
|
xw.WriteAttributeString("formatCode", "[h]:mm:ss");
|
|
xw.WriteEndElement(); // <numFmt>
|
|
xw.WriteEndElement(); // <numFmts>
|
|
|
|
|
|
xw.WriteStartElement("fonts");
|
|
xw.WriteAttributeString("count", "2");
|
|
|
|
xw.WriteStartElement("font");
|
|
xw.WriteStartElement("sz");
|
|
xw.WriteAttributeString("val", "11");
|
|
xw.WriteEndElement(); // <sz>
|
|
xw.WriteStartElement("color");
|
|
xw.WriteAttributeString("theme", "1");
|
|
xw.WriteEndElement(); // <color>
|
|
xw.WriteStartElement("name");
|
|
xw.WriteAttributeString("val", "Calibri");
|
|
xw.WriteEndElement(); // <name>
|
|
xw.WriteStartElement("family");
|
|
xw.WriteAttributeString("val", "2");
|
|
xw.WriteEndElement(); // <family>
|
|
xw.WriteStartElement("scheme");
|
|
xw.WriteAttributeString("val", "minor");
|
|
xw.WriteEndElement(); // <scheme>
|
|
xw.WriteEndElement(); // <font>
|
|
|
|
xw.WriteStartElement("font");
|
|
xw.WriteStartElement("b");
|
|
xw.WriteEndElement(); // <b>
|
|
xw.WriteStartElement("sz");
|
|
xw.WriteAttributeString("val", "11");
|
|
xw.WriteEndElement(); // <sz>
|
|
xw.WriteStartElement("color");
|
|
xw.WriteAttributeString("theme", "1");
|
|
xw.WriteEndElement(); // <color>
|
|
xw.WriteStartElement("name");
|
|
xw.WriteAttributeString("val", "Calibri");
|
|
xw.WriteEndElement(); // <name>
|
|
xw.WriteStartElement("family");
|
|
xw.WriteAttributeString("val", "2");
|
|
xw.WriteEndElement(); // <family>
|
|
xw.WriteStartElement("scheme");
|
|
xw.WriteAttributeString("val", "minor");
|
|
xw.WriteEndElement(); // <scheme>
|
|
xw.WriteEndElement(); // <font>
|
|
|
|
xw.WriteEndElement(); // fonts
|
|
|
|
xw.WriteStartElement("fills");
|
|
xw.WriteAttributeString("count", "1");
|
|
xw.WriteStartElement("fill");
|
|
xw.WriteStartElement("patternFill");
|
|
xw.WriteAttributeString("patternType", "none");
|
|
xw.WriteEndElement(); // <patternFill>
|
|
xw.WriteEndElement(); // <fill>
|
|
xw.WriteEndElement(); // <fills>
|
|
|
|
xw.WriteStartElement("borders");
|
|
xw.WriteAttributeString("count", "1");
|
|
xw.WriteStartElement("border");
|
|
xw.WriteElementString("left", null);
|
|
xw.WriteElementString("right", null);
|
|
xw.WriteElementString("top", null);
|
|
xw.WriteElementString("bottom", null);
|
|
xw.WriteElementString("diagonal", null);
|
|
xw.WriteEndElement(); // <border>
|
|
xw.WriteEndElement(); // <borders>
|
|
|
|
xw.WriteStartElement("cellStyleXfs");
|
|
xw.WriteAttributeString("count", "1");
|
|
xw.WriteStartElement("xf");
|
|
xw.WriteAttributeString("numFmtId", "0");
|
|
xw.WriteAttributeString("fontId", "0");
|
|
xw.WriteAttributeString("fillId", "0");
|
|
xw.WriteAttributeString("borderId", "0");
|
|
xw.WriteEndElement(); // <xf>
|
|
xw.WriteEndElement(); // <cellStyleXfs>
|
|
|
|
xw.WriteStartElement("cellXfs");
|
|
xw.WriteAttributeString("count", "6");
|
|
xw.WriteStartElement("xf");
|
|
xw.WriteAttributeString("xfId", "0");
|
|
xw.WriteEndElement(); // <xf>
|
|
xw.WriteStartElement("xf");
|
|
xw.WriteAttributeString("numFmtId", "166");
|
|
xw.WriteAttributeString("xfId", "0");
|
|
xw.WriteAttributeString("applyNumberFormat", "1");
|
|
xw.WriteEndElement(); // <xf>
|
|
xw.WriteStartElement("xf");
|
|
xw.WriteAttributeString("numFmtId", "167");
|
|
xw.WriteAttributeString("xfId", "0");
|
|
xw.WriteAttributeString("applyNumberFormat", "1");
|
|
xw.WriteEndElement(); // <xf>
|
|
xw.WriteStartElement("xf");
|
|
xw.WriteAttributeString("numFmtId", "168");
|
|
xw.WriteAttributeString("xfId", "0");
|
|
xw.WriteAttributeString("applyNumberFormat", "1");
|
|
xw.WriteEndElement(); // <xf>
|
|
xw.WriteStartElement("xf");
|
|
xw.WriteAttributeString("numFmtId", "169");
|
|
xw.WriteAttributeString("xfId", "0");
|
|
xw.WriteAttributeString("applyNumberFormat", "1");
|
|
xw.WriteEndElement(); // <xf>
|
|
xw.WriteStartElement("xf");
|
|
xw.WriteAttributeString("xfId", "0");
|
|
xw.WriteAttributeString("fontId", "1");
|
|
xw.WriteEndElement(); // <xf>
|
|
xw.WriteEndElement(); // <cellXfs>
|
|
|
|
xw.WriteStartElement("cellStyles");
|
|
xw.WriteAttributeString("count", "1");
|
|
xw.WriteStartElement("cellStyle");
|
|
xw.WriteAttributeString("name", "Normal");
|
|
xw.WriteAttributeString("builtinId", "0");
|
|
xw.WriteAttributeString("xfId", "0");
|
|
xw.WriteEndElement(); // <cellStyle>
|
|
xw.WriteEndElement(); // <cellStyles>
|
|
}
|
|
}
|
|
}
|
|
}
|