From 84d82d071dbc454d0c23ce60f8b14f10b404a1b9 Mon Sep 17 00:00:00 2001 From: Victor Irzak Date: Sat, 10 Oct 2026 15:03:24 -0400 Subject: [PATCH 1/2] Add OpenXmlTableOptions to export sheets as Excel tables Each exported sheet gets one table over its header and data. Inserting into an existing workbook and columns placed with gaps are rejected, since a table can't cover either. --- src/MiniExcel.OpenXml/Constants/ExcelXml.cs | 20 +++ .../Constants/WorksheetXml.cs | 1 + src/MiniExcel.OpenXml/OpenXmlConfiguration.cs | 6 + src/MiniExcel.OpenXml/OpenXmlTableOptions.cs | 47 +++++++ .../Writer/OpenXmlWriter.XmlGeneration.cs | 38 +++++- src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs | 25 +++- .../MiniExcelOpenXmlTableExportAsyncTests.cs | 126 ++++++++++++++++++ .../MiniExcelOpenXmlTableExportTests.cs | 25 ++++ 8 files changed, 286 insertions(+), 2 deletions(-) create mode 100644 src/MiniExcel.OpenXml/OpenXmlTableOptions.cs create mode 100644 tests/MiniExcel.OpenXml.Tests/Tables/MiniExcelOpenXmlTableExportAsyncTests.cs create mode 100644 tests/MiniExcel.OpenXml.Tests/Tables/MiniExcelOpenXmlTableExportTests.cs diff --git a/src/MiniExcel.OpenXml/Constants/ExcelXml.cs b/src/MiniExcel.OpenXml/Constants/ExcelXml.cs index 7d6a500e..843b10fc 100644 --- a/src/MiniExcel.OpenXml/Constants/ExcelXml.cs +++ b/src/MiniExcel.OpenXml/Constants/ExcelXml.cs @@ -120,6 +120,26 @@ internal static string DrawingRelationship(int sheetIndex) internal static string DrawingRelationship(int sheetIndex, string drawingFileName) => $""""""; + internal static string Table(int id, string name, string reference, bool hasHeaderRow, IReadOnlyList columnNames, OpenXmlTableOptions options) + { + static string Flag(bool value) => value ? "1" : "0"; + + var sb = new StringBuilder(); + sb.Append(""""""); + sb.Append($""""""); + if (hasHeaderRow && options.ShowFilterButton) + sb.Append($""""""); + + sb.Append($""""""); + for (var i = 0; i < columnNames.Count; i++) + sb.Append($""""""); + sb.Append(""); + + sb.Append($""""""); + sb.Append("
"); + return sb.ToString(); + } + internal static string TableRelationship(int sheetIndex) => $""""""; diff --git a/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs b/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs index d5f14f9e..5ce3920b 100644 --- a/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs +++ b/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs @@ -69,4 +69,5 @@ internal static string Cell(string cellReference, string? cellType, string style internal static string Autofilter(string dimensionRef) => $""; internal static string Drawing(int sheetIndex) => $""; + internal static string TableParts(int sheetIndex) => $""; } diff --git a/src/MiniExcel.OpenXml/OpenXmlConfiguration.cs b/src/MiniExcel.OpenXml/OpenXmlConfiguration.cs index a5007bba..91541c2e 100644 --- a/src/MiniExcel.OpenXml/OpenXmlConfiguration.cs +++ b/src/MiniExcel.OpenXml/OpenXmlConfiguration.cs @@ -9,6 +9,12 @@ public class OpenXmlConfiguration : MiniExcelBaseConfiguration public bool FillMergedCells { get; set; } public TableStyles TableStyles { get; set; } = TableStyles.Default; public bool AutoFilter { get; set; } = true; + + /// + /// When set, each sheet's data is written as an Excel table in this style. The sheet-level is then left out. + /// + public OpenXmlTableOptions? Table { get; set; } + public bool RightToLeft { get; set; } = false; public int FreezeRowCount { get; set; } = 1; public int FreezeColumnCount { get; set; } = 0; diff --git a/src/MiniExcel.OpenXml/OpenXmlTableOptions.cs b/src/MiniExcel.OpenXml/OpenXmlTableOptions.cs new file mode 100644 index 00000000..abf74871 --- /dev/null +++ b/src/MiniExcel.OpenXml/OpenXmlTableOptions.cs @@ -0,0 +1,47 @@ +namespace MiniExcelLib.OpenXml; + +/// +/// Exports each sheet's data as an Excel table. Set it on . +/// +/// +/// The table covers the header row and the data, so columns must be adjacent. Combine it with +/// to let the table style alone format the cells. +/// +public class OpenXmlTableOptions +{ + /// + /// The table name. The first sheet's table gets this name and later sheets append their sheet number. + /// Defaults to Table1, Table2 and so on. + /// + public string? Name { get; set; } + + /// + /// A built-in table style such as TableStyleMedium2 or TableStyleLight9. + /// + public string Style { get; set; } = "TableStyleMedium2"; + + /// + /// Shows filter buttons in the header row. + /// + public bool ShowFilterButton { get; set; } = true; + + /// + /// Shades alternate rows. + /// + public bool ShowRowStripes { get; set; } = true; + + /// + /// Shades alternate columns. + /// + public bool ShowColumnStripes { get; set; } + + /// + /// Highlights the first column. + /// + public bool ShowFirstColumn { get; set; } + + /// + /// Highlights the last column. + /// + public bool ShowLastColumn { get; set; } +} diff --git a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs index 29e3aa00..b5aa2837 100644 --- a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs +++ b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs @@ -404,6 +404,39 @@ private string GetDrawingXml(int sheetIndex) return drawing.ToString(); } + private static readonly Regex TableNameRegex = new(@"^[A-Za-z_\][A-Za-z0-9_.]*$"); + + private string? GetTableXml(OpenXmlTableOptions options, List mappings, int maxRowIndex, int maxColumnIndex) + { + // Null mappings are gaps between columns placed by index, which would leave a table column without a header + if (mappings.Any(m => m is null)) + throw new NotSupportedException("A table needs adjacent columns, so it can't be used with column indexes that leave gaps"); + + if (maxColumnIndex == 0 || (!_printHeader && maxRowIndex == 0)) + return null; + + List columnNames = _printHeader + ? [.. mappings.Where(m => !m!.ExcelIgnoreColumn).Select(m => m!.ExcelColumnName ?? "")] + : [.. Enumerable.Range(1, maxColumnIndex).Select(i => $"Column{i}")]; + + if (columnNames.Any(string.IsNullOrEmpty)) + throw new InvalidOperationException("Table column names can't be empty"); + if (columnNames.Distinct(StringComparer.OrdinalIgnoreCase).Count() != columnNames.Count) + throw new InvalidOperationException("Table column names must be unique"); + + var name = options.Name is null ? $"Table{_currentSheetIndex}" + : _currentSheetIndex == 1 ? options.Name + : $"{options.Name}{_currentSheetIndex}"; + if (!TableNameRegex.IsMatch(name)) + throw new ArgumentException($"'{name}' is not a valid table name"); + + // A table needs at least one data row, so a header without data takes in the empty row below it + var lastRow = _printHeader ? Math.Max(maxRowIndex, 2) : maxRowIndex; + var reference = $"A1:{CellReferenceConverter.GetCellFromCoordinates(maxColumnIndex, lastRow)}"; + + return ExcelXml.Table(_currentSheetIndex, name, reference, _printHeader, columnNames, options); + } + private (string WorkbookContents, string WorkbookRelsContents, Dictionary SheetRelsContents) GenerateWorkbookItems() { var workbookContents = new StringBuilder(); @@ -419,7 +452,10 @@ private string GetDrawingXml(int sheetIndex) workbookRelsContents.AppendLine(ExcelXml.WorksheetRelationship(sheetDto)); //TODO: support multiple drawing - sheetsRelsDict.Add(sheetDto.SheetIdx, ExcelXml.DrawingRelationship(sheetId)); + var sheetRels = ExcelXml.DrawingRelationship(sheetId); + if (_tables.ContainsKey(sheetDto.SheetIdx)) + sheetRels += ExcelXml.TableRelationship(sheetDto.SheetIdx); + sheetsRelsDict.Add(sheetDto.SheetIdx, sheetRels); } return (workbookContents.ToString(), workbookRelsContents.ToString(), sheetsRelsDict); diff --git a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs index 2c7aeaff..2593681c 100644 --- a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs +++ b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs @@ -14,6 +14,9 @@ internal sealed partial class OpenXmlWriter : IMiniExcelWriter private readonly OpenXmlConfiguration _configuration; private readonly List _sheets = []; private readonly List _files = []; + + // Table parts by sheet index, written once all sheets are done + private readonly Dictionary _tables = []; private readonly string _sheetName; private readonly bool _printHeader; @@ -86,6 +89,7 @@ public async Task SaveAsAsync(IProgress? progress = null, Cancellati await GenerateSharedStringsAsync(cancellationToken).ConfigureAwait(false); await GenerateDrawingRelsAsync(cancellationToken).ConfigureAwait(false); await GenerateDrawingsAsync(cancellationToken).ConfigureAwait(false); + await GenerateTablesAsync(cancellationToken).ConfigureAwait(false); await GenerateWorkbookAsync(false, cancellationToken).ConfigureAwait(false); await GenerateContentTypesAsync(cancellationToken).ConfigureAwait(false); @@ -97,6 +101,8 @@ public async Task InsertAsync(bool overwriteSheet = false, IProgress? { if (!_configuration.FastMode) throw new InvalidOperationException("Insert requires fast mode to be enabled"); + if (_configuration.Table is not null) + throw new NotSupportedException("Tables can't be added when inserting a sheet into an existing workbook"); #if NET10_0_OR_GREATER await using var disposableArchive = _archive.ConfigureAwait(false); @@ -331,12 +337,19 @@ private async Task WriteValuesAsync(MiniExcelStreamWriter writer, object va await writer.WriteAsync(WorksheetXml.EndSheetData, cancellationToken).ConfigureAwait(false); - if (_configuration.AutoFilter) + // A sheet can't have its own autofilter on top of a table's + var table = _configuration.Table is { } tableOptions ? GetTableXml(tableOptions, mappings, maxRowIndex, maxColumnIndex) : null; + if (_configuration.AutoFilter && _configuration.Table is null) { await writer.WriteAsync(WorksheetXml.Autofilter(GetDimensionRef(maxRowIndex, maxColumnIndex)), cancellationToken).ConfigureAwait(false); } await writer.WriteAsync(WorksheetXml.Drawing(_currentSheetIndex), cancellationToken).ConfigureAwait(false); + if (table is not null) + { + _tables[_currentSheetIndex] = table; + await writer.WriteAsync(WorksheetXml.TableParts(_currentSheetIndex), cancellationToken).ConfigureAwait(false); + } await writer.WriteAsync(WorksheetXml.EndWorksheet, cancellationToken).ConfigureAwait(false); if (_archive.Mode == ZipArchiveMode.Update && dimensionPlaceholderPostition != 0) @@ -564,6 +577,16 @@ await CreateZipEntryAsync( cancellationToken).ConfigureAwait(false); } + [CreateSyncVersion] + private async Task GenerateTablesAsync(CancellationToken cancellationToken) + { + foreach (var (sheetIndex, table) in _tables) + { + cancellationToken.ThrowIfCancellationRequested(); + await CreateZipEntryAsync(ExcelFileNames.Table(sheetIndex), ExcelContentTypes.Table, table, cancellationToken).ConfigureAwait(false); + } + } + [CreateSyncVersion] private async Task GenerateWorkbookAsync(bool removeOriginalEntry = false, CancellationToken cancellationToken = default) { diff --git a/tests/MiniExcel.OpenXml.Tests/Tables/MiniExcelOpenXmlTableExportAsyncTests.cs b/tests/MiniExcel.OpenXml.Tests/Tables/MiniExcelOpenXmlTableExportAsyncTests.cs new file mode 100644 index 00000000..e55f4b12 --- /dev/null +++ b/tests/MiniExcel.OpenXml.Tests/Tables/MiniExcelOpenXmlTableExportAsyncTests.cs @@ -0,0 +1,126 @@ +using ClosedXML.Excel; + +namespace MiniExcelLib.OpenXml.Tests.Tables; + +public class MiniExcelOpenXmlTableExportAsyncTests +{ + private readonly OpenXmlExporter _exporter = MiniExcelV2.Exporters.GetOpenXmlExporter(); + private readonly OpenXmlImporter _importer = MiniExcelV2.Importers.GetOpenXmlImporter(); + + private class Weld + { + public int Id { get; set; } + public string? Name { get; set; } + public double Depth { get; set; } + } + + private class IndexedWeld + { + [MiniExcelColumnIndex(0)] + public int Id { get; set; } + + [MiniExcelColumnIndex(2)] + public double Depth { get; set; } + } + + private static readonly Weld[] Welds = + [ + new() { Id = 1, Name = "first", Depth = 0.1 }, + new() { Id = 2, Name = "second", Depth = 0.2 } + ]; + + private async Task ExportAsync(object value, OpenXmlConfiguration configuration, bool printHeader = true) + { + var stream = new MemoryStream(); + await _exporter.ExportAsync(stream, value, printHeader, configuration: configuration); + stream.Position = 0; + return stream; + } + + [Fact] + public async Task DataIsWrittenAsATableInTheChosenStyleAsync() + { + var configuration = new OpenXmlConfiguration + { + TableStyles = TableStyles.None, + Table = new OpenXmlTableOptions { Name = "Welds", Style = "TableStyleMedium15", ShowFilterButton = false } + }; + + using var stream = await ExportAsync(Welds, configuration); + using var workbook = new XLWorkbook(stream); + var table = workbook.Worksheet(1).Tables.Single(); + + Assert.Equal("Welds", table.Name); + Assert.Equal("A1:C3", table.RangeAddress.ToString()); + Assert.Equal(["Id", "Name", "Depth"], table.Fields.Select(f => f.Name)); + Assert.Equal(XLTableTheme.TableStyleMedium15, table.Theme); + Assert.False(table.ShowAutoFilter); + Assert.True(table.ShowRowStripes); + Assert.Null(workbook.Worksheet(1).AutoFilter.Range); + + stream.Position = 0; + var rows = await _importer.QueryTableAsync(stream, tableName: "Welds").ToListAsync(); + Assert.Equal(2, rows.Count); + Assert.Equal("second", (string)rows[1].Name); + } + + [Fact] + public async Task EachSheetGetsItsOwnTableAsync() + { + var sheets = new Dictionary { ["First"] = Welds, ["Second"] = Welds.Take(1) }; + + using var stream = await ExportAsync(sheets, new OpenXmlConfiguration { Table = new OpenXmlTableOptions() }); + using var workbook = new XLWorkbook(stream); + + Assert.Equal("Table1", workbook.Worksheet("First").Tables.Single().Name); + Assert.Equal("A1:C3", workbook.Worksheet("First").Tables.Single().RangeAddress.ToString()); + Assert.Equal("Table2", workbook.Worksheet("Second").Tables.Single().Name); + Assert.Equal("A1:C2", workbook.Worksheet("Second").Tables.Single().RangeAddress.ToString()); + Assert.True(workbook.Worksheet("First").Tables.Single().ShowAutoFilter); + } + + [Fact] + public async Task HeaderWithoutDataStillMakesATableAsync() + { + using var stream = await ExportAsync(Array.Empty(), new OpenXmlConfiguration { Table = new OpenXmlTableOptions() }); + using var workbook = new XLWorkbook(stream); + + Assert.Equal("A1:C2", workbook.Worksheet(1).Tables.Single().RangeAddress.ToString()); + } + + [Fact] + public async Task TableWithoutHeaderRowAsync() + { + using var stream = await ExportAsync(Welds, new OpenXmlConfiguration { Table = new OpenXmlTableOptions() }, printHeader: false); + using var workbook = new XLWorkbook(stream); + var table = workbook.Worksheet(1).Tables.Single(); + + Assert.False(table.ShowHeaderRow); + Assert.Equal("A1:C2", table.RangeAddress.ToString()); + } + + [Fact] + public async Task ColumnGapsAreRejectedAsync() + { + IndexedWeld[] rows = [new() { Id = 1, Depth = 0.1 }]; + + await Assert.ThrowsAsync(() => ExportAsync(rows, new OpenXmlConfiguration { Table = new OpenXmlTableOptions() })); + } + + [Fact] + public async Task InvalidTableNameIsRejectedAsync() + { + var configuration = new OpenXmlConfiguration { Table = new OpenXmlTableOptions { Name = "My Welds" } }; + + await Assert.ThrowsAsync(() => ExportAsync(Welds, configuration)); + } + + [Fact] + public async Task InsertingASheetWithATableIsRejectedAsync() + { + using var stream = await ExportAsync(Welds, new OpenXmlConfiguration()); + var configuration = new OpenXmlConfiguration { FastMode = true, Table = new OpenXmlTableOptions() }; + + await Assert.ThrowsAsync(() => _exporter.InsertSheetAsync(stream, Welds, "Second", configuration: configuration)); + } +} diff --git a/tests/MiniExcel.OpenXml.Tests/Tables/MiniExcelOpenXmlTableExportTests.cs b/tests/MiniExcel.OpenXml.Tests/Tables/MiniExcelOpenXmlTableExportTests.cs new file mode 100644 index 00000000..ac08956b --- /dev/null +++ b/tests/MiniExcel.OpenXml.Tests/Tables/MiniExcelOpenXmlTableExportTests.cs @@ -0,0 +1,25 @@ +using ClosedXML.Excel; + +namespace MiniExcelLib.OpenXml.Tests.Tables; + +public class MiniExcelOpenXmlTableExportTests +{ + private readonly OpenXmlExporter _exporter = MiniExcelV2.Exporters.GetOpenXmlExporter(); + + [Fact] + public void DataIsWrittenAsATable() + { + var rows = new[] { new { Id = 1, Name = "first" }, new { Id = 2, Name = "second" } }; + var configuration = new OpenXmlConfiguration { Table = new OpenXmlTableOptions { Style = "TableStyleLight9" } }; + + var stream = new MemoryStream(); + _exporter.Export(stream, rows, configuration: configuration); + stream.Position = 0; + + using var workbook = new XLWorkbook(stream); + var table = workbook.Worksheet(1).Tables.Single(); + Assert.Equal("Table1", table.Name); + Assert.Equal("A1:B3", table.RangeAddress.ToString()); + Assert.Equal(XLTableTheme.TableStyleLight9, table.Theme); + } +} From bb85b8bc80494cfcdb910bc3b06f32f18923ec5a Mon Sep 17 00:00:00 2001 From: Victor Irzak Date: Sat, 10 Oct 2026 15:12:13 -0400 Subject: [PATCH 2/2] Apply the header style to minimal styles and add a header row height A table formatted by its table style still needs a wrapped, aligned header, which TableStyles.None ignored. The fill stays out so it does not hide the table style. --- .../Constants/WorksheetXml.cs | 3 ++ .../Builder/MinimalSheetStyleBuilder.cs | 26 +++++++++++++- .../Styles/OpenXmlHeaderStyle.cs | 5 +++ .../Writer/OpenXmlWriter.CopyInsert.cs | 2 +- src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs | 8 ++--- .../MiniExcelOpenXmlTableExportAsyncTests.cs | 34 +++++++++++++++++++ 6 files changed, 72 insertions(+), 6 deletions(-) diff --git a/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs b/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs index 5ce3920b..4218d78e 100644 --- a/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs +++ b/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs @@ -37,6 +37,9 @@ internal static string PaneSelection(string pane, string? activeCell, string? sq "/>"); internal static string StartRow(int rowIndex) => $""; + internal static string StartRow(int rowIndex, double? height) => height is { } ht + ? $"" + : StartRow(rowIndex); internal const string EndRow = ""; internal const string StartCols = ""; diff --git a/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs b/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs index 7afce315..9115cd11 100644 --- a/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs +++ b/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs @@ -1,6 +1,6 @@ namespace MiniExcelLib.OpenXml.Styles.Builder; -internal partial class MinimalSheetStyleBuilder(SheetStyleBuilderContext context) : SheetStyleBuilderBase(context) +internal partial class MinimalSheetStyleBuilder(SheetStyleBuilderContext context, OpenXmlStyleOptions? styleOptions = null) : SheetStyleBuilderBase(context) { private static readonly SheetStyleElementInfos GenerateElementInfos = new() { @@ -88,6 +88,8 @@ protected override async Task GenerateCellXfAsync() * * */ + // The second xf is the header's, which takes the alignment of a header style but not its fill + var headerStyle = styleOptions?.HeaderStyle; for (int i = 0; i < 3; i++) { await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); @@ -96,6 +98,28 @@ protected override async Task GenerateCellXfAsync() await NewWriter.WriteAttributeStringAsync(null, "fillId", null, "0").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "borderId", null, "0").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "xfId", null, "0").ConfigureAwait(false); + if (i == 1 && headerStyle is not null) + { + /* + * + */ + await NewWriter.WriteAttributeStringAsync(null, "applyAlignment", null, "1").ConfigureAwait(false); + await NewWriter.WriteStartElementAsync(OldReader.Prefix, "alignment", OldReader.NamespaceURI).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "horizontal", null, headerStyle.HorizontalAlignment switch + { + HorizontalCellAlignment.Center => "center", + HorizontalCellAlignment.Right => "right", + _ => "general" + }).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "vertical", null, headerStyle.VerticalAlignment switch + { + VerticalCellAlignment.Top => "top", + VerticalCellAlignment.Center => "center", + _ => "bottom" + }).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "wrapText", null, headerStyle.WrapText ? "1" : "0").ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); + } await NewWriter.WriteEndElementAsync().ConfigureAwait(false); } diff --git a/src/MiniExcel.OpenXml/Styles/OpenXmlHeaderStyle.cs b/src/MiniExcel.OpenXml/Styles/OpenXmlHeaderStyle.cs index 95ea3f01..53478e14 100644 --- a/src/MiniExcel.OpenXml/Styles/OpenXmlHeaderStyle.cs +++ b/src/MiniExcel.OpenXml/Styles/OpenXmlHeaderStyle.cs @@ -23,4 +23,9 @@ public class OpenXmlHeaderStyle /// Vertical alignment /// public VerticalCellAlignment VerticalAlignment { get; set; } = VerticalCellAlignment.Bottom; + + /// + /// The height of the header row in points, or null for the default height + /// + public double? RowHeight { get; set; } } \ No newline at end of file diff --git a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.CopyInsert.cs b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.CopyInsert.cs index 635beb12..b0de1bd5 100644 --- a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.CopyInsert.cs +++ b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.CopyInsert.cs @@ -183,7 +183,7 @@ private async Task CopySheetStylesAndGetBuilderUtil oldStylesContext = new SheetStyleBuilderContext(_zipContentsMap, copiedArchive, Utf8WithBom); SheetStyleBuilderBase builder = _configuration.TableStyles switch { - TableStyles.None => new MinimalSheetStyleBuilder(oldStylesContext), + TableStyles.None => new MinimalSheetStyleBuilder(oldStylesContext, _configuration.StyleOptions), TableStyles.Default => new DefaultSheetStyleBuilder(oldStylesContext, _configuration.StyleOptions), _ => throw new InvalidEnumArgumentException(nameof(_configuration.TableStyles), (int)_configuration.TableStyles, typeof(TableStyles)) }; diff --git a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs index 2593681c..a2ae412b 100644 --- a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs +++ b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs @@ -296,7 +296,7 @@ private async Task WriteValuesAsync(MiniExcelStreamWriter writer, object va var currentRowIndex = 0; if (_printHeader) { - await PrintHeaderAsync(writer, mappings!, cancellationToken).ConfigureAwait(false); + await PrintHeaderAsync(writer, mappings!, _configuration.StyleOptions.HeaderStyle?.RowHeight, cancellationToken).ConfigureAwait(false); currentRowIndex++; } @@ -427,10 +427,10 @@ private static async Task WriteColumnsWidthsAsync(MiniExcelStreamWriter writer, } [CreateSyncVersion] - private static async Task PrintHeaderAsync(MiniExcelStreamWriter writer, List mappings, CancellationToken cancellationToken = default) + private static async Task PrintHeaderAsync(MiniExcelStreamWriter writer, List mappings, double? rowHeight, CancellationToken cancellationToken = default) { const int yIndex = 1; - await writer.WriteAsync(WorksheetXml.StartRow(yIndex), cancellationToken).ConfigureAwait(false); + await writer.WriteAsync(WorksheetXml.StartRow(yIndex, rowHeight), cancellationToken).ConfigureAwait(false); var xIndex = 1; foreach (var map in mappings) @@ -524,7 +524,7 @@ private async Task GetSheetStyleBuilderAsync(CancellationTok { SheetStyleBuilderBase builder = _configuration.TableStyles switch { - TableStyles.None => new MinimalSheetStyleBuilder(_sheetStyleBuilderContext), + TableStyles.None => new MinimalSheetStyleBuilder(_sheetStyleBuilderContext, _configuration.StyleOptions), TableStyles.Default => new DefaultSheetStyleBuilder(_sheetStyleBuilderContext, _configuration.StyleOptions), _ => throw new InvalidEnumArgumentException(nameof(_configuration.TableStyles), (int)_configuration.TableStyles, typeof(TableStyles)) }; diff --git a/tests/MiniExcel.OpenXml.Tests/Tables/MiniExcelOpenXmlTableExportAsyncTests.cs b/tests/MiniExcel.OpenXml.Tests/Tables/MiniExcelOpenXmlTableExportAsyncTests.cs index e55f4b12..541528d8 100644 --- a/tests/MiniExcel.OpenXml.Tests/Tables/MiniExcelOpenXmlTableExportAsyncTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/Tables/MiniExcelOpenXmlTableExportAsyncTests.cs @@ -1,4 +1,6 @@ using ClosedXML.Excel; +using MiniExcelLib.Core.Enums; +using MiniExcelLib.OpenXml.Styles; namespace MiniExcelLib.OpenXml.Tests.Tables; @@ -64,6 +66,38 @@ public async Task DataIsWrittenAsATableInTheChosenStyleAsync() Assert.Equal("second", (string)rows[1].Name); } + [Fact] + public async Task HeaderStyleAppliesWithoutCellStylesAsync() + { + var configuration = new OpenXmlConfiguration + { + TableStyles = TableStyles.None, + StyleOptions = new OpenXmlStyleOptions + { + HeaderStyle = new OpenXmlHeaderStyle + { + WrapText = true, + HorizontalAlignment = HorizontalCellAlignment.Center, + VerticalAlignment = VerticalCellAlignment.Center, + RowHeight = 45, + } + }, + Table = new OpenXmlTableOptions { Style = "TableStyleMedium15" } + }; + + using var stream = await ExportAsync(Welds, configuration); + using var workbook = new XLWorkbook(stream); + var sheet = workbook.Worksheet(1); + var header = sheet.Cell("A1").Style; + + Assert.True(header.Alignment.WrapText); + Assert.Equal(XLAlignmentHorizontalValues.Center, header.Alignment.Horizontal); + Assert.Equal(XLAlignmentVerticalValues.Center, header.Alignment.Vertical); + Assert.Equal(XLFillPatternValues.None, header.Fill.PatternType); + Assert.Equal(45, sheet.Row(1).Height); + Assert.False(sheet.Cell("A2").Style.Alignment.WrapText); + } + [Fact] public async Task EachSheetGetsItsOwnTableAsync() {