Async implementation of freezing top row (#684)

* Add async implementation of frozen rows and columns

* Add unit tests for async implementation of frozen rows and columns

* Fix formatting and delete temp file in unit test

* move freezing top row implementation to DefaultOpenXml.cs
This commit is contained in:
BaatenHannes 2024-11-02 12:19:10 +01:00 committed by GitHub
parent a0797a53f6
commit 92b295d4b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 183 additions and 96 deletions

View File

@ -119,6 +119,9 @@ namespace MiniExcelLibs.OpenXml
} }
maxColumnIndex = props.Count; maxColumnIndex = props.Count;
//sheet view
await writer.WriteAsync(GetSheetViews());
await WriteColumnsWidthsAsync(writer, props); await WriteColumnsWidthsAsync(writer, props);
await writer.WriteAsync(WorksheetXml.StartSheetData); await writer.WriteAsync(WorksheetXml.StartSheetData);
@ -241,6 +244,9 @@ namespace MiniExcelLibs.OpenXml
await writer.WriteAsync(WorksheetXml.Dimension(GetDimensionRef(maxRowIndex, maxColumnIndex))); await writer.WriteAsync(WorksheetXml.Dimension(GetDimensionRef(maxRowIndex, maxColumnIndex)));
} }
//sheet view
await writer.WriteAsync(GetSheetViews());
//cols:width //cols:width
await WriteColumnsWidthsAsync(writer, props); await WriteColumnsWidthsAsync(writer, props);
@ -309,6 +315,9 @@ namespace MiniExcelLibs.OpenXml
props.Add(prop); props.Add(prop);
} }
//sheet view
await writer.WriteAsync(GetSheetViews());
await WriteColumnsWidthsAsync(writer, props); await WriteColumnsWidthsAsync(writer, props);
await writer.WriteAsync(WorksheetXml.StartSheetData); await writer.WriteAsync(WorksheetXml.StartSheetData);

View File

@ -83,6 +83,103 @@ namespace MiniExcelLibs.OpenXml
return info; return info;
} }
private string GetSheetViews()
{
// exit early if no style to write
if (_configuration.FreezeRowCount <= 0 && _configuration.FreezeColumnCount <= 0)
{
return string.Empty;
}
var sb = new StringBuilder();
// start sheetViews
sb.Append(WorksheetXml.StartSheetViews);
sb.Append(WorksheetXml.StartSheetView());
// Write panes
sb.Append(GetPanes());
// end sheetViews
sb.Append(WorksheetXml.EndSheetView);
sb.Append(WorksheetXml.EndSheetViews);
return sb.ToString();
}
private string GetPanes()
{
var sb = new StringBuilder();
string activePane;
if (_configuration.FreezeColumnCount > 0 && _configuration.FreezeRowCount > 0)
{
activePane = "bottomRight";
}
else if (_configuration.FreezeColumnCount > 0)
{
activePane = "topRight";
}
else
{
activePane = "bottomLeft";
}
sb.Append(
WorksheetXml.StartPane(
xSplit: _configuration.FreezeColumnCount > 0 ? _configuration.FreezeColumnCount : (int?)null,
ySplit: _configuration.FreezeRowCount > 0 ? _configuration.FreezeRowCount : (int?)null,
topLeftCell: ExcelOpenXmlUtils.ConvertXyToCell(
_configuration.FreezeColumnCount + 1,
_configuration.FreezeRowCount + 1
),
activePane: activePane,
state: "frozen"
)
);
// write pane selections
if (_configuration.FreezeColumnCount > 0 && _configuration.FreezeRowCount > 0)
{
// freeze row and column
/*
<selection pane="topRight" activeCell="B1" sqref="B1"/>
<selection pane="bottomLeft" activeCell="A3" sqref="A3"/>
<selection pane="bottomRight" activeCell="B3" sqref="B3"/>
*/
var cellTR = ExcelOpenXmlUtils.ConvertXyToCell(_configuration.FreezeColumnCount + 1, 1);
sb.Append(WorksheetXml.PaneSelection("topRight", cellTR, cellTR));
var cellBL = ExcelOpenXmlUtils.ConvertXyToCell(1, _configuration.FreezeRowCount + 1);
sb.Append(WorksheetXml.PaneSelection("bottomLeft", cellBL, cellBL));
var cellBR = ExcelOpenXmlUtils.ConvertXyToCell(_configuration.FreezeColumnCount + 1, _configuration.FreezeRowCount + 1);
sb.Append(WorksheetXml.PaneSelection("bottomRight", cellBR, cellBR));
}
else if (_configuration.FreezeColumnCount > 0)
{
// freeze column
/*
<selection pane="topRight" activeCell="A1" sqref="A1"/>
*/
var cellTR = ExcelOpenXmlUtils.ConvertXyToCell(_configuration.FreezeColumnCount, 1);
sb.Append(WorksheetXml.PaneSelection("topRight", cellTR, cellTR));
}
else
{
// freeze row
/*
<selection pane="bottomLeft"/>
*/
sb.Append(WorksheetXml.PaneSelection("bottomLeft", null, null));
}
return sb.ToString();
}
private ExcelColumnInfo GetColumnInfosFromDynamicConfiguration(string columnName) private ExcelColumnInfo GetColumnInfosFromDynamicConfiguration(string columnName)
{ {

View File

@ -152,7 +152,7 @@ namespace MiniExcelLibs.OpenXml
maxColumnIndex = props.Count; maxColumnIndex = props.Count;
//sheet view //sheet view
WriteSheetViews(writer); writer.Write(GetSheetViews());
WriteColumnsWidths(writer, props); WriteColumnsWidths(writer, props);
@ -279,7 +279,7 @@ namespace MiniExcelLibs.OpenXml
} }
//sheet view //sheet view
WriteSheetViews(writer); writer.Write(GetSheetViews());
//cols:width //cols:width
WriteColumnsWidths(writer, props); WriteColumnsWidths(writer, props);
@ -352,7 +352,7 @@ namespace MiniExcelLibs.OpenXml
} }
//sheet view //sheet view
WriteSheetViews(writer); writer.Write(GetSheetViews());
WriteColumnsWidths(writer, props); WriteColumnsWidths(writer, props);
@ -411,91 +411,6 @@ namespace MiniExcelLibs.OpenXml
writer.Write(WorksheetXml.EndCols); writer.Write(WorksheetXml.EndCols);
} }
private void WriteSheetViews(MiniExcelStreamWriter writer) {
// exit early if no style to write
if (_configuration.FreezeRowCount <= 0 && _configuration.FreezeColumnCount <= 0)
{
return;
}
// start sheetViews
writer.Write(WorksheetXml.StartSheetViews);
writer.Write(WorksheetXml.StartSheetView());
// Write panes
WritePanes(writer);
// end sheetViews
writer.Write(WorksheetXml.EndSheetView);
writer.Write(WorksheetXml.EndSheetViews);
}
private void WritePanes(MiniExcelStreamWriter writer) {
string activePane;
if (_configuration.FreezeColumnCount > 0 && _configuration.FreezeRowCount > 0)
{
activePane = "bottomRight";
}
else if (_configuration.FreezeColumnCount > 0)
{
activePane = "topRight";
}
else
{
activePane = "bottomLeft";
}
writer.Write( WorksheetXml.StartPane(
xSplit: _configuration.FreezeColumnCount > 0 ? _configuration.FreezeColumnCount : (int?)null,
ySplit: _configuration.FreezeRowCount > 0 ? _configuration.FreezeRowCount : (int?)null,
topLeftCell: ExcelOpenXmlUtils.ConvertXyToCell(
_configuration.FreezeColumnCount + 1,
_configuration.FreezeRowCount + 1
),
activePane: activePane,
state: "frozen"
) );
// write pane selections
if (_configuration.FreezeColumnCount > 0 && _configuration.FreezeRowCount > 0)
{
// freeze row and column
/*
<selection pane="topRight" activeCell="B1" sqref="B1"/>
<selection pane="bottomLeft" activeCell="A3" sqref="A3"/>
<selection pane="bottomRight" activeCell="B3" sqref="B3"/>
*/
var cellTR = ExcelOpenXmlUtils.ConvertXyToCell(_configuration.FreezeColumnCount+1, 1);
writer.Write(WorksheetXml.PaneSelection("topRight", cellTR, cellTR));
var cellBL = ExcelOpenXmlUtils.ConvertXyToCell(1, _configuration.FreezeRowCount+1);
writer.Write(WorksheetXml.PaneSelection("bottomLeft", cellBL, cellBL));
var cellBR = ExcelOpenXmlUtils.ConvertXyToCell(_configuration.FreezeColumnCount+1, _configuration.FreezeRowCount+1);
writer.Write(WorksheetXml.PaneSelection("bottomRight", cellBR, cellBR));
}
else if ( _configuration.FreezeColumnCount > 0 )
{
// freeze column
/*
<selection pane="topRight" activeCell="A1" sqref="A1"/>
*/
var cellTR = ExcelOpenXmlUtils.ConvertXyToCell(_configuration.FreezeColumnCount, 1);
writer.Write(WorksheetXml.PaneSelection("topRight", cellTR, cellTR));
}
else
{
// freeze row
/*
<selection pane="bottomLeft"/>
*/
writer.Write(WorksheetXml.PaneSelection("bottomLeft", null, null));
}
}
private static void PrintHeader(MiniExcelStreamWriter writer, List<ExcelColumnInfo> props) private static void PrintHeader(MiniExcelStreamWriter writer, List<ExcelColumnInfo> props)
{ {
var xIndex = 1; var xIndex = 1;

View File

@ -894,6 +894,70 @@ namespace MiniExcelLibs.Tests
File.Delete(path); File.Delete(path);
} }
[Fact()]
public async Task SaveAsFrozenRowsAndColumnsTest()
{
var config = new OpenXmlConfiguration
{
FreezeRowCount = 1,
FreezeColumnCount = 2
};
{
// Test enumerable
var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx");
await MiniExcel.SaveAsAsync(
path,
new[] {
new { Column1 = "MiniExcel", Column2 = 1 },
new { Column1 = "Github", Column2 = 2}
},
configuration: config
);
using (var stream = File.OpenRead(path))
{
var rows = stream.Query(useHeaderRow: true).ToList();
Assert.Equal("MiniExcel", rows[0].Column1);
Assert.Equal(1, rows[0].Column2);
Assert.Equal("Github", rows[1].Column1);
Assert.Equal(2, rows[1].Column2);
}
Assert.Equal("A1:B3", Helpers.GetFirstSheetDimensionRefValue(path));
File.Delete(path);
}
{
// test table
var table = new DataTable();
{
table.Columns.Add("a", typeof(string));
table.Columns.Add("b", typeof(decimal));
table.Columns.Add("c", typeof(bool));
table.Columns.Add("d", typeof(DateTime));
table.Rows.Add("some text", 1234567890, true, DateTime.Now);
table.Rows.Add(@"<test>Hello World</test>", -1234567890, false, DateTime.Now.Date);
}
var pathTable = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx");
await MiniExcel.SaveAsAsync(pathTable, table, configuration: config);
Assert.Equal("A1:D3", Helpers.GetFirstSheetDimensionRefValue(pathTable));
// data reader
var reader = table.CreateDataReader();
var pathReader = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx");
await MiniExcel.SaveAsAsync(pathReader, reader, configuration: config);
Assert.Equal("A1:D3", Helpers.GetFirstSheetDimensionRefValue(pathTable));
}
}
[Fact()] [Fact()]
public async Task SaveAsByDapperRows() public async Task SaveAsByDapperRows()
{ {

View File

@ -166,7 +166,7 @@ namespace MiniExcelLibs.Tests
} }
} }
[Fact] [Fact]
public void QueryRangeToIDictionary() public void QueryRangeToIDictionary()
{ {
@ -201,7 +201,7 @@ namespace MiniExcelLibs.Tests
Assert.Equal(null, rows[2].A); Assert.Equal(null, rows[2].A);
Assert.Equal(2, rows[2].B); Assert.Equal(2, rows[2].B);
Assert.Equal(null, rows[2].C); Assert.Equal(null, rows[2].C);
Assert.Equal(4, rows[2].D); Assert.Equal(4, rows[2].D);
Assert.Equal(null, rows[3].A); Assert.Equal(null, rows[3].A);
@ -860,7 +860,8 @@ namespace MiniExcelLibs.Tests
} }
[Fact()] [Fact()]
public void SaveAsFrozenRowsAndColumnsTest() { public void SaveAsFrozenRowsAndColumnsTest()
{
var config = new OpenXmlConfiguration var config = new OpenXmlConfiguration
{ {
@ -880,7 +881,8 @@ namespace MiniExcelLibs.Tests
configuration: config configuration: config
); );
using (var stream = File.OpenRead(path)) { using (var stream = File.OpenRead(path))
{
var rows = stream.Query(useHeaderRow: true).ToList(); var rows = stream.Query(useHeaderRow: true).ToList();
Assert.Equal("MiniExcel", rows[0].Column1); Assert.Equal("MiniExcel", rows[0].Column1);
@ -890,9 +892,9 @@ namespace MiniExcelLibs.Tests
} }
Assert.Equal("A1:B3", Helpers.GetFirstSheetDimensionRefValue(path)); Assert.Equal("A1:B3", Helpers.GetFirstSheetDimensionRefValue(path));
//File.Delete(path); File.Delete(path);
} }
{ {
// test table // test table
var table = new DataTable(); var table = new DataTable();
@ -905,7 +907,7 @@ namespace MiniExcelLibs.Tests
table.Rows.Add(@"<test>Hello World</test>", -1234567890, false, DateTime.Now.Date); table.Rows.Add(@"<test>Hello World</test>", -1234567890, false, DateTime.Now.Date);
} }
var pathTable = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx"); var pathTable = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx");
MiniExcel.SaveAs(pathTable, table, configuration: config ); MiniExcel.SaveAs(pathTable, table, configuration: config);
Assert.Equal("A1:D3", Helpers.GetFirstSheetDimensionRefValue(pathTable)); Assert.Equal("A1:D3", Helpers.GetFirstSheetDimensionRefValue(pathTable));
@ -1392,7 +1394,7 @@ namespace MiniExcelLibs.Tests
{ {
table.Columns.Add("Column1", typeof(string)); table.Columns.Add("Column1", typeof(string));
table.Columns.Add("Column2", typeof(int)); table.Columns.Add("Column2", typeof(int));
table.Columns.Add("Column3", typeof(DateTime)); table.Columns.Add("Column3", typeof(DateTime));
table.Columns.Add("Column4", typeof(DateOnly)); table.Columns.Add("Column4", typeof(DateOnly));
table.Rows.Add("MiniExcel", 1, dateTime, onlyDate); table.Rows.Add("MiniExcel", 1, dateTime, onlyDate);
table.Rows.Add("Github", 2, dateTime, onlyDate); table.Rows.Add("Github", 2, dateTime, onlyDate);