mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-13 17:23:02 -05:00
Improves "save as Excel" functionality (#2266)
This commit is contained in:
@@ -57,6 +57,8 @@
|
||||
<PackageReference Update="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Update="coverlet.msbuild" Version="6.0.0" />
|
||||
<PackageReference Update="Microsoft.SqlServer.XEvent.XELite" Version="2023.1.30.3" />
|
||||
<PackageReference Update="SkiaSharp" Version="2.88.6" />
|
||||
<PackageReference Update="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.6" Condition="$([MSBuild]::IsOsPlatform('Linux'))" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- When updating version of Dependencies in the below section, please also update the version in the following files:
|
||||
|
||||
@@ -50,6 +50,8 @@
|
||||
<PackageReference Include="Microsoft.SqlServer.Management.SmoMetadataProvider" />
|
||||
<PackageReference Include="Microsoft.SqlServer.SqlManagementObjects" />
|
||||
<PackageReference Include="Microsoft.SqlServer.Management.SqlParser" />
|
||||
<PackageReference Include="SkiaSharp" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Condition="$([MSBuild]::IsOsPlatform('Linux'))" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" />
|
||||
<PackageReference Include="Microsoft.SqlServer.XEvent.XELite" />
|
||||
|
||||
@@ -115,6 +115,26 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
/// Include headers of columns in Excel
|
||||
/// </summary>
|
||||
public bool IncludeHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Freeze header row in Excel
|
||||
/// </summary>
|
||||
public bool FreezeHeaderRow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Bold header row in Excel
|
||||
/// </summary>
|
||||
public bool BoldHeaderRow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable auto filter on header row in Excel
|
||||
/// </summary>
|
||||
public bool AutoFilterHeaderRow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Auto size columns in Excel
|
||||
/// </summary>
|
||||
public bool AutoSizeColumns { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -157,6 +157,30 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
get { return this.GetOptionValue<int>(SerializationOptionsHelper.MaxCharsToStore); }
|
||||
set { this.SetOptionValue<int>(SerializationOptionsHelper.Formatted, value); }
|
||||
}
|
||||
|
||||
public bool FreezeHeaderRow
|
||||
{
|
||||
get { return this.GetOptionValue<bool>(SerializationOptionsHelper.FreezeHeaderRow); }
|
||||
set { this.SetOptionValue<bool>(SerializationOptionsHelper.FreezeHeaderRow, value); }
|
||||
}
|
||||
|
||||
public bool BoldHeaderRow
|
||||
{
|
||||
get { return this.GetOptionValue<bool>(SerializationOptionsHelper.BoldHeaderRow); }
|
||||
set { this.SetOptionValue<bool>(SerializationOptionsHelper.BoldHeaderRow, value); }
|
||||
}
|
||||
|
||||
public bool AutoFilterHeaderRow
|
||||
{
|
||||
get { return this.GetOptionValue<bool>(SerializationOptionsHelper.AutoFilterHeaderRow); }
|
||||
set { this.SetOptionValue<bool>(SerializationOptionsHelper.AutoFilterHeaderRow, value); }
|
||||
}
|
||||
|
||||
public bool AutoSizeColumns
|
||||
{
|
||||
get { return this.GetOptionValue<bool>(SerializationOptionsHelper.AutoSizeColumns); }
|
||||
set { this.SetOptionValue<bool>(SerializationOptionsHelper.AutoSizeColumns, value); }
|
||||
}
|
||||
}
|
||||
|
||||
public class SerializeDataResult
|
||||
@@ -184,5 +208,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
internal const string Encoding = "encoding";
|
||||
internal const string Formatted = "formatted";
|
||||
internal const string MaxCharsToStore = "maxchars";
|
||||
internal const string FreezeHeaderRow = "freezeHeaderRow";
|
||||
internal const string BoldHeaderRow = "boldHeaderRow";
|
||||
internal const string AutoFilterHeaderRow = "autoFilterHeaderRow";
|
||||
internal const string AutoSizeColumns = "autoSizeColumns";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
@@ -16,14 +18,28 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
|
||||
/// </summary>
|
||||
public class SaveAsExcelFileStreamWriter : SaveAsStreamWriter
|
||||
{
|
||||
// Font family used in Excel sheet
|
||||
private const string FontFamily = "Calibri";
|
||||
|
||||
// Font size in Excel sheet (points with conversion to pixels)
|
||||
private const float FontSizePixels = 11F * (96F / 72F);
|
||||
|
||||
// Pixel width of auto-filter button
|
||||
private const float AutoFilterPixelWidth = 17F;
|
||||
|
||||
#region Member Variables
|
||||
|
||||
private readonly SaveResultsAsExcelRequestParams saveParams;
|
||||
private readonly float[] columnWidths;
|
||||
private readonly int columnEndIndex;
|
||||
private readonly int columnStartIndex;
|
||||
private readonly SaveAsExcelFileStreamWriterHelper helper;
|
||||
|
||||
private bool headerWritten;
|
||||
private SaveAsExcelFileStreamWriterHelper helper;
|
||||
private SaveAsExcelFileStreamWriterHelper.ExcelSheet sheet;
|
||||
|
||||
private SKPaint paint;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
@@ -41,7 +57,89 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
saveParams = requestParams;
|
||||
helper = new SaveAsExcelFileStreamWriterHelper(stream);
|
||||
sheet = helper.AddSheet();
|
||||
|
||||
// Do some setup if the caller requested automatically sized columns
|
||||
if (requestParams.AutoSizeColumns)
|
||||
{
|
||||
// Set column counts depending on whether save request is for entire set or a subset
|
||||
columnEndIndex = columns.Count;
|
||||
columnStartIndex = 0;
|
||||
var columnCount = columns.Count;
|
||||
|
||||
if (requestParams.IsSaveSelection)
|
||||
{
|
||||
// ReSharper disable PossibleInvalidOperationException IsSaveSelection verifies these values exist
|
||||
columnEndIndex = requestParams.ColumnEndIndex.Value + 1;
|
||||
columnStartIndex = requestParams.ColumnStartIndex.Value;
|
||||
columnCount = columnEndIndex - columnStartIndex;
|
||||
// ReSharper restore PossibleInvalidOperationException
|
||||
}
|
||||
|
||||
columnWidths = new float[columnCount];
|
||||
|
||||
// If the caller requested headers the column widths can be initially set based on the header values
|
||||
if (requestParams.IncludeHeaders)
|
||||
{
|
||||
// Setup for measuring the header, set font style based on whether the header should be bold or not
|
||||
using var headerPaint = new SKPaint();
|
||||
headerPaint.Typeface = SKTypeface.FromFamilyName(FontFamily, requestParams.BoldHeaderRow ? SKFontStyle.Bold : SKFontStyle.Normal);
|
||||
headerPaint.TextSize = FontSizePixels;
|
||||
var skBounds = SKRect.Empty;
|
||||
|
||||
// Loop over all the columns
|
||||
for (int columnIndex = columnStartIndex; columnIndex < columnEndIndex; ++columnIndex)
|
||||
{
|
||||
var columnNumber = columnIndex - columnStartIndex;
|
||||
|
||||
// Measure the header text
|
||||
var textWidth = headerPaint.MeasureText(columns[columnIndex].ColumnName.AsSpan(), ref skBounds);
|
||||
|
||||
// Add extra for the auto filter button if requested
|
||||
if (requestParams.AutoFilterHeaderRow)
|
||||
{
|
||||
textWidth += AutoFilterPixelWidth;
|
||||
}
|
||||
|
||||
// Just store the width as a starting point
|
||||
columnWidths[columnNumber] = textWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Excel supports specifying column widths so measure if the user wants the columns automatically sized
|
||||
/// </summary>
|
||||
public override bool ShouldMeasureRowColumns => saveParams.AutoSizeColumns;
|
||||
|
||||
/// <summary>
|
||||
/// Measures each column of a row of data and stores updates the maximum width of the column if needed
|
||||
/// </summary>
|
||||
/// <param name="row">The row of data to measure</param>
|
||||
public override void MeasureRowColumns(IList<DbCellValue> row)
|
||||
{
|
||||
// Create the paint object if not done already
|
||||
if (paint == null)
|
||||
{
|
||||
paint = new SKPaint();
|
||||
|
||||
paint.Typeface = SKTypeface.FromFamilyName(FontFamily);
|
||||
paint.TextSize = FontSizePixels;
|
||||
}
|
||||
|
||||
var skBounds = SKRect.Empty;
|
||||
|
||||
// Loop over all the columns
|
||||
for (int columnIndex = columnStartIndex; columnIndex < columnEndIndex; ++columnIndex)
|
||||
{
|
||||
var columnNumber = columnIndex - columnStartIndex;
|
||||
|
||||
// Measure the width of the text
|
||||
var textWidth = paint.MeasureText(row[columnIndex].DisplayValue.AsSpan(), ref skBounds);
|
||||
|
||||
// Update the max if the new width is greater
|
||||
columnWidths[columnNumber] = Math.Max(columnWidths[columnNumber], textWidth);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -55,13 +153,44 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
|
||||
/// </param>
|
||||
public override void WriteRow(IList<DbCellValue> row, IReadOnlyList<DbColumnWrapper> columns)
|
||||
{
|
||||
// Check to make sure the sheet has been created
|
||||
if (sheet == null)
|
||||
{
|
||||
// Get rid of any paint object from the auto-sizing
|
||||
paint?.Dispose();
|
||||
|
||||
// Create the blank sheet
|
||||
sheet = helper.AddSheet(null, columns.Count);
|
||||
|
||||
// The XLSX format has strict ordering requirements so these must be done in the proper order
|
||||
|
||||
// First freeze the header row if the caller has requested header rows and that the header should be frozen
|
||||
if (saveParams.IncludeHeaders && saveParams.FreezeHeaderRow)
|
||||
{
|
||||
sheet.FreezeHeaderRow();
|
||||
}
|
||||
|
||||
// Next if column widths have been specified they should be saved to the sheet
|
||||
if (columnWidths != null)
|
||||
{
|
||||
sheet.WriteColumnInformation(columnWidths);
|
||||
}
|
||||
|
||||
// Lastly enable auto filter if the caller has requested header rows and that the header should be frozen
|
||||
if (saveParams.IncludeHeaders && saveParams.AutoFilterHeaderRow)
|
||||
{
|
||||
sheet.EnableAutoFilter();
|
||||
}
|
||||
}
|
||||
|
||||
// Write out the header if we haven't already and the user chose to have it
|
||||
if (saveParams.IncludeHeaders && !headerWritten)
|
||||
{
|
||||
sheet.AddRow();
|
||||
for (int i = ColumnStartIndex; i <= ColumnEndIndex; i++)
|
||||
{
|
||||
sheet.AddCell(columns[i].ColumnName);
|
||||
// Add the header text and bold if requested
|
||||
sheet.AddCell(columns[i].ColumnName, saveParams.BoldHeaderRow);
|
||||
}
|
||||
headerWritten = true;
|
||||
}
|
||||
@@ -74,7 +203,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
|
||||
}
|
||||
|
||||
private bool disposed;
|
||||
override protected void Dispose(bool disposing)
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposed)
|
||||
return;
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,17 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
|
||||
throw new InvalidOperationException("This type of writer is meant to write values from a list of cell values only.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether columns should be measured based on whether the output format supports it and if the caller wants the columns automatically sized
|
||||
/// </summary>
|
||||
public virtual bool ShouldMeasureRowColumns => false;
|
||||
|
||||
/// <summary>
|
||||
/// Measures the columns in a row of data as part of determining automatic column widths
|
||||
/// </summary>
|
||||
/// <param name="row">The row of data to measure</param>
|
||||
public virtual void MeasureRowColumns(IList<DbCellValue> row) { }
|
||||
|
||||
/// <summary>
|
||||
/// Writes a row of data to the output file using the format provided by the implementing class.
|
||||
/// </summary>
|
||||
|
||||
@@ -533,8 +533,30 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
using (var fileReader = fileFactory.GetReader(outputFileName))
|
||||
using (var fileWriter = fileFactory.GetWriter(saveParams.FilePath, Columns))
|
||||
{
|
||||
DateTime recentLogTime;
|
||||
|
||||
// Some writers (like Excel) require the column widths before any of the rows have been written so we need to loop over
|
||||
// all the rows of data in order to calculate what the column widths should be before the rows are actually written
|
||||
if (fileWriter is SaveAsStreamWriter { ShouldMeasureRowColumns: true } saveAsStreamWriter)
|
||||
{
|
||||
Logger.Verbose($"Started measuring {RowCount} rows");
|
||||
recentLogTime = DateTime.Now;
|
||||
// Iterate over the rows that are in the selected row set
|
||||
for (long i = rowStartIndex; i < rowEndIndex; ++i)
|
||||
{
|
||||
var row = fileReader.ReadRow(fileOffsets[i], i, Columns);
|
||||
saveAsStreamWriter.MeasureRowColumns(row);
|
||||
if (DateTime.Now.Subtract(recentLogTime).TotalSeconds > 1)
|
||||
{
|
||||
Logger.Verbose($"Measure progress: {i - rowStartIndex + 1}/{RowCount}.");
|
||||
recentLogTime = DateTime.Now;
|
||||
}
|
||||
}
|
||||
Logger.Verbose($"Measured {RowCount} rows");
|
||||
}
|
||||
|
||||
Logger.Verbose($"Started exporting {RowCount} rows to file: {outputFileName}");
|
||||
var recentLogTime = DateTime.Now;
|
||||
recentLogTime = DateTime.Now;
|
||||
// Iterate over the rows that are in the selected row set
|
||||
for (long i = rowStartIndex; i < rowEndIndex; ++i)
|
||||
{
|
||||
|
||||
@@ -290,7 +290,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
FilePath = this.requestParams.FilePath,
|
||||
BatchIndex = 0,
|
||||
ResultSetIndex = 0,
|
||||
IncludeHeaders = this.requestParams.IncludeHeaders
|
||||
IncludeHeaders = this.requestParams.IncludeHeaders,
|
||||
FreezeHeaderRow = this.requestParams.FreezeHeaderRow,
|
||||
BoldHeaderRow = this.requestParams.BoldHeaderRow,
|
||||
AutoFilterHeaderRow = this.requestParams.AutoFilterHeaderRow,
|
||||
AutoSizeColumns = this.requestParams.AutoSizeColumns
|
||||
};
|
||||
}
|
||||
private SaveResultsAsCsvRequestParams CreateCsvRequestParams()
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<numFmt numFmtId="168" formatCode="yyyy-mm-dd hh:mm:ss" />
|
||||
<numFmt numFmtId="169" formatCode="[h]:mm:ss" />
|
||||
</numFmts>
|
||||
<fonts count="1">
|
||||
<fonts count="2">
|
||||
<font>
|
||||
<sz val="11" />
|
||||
<color theme="1" />
|
||||
@@ -14,6 +14,14 @@
|
||||
<family val="2" />
|
||||
<scheme val="minor" />
|
||||
</font>
|
||||
<font>
|
||||
<b />
|
||||
<sz val="11" />
|
||||
<color theme="1" />
|
||||
<name val="Calibri" />
|
||||
<family val="2" />
|
||||
<scheme val="minor" />
|
||||
</font>
|
||||
</fonts>
|
||||
<fills count="1">
|
||||
<fill>
|
||||
@@ -32,12 +40,13 @@
|
||||
<cellStyleXfs count="1">
|
||||
<xf numFmtId="0" fontId="0" fillId="0" borderId="0" />
|
||||
</cellStyleXfs>
|
||||
<cellXfs count="5">
|
||||
<cellXfs count="6">
|
||||
<xf xfId="0" />
|
||||
<xf numFmtId="166" xfId="0" applyNumberFormat="1" />
|
||||
<xf numFmtId="167" xfId="0" applyNumberFormat="1" />
|
||||
<xf numFmtId="168" xfId="0" applyNumberFormat="1" />
|
||||
<xf numFmtId="169" xfId="0" applyNumberFormat="1" />
|
||||
<xf xfId="0" fontId="1" />
|
||||
</cellXfs>
|
||||
<cellStyles count="1">
|
||||
<cellStyle name="Normal" builtinId="0" xfId="0" />
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<worksheet xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||||
<sheetData>
|
||||
<row r="1">
|
||||
<c r="B1" t="inlineStr"><is><t></t></is></c>
|
||||
<c r="C1" t="inlineStr"><is><t>test string</t></is></c>
|
||||
<c r="D1"><v>3</v></c>
|
||||
<c r="E1"><v>3.5</v></c>
|
||||
<c r="F1" t="b"><v>0</v></c>
|
||||
<c r="G1" t="b"><v>1</v></c>
|
||||
</row>
|
||||
<row r="2">
|
||||
<c r="A2" t="inlineStr"><is><t>1900-02-28</t></is></c>
|
||||
<c r="B2" s="1"><v>61</v></c>
|
||||
<c r="C2" s="3"><v>61.625</v></c>
|
||||
<c r="D2" s="2"><v>0.625</v></c>
|
||||
<c r="E2" s="2"><v>0.625</v></c>
|
||||
<c r="F2" s="4"><v>1</v></c>
|
||||
</row>
|
||||
</sheetData>
|
||||
<autoFilter ref="A1:G1" />
|
||||
</worksheet>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<worksheet xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||||
<sheetData>
|
||||
<row r="1">
|
||||
<c r="B1" t="inlineStr"><is><t></t></is></c>
|
||||
<c r="C1" t="inlineStr"><is><t>test string</t></is></c>
|
||||
<c r="D1"><v>3</v></c>
|
||||
<c r="E1"><v>3.5</v></c>
|
||||
<c r="F1" t="b"><v>0</v></c>
|
||||
<c r="G1" t="b"><v>1</v></c>
|
||||
</row>
|
||||
<row r="2">
|
||||
<c r="A2" t="inlineStr"><is><t>1900-02-28</t></is></c>
|
||||
<c r="B2" s="1"><v>61</v></c>
|
||||
<c r="C2" s="3"><v>61.625</v></c>
|
||||
<c r="D2" s="2"><v>0.625</v></c>
|
||||
<c r="E2" s="2"><v>0.625</v></c>
|
||||
<c r="F2" s="4"><v>1</v></c>
|
||||
</row>
|
||||
</sheetData>
|
||||
</worksheet>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<worksheet xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||||
<sheetViews>
|
||||
<sheetView tabSelected="1" workbookViewId="0">
|
||||
<pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen" />
|
||||
<selection pane="bottomLeft" />
|
||||
</sheetView>
|
||||
</sheetViews>
|
||||
<sheetData>
|
||||
<row r="1">
|
||||
<c r="B1" t="inlineStr"><is><t></t></is></c>
|
||||
<c r="C1" t="inlineStr"><is><t>test string</t></is></c>
|
||||
<c r="D1"><v>3</v></c>
|
||||
<c r="E1"><v>3.5</v></c>
|
||||
<c r="F1" t="b"><v>0</v></c>
|
||||
<c r="G1" t="b"><v>1</v></c>
|
||||
</row>
|
||||
<row r="2">
|
||||
<c r="A2" t="inlineStr"><is><t>1900-02-28</t></is></c>
|
||||
<c r="B2" s="1"><v>61</v></c>
|
||||
<c r="C2" s="3"><v>61.625</v></c>
|
||||
<c r="D2" s="2"><v>0.625</v></c>
|
||||
<c r="E2" s="2"><v>0.625</v></c>
|
||||
<c r="F2" s="4"><v>1</v></c>
|
||||
</row>
|
||||
</sheetData>
|
||||
</worksheet>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<worksheet xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||||
<cols>
|
||||
<col min="1" max="1" width="2.28515625" bestFit="1" customWidth="1" />
|
||||
<col min="2" max="2" width="2.42578125" bestFit="1" customWidth="1" />
|
||||
<col min="3" max="3" width="2.5703125" bestFit="1" customWidth="1" />
|
||||
<col min="4" max="4" width="2.7109375" bestFit="1" customWidth="1" />
|
||||
<col min="5" max="5" width="2.85546875" bestFit="1" customWidth="1" />
|
||||
<col min="6" max="6" width="3" bestFit="1" customWidth="1" />
|
||||
<col min="7" max="7" width="3.140625" bestFit="1" customWidth="1" />
|
||||
</cols>
|
||||
<sheetData>
|
||||
<row r="1">
|
||||
<c r="B1" t="inlineStr"><is><t></t></is></c>
|
||||
<c r="C1" t="inlineStr"><is><t>test string</t></is></c>
|
||||
<c r="D1"><v>3</v></c>
|
||||
<c r="E1"><v>3.5</v></c>
|
||||
<c r="F1" t="b"><v>0</v></c>
|
||||
<c r="G1" t="b"><v>1</v></c>
|
||||
</row>
|
||||
<row r="2">
|
||||
<c r="A2" t="inlineStr"><is><t>1900-02-28</t></is></c>
|
||||
<c r="B2" s="1"><v>61</v></c>
|
||||
<c r="C2" s="3"><v>61.625</v></c>
|
||||
<c r="D2" s="2"><v>0.625</v></c>
|
||||
<c r="E2" s="2"><v>0.625</v></c>
|
||||
<c r="F2" s="4"><v>1</v></c>
|
||||
</row>
|
||||
</sheetData>
|
||||
</worksheet>
|
||||
@@ -16,14 +16,14 @@ using NUnit.Framework;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.DataStorage
|
||||
{
|
||||
public partial class SaveAsExcelFileStreamWriterHelperTests : IDisposable
|
||||
public partial class SaveAsExcelFileStreamWriterHelperTests
|
||||
{
|
||||
private Stream _stream;
|
||||
public SaveAsExcelFileStreamWriterHelperTests()
|
||||
[Test]
|
||||
public void DefaultSheet()
|
||||
{
|
||||
_stream = new MemoryStream();
|
||||
using (var helper = new SaveAsExcelFileStreamWriterHelper(_stream, true))
|
||||
using (var sheet = helper.AddSheet())
|
||||
var stream = new MemoryStream();
|
||||
using (var helper = new SaveAsExcelFileStreamWriterHelper(stream, true))
|
||||
using (var sheet = helper.AddSheet(null, 7))
|
||||
{
|
||||
var value = new DbCellValue();
|
||||
sheet.AddRow();
|
||||
@@ -70,64 +70,431 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.DataStorage
|
||||
value.RawObject = new TimeSpan(24, 00, 00);
|
||||
sheet.AddCell(value);
|
||||
}
|
||||
|
||||
ContentMatch(stream, "[Content_Types].xml");
|
||||
ContentMatch(stream, "_rels/.rels");
|
||||
ContentMatch(stream, "xl/_rels/workbook.xml.rels");
|
||||
ContentMatch(stream, "xl/styles.xml");
|
||||
ContentMatch(stream, "xl/workbook.xml");
|
||||
ContentMatch(stream, "xl/worksheets/sheet1.xml");
|
||||
|
||||
stream.Dispose();
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void SheetWithFrozenHeaderRow()
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
using (var helper = new SaveAsExcelFileStreamWriterHelper(stream, true))
|
||||
using (var sheet = helper.AddSheet(null, 7))
|
||||
{
|
||||
sheet.FreezeHeaderRow();
|
||||
|
||||
var value = new DbCellValue();
|
||||
sheet.AddRow();
|
||||
|
||||
value.IsNull = true;
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.IsNull = false;
|
||||
value.RawObject = "";
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = "test string";
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = 3;
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = 3.5;
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = false;
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = true;
|
||||
sheet.AddCell(value);
|
||||
|
||||
sheet.AddRow();
|
||||
|
||||
value.RawObject = new DateTime(1900, 2, 28);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new DateTime(1900, 3, 1);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new DateTime(1900, 3, 1, 15, 00, 00);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new DateTime(1, 1, 1, 15, 00, 00);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new TimeSpan(15, 00, 00);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new TimeSpan(24, 00, 00);
|
||||
sheet.AddCell(value);
|
||||
}
|
||||
|
||||
ContentMatch(stream, "[Content_Types].xml");
|
||||
ContentMatch(stream, "_rels/.rels");
|
||||
ContentMatch(stream, "xl/_rels/workbook.xml.rels");
|
||||
ContentMatch(stream, "xl/styles.xml");
|
||||
ContentMatch(stream, "xl/workbook.xml");
|
||||
ContentMatch(stream, "xl/worksheets/sheet1.xml", "xl/worksheets/sheet1-headerRowFrozen.xml");
|
||||
|
||||
stream.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SheetWithFrozenHeaderRowCalledTooLate()
|
||||
{
|
||||
var expectedException = new InvalidOperationException("Must be called before calling AddRow");
|
||||
var actualException = new Exception();
|
||||
var stream = new MemoryStream();
|
||||
using (var helper = new SaveAsExcelFileStreamWriterHelper(stream, true))
|
||||
using (var sheet = helper.AddSheet(null, 7))
|
||||
{
|
||||
sheet.AddRow();
|
||||
|
||||
try
|
||||
{
|
||||
sheet.FreezeHeaderRow();
|
||||
|
||||
Assert.Fail("Did not throw an exception when calling FreezeHeaderRow too late");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
actualException = e;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.AreEqual(expectedException.GetType(), actualException.GetType());
|
||||
Assert.AreEqual(expectedException.Message, actualException.Message);
|
||||
|
||||
stream.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SheetWithBoldHeaderRow()
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
using (var helper = new SaveAsExcelFileStreamWriterHelper(stream, true))
|
||||
using (var sheet = helper.AddSheet(null, 7))
|
||||
{
|
||||
var value = new DbCellValue();
|
||||
sheet.AddRow();
|
||||
|
||||
value.IsNull = true;
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.IsNull = false;
|
||||
value.RawObject = "";
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = "test string";
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = 3;
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = 3.5;
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = false;
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = true;
|
||||
sheet.AddCell(value);
|
||||
|
||||
sheet.AddRow();
|
||||
|
||||
value.RawObject = new DateTime(1900, 2, 28);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new DateTime(1900, 3, 1);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new DateTime(1900, 3, 1, 15, 00, 00);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new DateTime(1, 1, 1, 15, 00, 00);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new TimeSpan(15, 00, 00);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new TimeSpan(24, 00, 00);
|
||||
sheet.AddCell(value);
|
||||
}
|
||||
|
||||
ContentMatch(stream, "[Content_Types].xml");
|
||||
ContentMatch(stream, "_rels/.rels");
|
||||
ContentMatch(stream, "xl/_rels/workbook.xml.rels");
|
||||
ContentMatch(stream, "xl/styles.xml");
|
||||
ContentMatch(stream, "xl/workbook.xml");
|
||||
ContentMatch(stream, "xl/worksheets/sheet1.xml", "xl/worksheets/sheet1-headerRowBold.xml");
|
||||
|
||||
stream.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SheetWithAutoFilterEnabled()
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
using (var helper = new SaveAsExcelFileStreamWriterHelper(stream, true))
|
||||
using (var sheet = helper.AddSheet(null, 7))
|
||||
{
|
||||
sheet.EnableAutoFilter();
|
||||
|
||||
var value = new DbCellValue();
|
||||
sheet.AddRow();
|
||||
|
||||
value.IsNull = true;
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.IsNull = false;
|
||||
value.RawObject = "";
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = "test string";
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = 3;
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = 3.5;
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = false;
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = true;
|
||||
sheet.AddCell(value);
|
||||
|
||||
sheet.AddRow();
|
||||
|
||||
value.RawObject = new DateTime(1900, 2, 28);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new DateTime(1900, 3, 1);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new DateTime(1900, 3, 1, 15, 00, 00);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new DateTime(1, 1, 1, 15, 00, 00);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new TimeSpan(15, 00, 00);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new TimeSpan(24, 00, 00);
|
||||
sheet.AddCell(value);
|
||||
}
|
||||
|
||||
ContentMatch(stream, "[Content_Types].xml");
|
||||
ContentMatch(stream, "_rels/.rels");
|
||||
ContentMatch(stream, "xl/_rels/workbook.xml.rels");
|
||||
ContentMatch(stream, "xl/styles.xml");
|
||||
ContentMatch(stream, "xl/workbook.xml");
|
||||
ContentMatch(stream, "xl/worksheets/sheet1.xml", "xl/worksheets/sheet1-autoFilterEnabled.xml");
|
||||
|
||||
stream.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SheetWriteColumnInformationCalledTooLate()
|
||||
{
|
||||
var expectedException = new InvalidOperationException("Must be called before calling AddRow");
|
||||
var actualException = new Exception();
|
||||
var stream = new MemoryStream();
|
||||
using (var helper = new SaveAsExcelFileStreamWriterHelper(stream, true))
|
||||
using (var sheet = helper.AddSheet(null, 7))
|
||||
{
|
||||
sheet.AddRow();
|
||||
|
||||
try
|
||||
{
|
||||
sheet.WriteColumnInformation(new []{ 1F, 2F, 3F, 4F, 5F, 6F, 7F });
|
||||
|
||||
Assert.Fail("Did not throw an exception when calling WriteColumnInformation too late");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
actualException = e;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.AreEqual(expectedException.GetType(), actualException.GetType());
|
||||
Assert.AreEqual(expectedException.Message, actualException.Message);
|
||||
|
||||
stream.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SheetWriteColumnInformationWithWrongAmount()
|
||||
{
|
||||
var expectedException = new InvalidOperationException("Column count mismatch");
|
||||
var actualException = new Exception();
|
||||
var stream = new MemoryStream();
|
||||
using (var helper = new SaveAsExcelFileStreamWriterHelper(stream, true))
|
||||
using (var sheet = helper.AddSheet(null, 7))
|
||||
{
|
||||
try
|
||||
{
|
||||
sheet.WriteColumnInformation(new[] { 1F, 2F, 3F, 4F, 5F });
|
||||
|
||||
Assert.Fail("Did not throw an exception when calling WriteColumnInformation with the wrong number of columns");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
actualException = e;
|
||||
}
|
||||
|
||||
var value = new DbCellValue();
|
||||
sheet.AddRow();
|
||||
|
||||
value.IsNull = true;
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.IsNull = false;
|
||||
value.RawObject = "";
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = "test string";
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = 3;
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = 3.5;
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = false;
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = true;
|
||||
sheet.AddCell(value);
|
||||
|
||||
sheet.AddRow();
|
||||
|
||||
value.RawObject = new DateTime(1900, 2, 28);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new DateTime(1900, 3, 1);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new DateTime(1900, 3, 1, 15, 00, 00);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new DateTime(1, 1, 1, 15, 00, 00);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new TimeSpan(15, 00, 00);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new TimeSpan(24, 00, 00);
|
||||
sheet.AddCell(value);
|
||||
}
|
||||
|
||||
Assert.AreEqual(expectedException.GetType(), actualException.GetType());
|
||||
Assert.AreEqual(expectedException.Message, actualException.Message);
|
||||
|
||||
stream.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SheetWriteColumnInformation()
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
using (var helper = new SaveAsExcelFileStreamWriterHelper(stream, true))
|
||||
using (var sheet = helper.AddSheet(null, 7))
|
||||
{
|
||||
sheet.WriteColumnInformation(new[] { 1F, 2F, 3F, 4F, 5F, 6F, 7F });
|
||||
|
||||
var value = new DbCellValue();
|
||||
sheet.AddRow();
|
||||
|
||||
value.IsNull = true;
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.IsNull = false;
|
||||
value.RawObject = "";
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = "test string";
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = 3;
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = 3.5;
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = false;
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = true;
|
||||
sheet.AddCell(value);
|
||||
|
||||
sheet.AddRow();
|
||||
|
||||
value.RawObject = new DateTime(1900, 2, 28);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new DateTime(1900, 3, 1);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new DateTime(1900, 3, 1, 15, 00, 00);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new DateTime(1, 1, 1, 15, 00, 00);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new TimeSpan(15, 00, 00);
|
||||
sheet.AddCell(value);
|
||||
|
||||
value.RawObject = new TimeSpan(24, 00, 00);
|
||||
sheet.AddCell(value);
|
||||
}
|
||||
|
||||
ContentMatch(stream, "[Content_Types].xml");
|
||||
ContentMatch(stream, "_rels/.rels");
|
||||
ContentMatch(stream, "xl/_rels/workbook.xml.rels");
|
||||
ContentMatch(stream, "xl/styles.xml");
|
||||
ContentMatch(stream, "xl/workbook.xml");
|
||||
ContentMatch(stream, "xl/worksheets/sheet1.xml", "xl/worksheets/sheet1-withColumns.xml");
|
||||
|
||||
stream.Dispose();
|
||||
}
|
||||
|
||||
[GeneratedRegex("\\r?\\n\\s*")]
|
||||
private static partial Regex GetContentRemoveLinebreakLeadingSpaceRegex();
|
||||
|
||||
private void ContentMatch(string fileName)
|
||||
private void ContentMatch(Stream stream, string fileName)
|
||||
{
|
||||
ContentMatch(stream, fileName, fileName);
|
||||
}
|
||||
|
||||
private void ContentMatch(Stream stream, string realFileName, string referenceFileName)
|
||||
{
|
||||
string referencePath = Path.Combine(RunEnvironmentInfo.GetTestDataLocation(),
|
||||
"DataStorage",
|
||||
"SaveAsExcelFileStreamWriterHelperTests",
|
||||
fileName);
|
||||
referenceFileName);
|
||||
string referenceContent = File.ReadAllText(referencePath);
|
||||
referenceContent = GetContentRemoveLinebreakLeadingSpaceRegex().Replace(referenceContent, "");
|
||||
|
||||
using (var zip = new ZipArchive(_stream, ZipArchiveMode.Read, true))
|
||||
using (var zip = new ZipArchive(stream, ZipArchiveMode.Read, true))
|
||||
{
|
||||
using (var reader = new StreamReader(zip?.GetEntry(fileName)?.Open()!))
|
||||
using (var reader = new StreamReader(zip?.GetEntry(realFileName)?.Open()!))
|
||||
{
|
||||
string realContent = reader.ReadToEnd();
|
||||
Assert.AreEqual(referenceContent, realContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
[Test]
|
||||
public void CheckContentType()
|
||||
{
|
||||
ContentMatch("[Content_Types].xml");
|
||||
}
|
||||
[Test]
|
||||
public void CheckTopRels()
|
||||
{
|
||||
ContentMatch("_rels/.rels");
|
||||
}
|
||||
[Test]
|
||||
public void CheckWorkbookRels()
|
||||
{
|
||||
ContentMatch("xl/_rels/workbook.xml.rels");
|
||||
}
|
||||
[Test]
|
||||
public void CheckStyles()
|
||||
{
|
||||
ContentMatch("xl/styles.xml");
|
||||
}
|
||||
[Test]
|
||||
public void CheckWorkbook()
|
||||
{
|
||||
ContentMatch("xl/workbook.xml");
|
||||
}
|
||||
[Test]
|
||||
public void CheckSheet1()
|
||||
{
|
||||
ContentMatch("xl/worksheets/sheet1.xml");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_stream.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public class SaveAsExcelFileStreamWriterHelperReferenceManagerTests
|
||||
|
||||
Reference in New Issue
Block a user