Improves "save as Excel" functionality (#2266)

This commit is contained in:
2023-10-11 19:32:04 -04:00
committed by GitHub
parent a3b42e43bd
commit ad0aedb0a8
15 changed files with 969 additions and 121 deletions

View File

@@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Data.SqlTypes;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Xml;
@@ -75,21 +76,31 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
// 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>
internal ExcelSheet(XmlWriter writer)
/// <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");
writer.WriteStartElement("sheetData");
referenceManager = new ReferenceManager(writer);
}
@@ -98,6 +109,13 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
/// </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;
@@ -110,8 +128,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
/// <summary>
/// Write a string cell
/// </summary>
/// <param name="value">string value to write</param>
public void AddCell(string value)
/// <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
@@ -129,21 +148,28 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
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();
writer.WriteEndElement();
writer.WriteEndElement(); // <t>
writer.WriteEndElement(); // <is>
writer.WriteEndElement();
writer.WriteEndElement(); // <c>
}
/// <summary>
/// Write a object cell
/// </summary>
/// The program will try to output number/datetime, otherwise, call the ToString
/// <param name="o"></param>
public void AddCell(DbCellValue dbCellValue)
/// <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)
@@ -169,25 +195,100 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
AddCell((DateTime)o);
break;
case TypeCode.String:
AddCell((string)o);
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 precisons in SQL Server and .NET.
// 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);
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
@@ -196,6 +297,15 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
{
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();
}
@@ -203,7 +313,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
/// <summary>
/// Write a empty cell
/// </summary>
/// This only increases the internal bookmark and doesn't arcturally write out anything.
/// This only increases the internal bookmark and doesn't actually write out anything.
private void AddCellEmpty()
{
referenceManager.IncreaseColumnReference();
@@ -212,7 +322,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
/// <summary>
/// Write a bool cell.
/// </summary>
/// <param name="time"></param>
/// <param name="value">Boolean value to write</param>
private void AddCell(bool value)
{
// Excel FALSE: <c r="A1" t="b"><v>0</v></c>
@@ -234,15 +344,15 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
{
writer.WriteValue("0");
}
writer.WriteEndElement();
writer.WriteEndElement(); // <v>
writer.WriteEndElement();
writer.WriteEndElement(); // <c>
}
/// <summary>
/// Write a TimeSpan cell.
/// </summary>
/// <param name="time"></param>
/// <param name="time">TimeSpan value to write</param>
private void AddCell(TimeSpan time)
{
referenceManager.AssureColumnReference();
@@ -262,7 +372,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
/// <summary>
/// Write a DateTime cell.
/// </summary>
/// <param name="dateTime">Datetime</param>
/// <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.
@@ -307,9 +417,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
writer.WriteStartElement("v");
writer.WriteValue(number);
writer.WriteEndElement();
writer.WriteEndElement(); // <v>
writer.WriteEndElement();
writer.WriteEndElement(); // <c>
}
@@ -326,9 +436,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
writer.WriteStartElement("v");
writer.WriteValue(excelDate);
writer.WriteEndElement();
writer.WriteEndElement(); // <v>
writer.WriteEndElement();
writer.WriteEndElement(); // <c>
}
private void EndRowIfNeeded()
@@ -338,8 +448,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
writer.WriteEndElement(); // <row>
}
}
}
/// <summary>
@@ -429,6 +537,26 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
}
}
/// <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>
@@ -511,11 +639,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
/// <param name="sheetName">Sheet name</param>
/// <returns>ExcelSheet for writing the sheet content</returns>
/// <remarks>
/// When the sheetName is null, sheet1,shhet2,..., will be used.
/// The following charactors are not allowed in the sheetName
/// 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 = null)
public ExcelSheet AddSheet(string sheetName, int columnCount)
{
string sheetFileName = "sheet" + (sheetNames.Count + 1);
sheetName ??= sheetFileName;
@@ -523,7 +651,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
sheetNames.Add(sheetName);
XmlWriter sheetWriter = AddEntry($"xl/worksheets/{sheetFileName}.xml");
return new ExcelSheet(sheetWriter);
return new ExcelSheet(sheetWriter, columnCount);
}
/// <summary>
@@ -568,26 +696,26 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
xw.WriteStartElement("Default");
xw.WriteAttributeString("Extension", "rels");
xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-package.relationships+xml");
xw.WriteEndElement();
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();
xw.WriteEndElement(); // <Override>
xw.WriteStartElement("Override");
xw.WriteAttributeString("PartName", "/xl/styles.xml");
xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml");
xw.WriteEndElement();
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();
xw.WriteEndElement(); // <Override>
}
xw.WriteEndElement();
xw.WriteEndElement(); // <Types>
xw.WriteEndDocument();
}
}
@@ -607,9 +735,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
xw.WriteAttributeString("Id", "rId1");
xw.WriteAttributeString("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument");
xw.WriteAttributeString("Target", "xl/workbook.xml");
xw.WriteEndElement();
xw.WriteEndElement(); // <Relationship>
xw.WriteEndElement();
xw.WriteEndElement(); // <Relationships>
xw.WriteEndDocument();
}
@@ -648,7 +776,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
xw.WriteAttributeString("name", sheetNames[i - 1]);
xw.WriteAttributeString("sheetId", i.ToString());
xw.WriteAttributeString("r", "id", null, "rId" + i);
xw.WriteEndElement();
xw.WriteEndElement(); // <sheet>
}
xw.WriteEndDocument();
}
@@ -668,7 +796,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
xw.WriteAttributeString("Id", "rId0");
xw.WriteAttributeString("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles");
xw.WriteAttributeString("Target", "styles.xml");
xw.WriteEndElement();
xw.WriteEndElement(); // <Relationship>
for (int i = 1; i <= sheetNames.Count; i++)
{
@@ -676,9 +804,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
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();
xw.WriteEndElement(); // <Relationship>
}
xw.WriteEndElement();
xw.WriteEndElement(); // <Relationships>
xw.WriteEndDocument();
}
}
@@ -702,41 +830,63 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
xw.WriteStartElement("numFmt");
xw.WriteAttributeString("numFmtId", "166");
xw.WriteAttributeString("formatCode", "yyyy-mm-dd");
xw.WriteEndElement();
xw.WriteEndElement(); // <numFmt>
xw.WriteStartElement("numFmt");
xw.WriteAttributeString("numFmtId", "167");
xw.WriteAttributeString("formatCode", "hh:mm:ss");
xw.WriteEndElement();
xw.WriteEndElement(); // <numFmt>
xw.WriteStartElement("numFmt");
xw.WriteAttributeString("numFmtId", "168");
xw.WriteAttributeString("formatCode", "yyyy-mm-dd hh:mm:ss");
xw.WriteEndElement();
xw.WriteEndElement(); // <numFmt>
xw.WriteStartElement("numFmt");
xw.WriteAttributeString("numFmtId", "169");
xw.WriteAttributeString("formatCode", "[h]:mm:ss");
xw.WriteEndElement();
xw.WriteEndElement(); //mumFmts
xw.WriteEndElement(); // <numFmt>
xw.WriteEndElement(); // <numFmts>
xw.WriteStartElement("fonts");
xw.WriteAttributeString("count", "1");
xw.WriteAttributeString("count", "2");
xw.WriteStartElement("font");
xw.WriteStartElement("sz");
xw.WriteAttributeString("val", "11");
xw.WriteEndElement();
xw.WriteEndElement(); // <sz>
xw.WriteStartElement("color");
xw.WriteAttributeString("theme", "1");
xw.WriteEndElement();
xw.WriteEndElement(); // <color>
xw.WriteStartElement("name");
xw.WriteAttributeString("val", "Calibri");
xw.WriteEndElement();
xw.WriteEndElement(); // <name>
xw.WriteStartElement("family");
xw.WriteAttributeString("val", "2");
xw.WriteEndElement();
xw.WriteEndElement(); // <family>
xw.WriteStartElement("scheme");
xw.WriteAttributeString("val", "minor");
xw.WriteEndElement();
xw.WriteEndElement(); // font
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");
@@ -744,9 +894,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
xw.WriteStartElement("fill");
xw.WriteStartElement("patternFill");
xw.WriteAttributeString("patternType", "none");
xw.WriteEndElement(); // patternFill
xw.WriteEndElement(); // fill
xw.WriteEndElement(); // fills
xw.WriteEndElement(); // <patternFill>
xw.WriteEndElement(); // <fill>
xw.WriteEndElement(); // <fills>
xw.WriteStartElement("borders");
xw.WriteAttributeString("count", "1");
@@ -756,8 +906,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
xw.WriteElementString("top", null);
xw.WriteElementString("bottom", null);
xw.WriteElementString("diagonal", null);
xw.WriteEndElement(); // board
xw.WriteEndElement(); // borders
xw.WriteEndElement(); // <border>
xw.WriteEndElement(); // <borders>
xw.WriteStartElement("cellStyleXfs");
xw.WriteAttributeString("count", "1");
@@ -766,35 +916,39 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
xw.WriteAttributeString("fontId", "0");
xw.WriteAttributeString("fillId", "0");
xw.WriteAttributeString("borderId", "0");
xw.WriteEndElement(); // xf
xw.WriteEndElement(); // cellStyleXfs
xw.WriteEndElement(); // <xf>
xw.WriteEndElement(); // <cellStyleXfs>
xw.WriteStartElement("cellXfs");
xw.WriteAttributeString("count", "5");
xw.WriteAttributeString("count", "6");
xw.WriteStartElement("xf");
xw.WriteAttributeString("xfId", "0");
xw.WriteEndElement();
xw.WriteEndElement(); // <xf>
xw.WriteStartElement("xf");
xw.WriteAttributeString("numFmtId", "166");
xw.WriteAttributeString("xfId", "0");
xw.WriteAttributeString("applyNumberFormat", "1");
xw.WriteEndElement();
xw.WriteEndElement(); // <xf>
xw.WriteStartElement("xf");
xw.WriteAttributeString("numFmtId", "167");
xw.WriteAttributeString("xfId", "0");
xw.WriteAttributeString("applyNumberFormat", "1");
xw.WriteEndElement();
xw.WriteEndElement(); // <xf>
xw.WriteStartElement("xf");
xw.WriteAttributeString("numFmtId", "168");
xw.WriteAttributeString("xfId", "0");
xw.WriteAttributeString("applyNumberFormat", "1");
xw.WriteEndElement();
xw.WriteEndElement(); // <xf>
xw.WriteStartElement("xf");
xw.WriteAttributeString("numFmtId", "169");
xw.WriteAttributeString("xfId", "0");
xw.WriteAttributeString("applyNumberFormat", "1");
xw.WriteEndElement();
xw.WriteEndElement(); // cellXfs
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");
@@ -802,8 +956,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
xw.WriteAttributeString("name", "Normal");
xw.WriteAttributeString("builtinId", "0");
xw.WriteAttributeString("xfId", "0");
xw.WriteEndElement(); // cellStyle
xw.WriteEndElement(); // cellStyles
xw.WriteEndElement(); // <cellStyle>
xw.WriteEndElement(); // <cellStyles>
}
}
}