From 4aa2c8d77c87d95eb25d9d046904d196d17b9c4b Mon Sep 17 00:00:00 2001 From: ang Date: Thu, 23 Jul 2026 18:16:06 +0200 Subject: [PATCH] Add configurable header alignment --- CHANGELOG.md | 4 ++ docs/design.md | 4 +- docs/schemas.md | 5 +- docs/styling.md | 8 ++- docs/templates.md | 2 +- src/CellSharp/CellSharp.csproj | 2 +- src/CellSharp/ExcelSchema.cs | 45 ++++++++++++ src/CellSharp/Internal/CellStyleDefinition.cs | 21 ++++-- src/CellSharp/Internal/ExportProperty.cs | 15 ++++ src/CellSharp/Internal/SchemaColumn.cs | 12 ++++ .../Internal/WorkbookStyleCatalog.cs | 12 ++-- src/CellSharp/Internal/XlsxWorksheetLayout.cs | 4 +- src/CellSharp/Internal/XlsxWorksheetWriter.cs | 8 +-- tests/CellSharp.Tests/ExcelStylingTests.cs | 68 +++++++++++++++++++ 14 files changed, 184 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afc86ad..91692fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.6.1-prerelease + +- Added per-column vertical alignment and configurable header horizontal and vertical alignment. Headers inherit their column alignment unless explicitly overridden. + ## 0.6.0-prerelease - Prepared the public prerelease with strongly typed read/write APIs, schemas, validation, formulas, templates, tables, images, report components, and multi-sheet workbooks. diff --git a/docs/design.md b/docs/design.md index e81ed38..21efed1 100644 --- a/docs/design.md +++ b/docs/design.md @@ -68,9 +68,9 @@ Formula-looking strings are always emitted as inline text cells, not formula cel ## Styling -Schema describes data structure; write options describe workbook presentation. A schema column may carry small export hints (`Format`, `Width`, and `Align`) because they are attached to a stable known field; they are ignored by the import pipeline and never change values. `ExcelTheme` is a shortcut that resolves internally into an `ExcelStyleTemplate`. A custom immutable `ExcelStyleTemplate` replaces the theme as the base visual identity and has no Open XML or workbook state, so one static instance can be safely reused across exports. +Schema describes data structure; write options describe workbook presentation. A schema column may carry small export hints (`Format`, `Width`, `Align`, `VerticalAlign`, `HeaderAlign`, and `HeaderVerticalAlign`) because they are attached to a stable known field; they are ignored by the import pipeline and never change values. `ExcelTheme` is a shortcut that resolves internally into an `ExcelStyleTemplate`. A custom immutable `ExcelStyleTemplate` replaces the theme as the base visual identity and has no Open XML or workbook state, so one static instance can be safely reused across exports. -Precedence is column format/alignment, then explicit `HeaderStyle` values, then the custom template or resolved built-in theme, then fallback defaults. Explicit column width wins over write-level autofit estimation. Alternating rows obtain their color from the active base template instead of from writer logic. +Header alignment precedence is an explicit `HeaderAlign` or `HeaderVerticalAlign` value, then the corresponding column alignment, then the centered fallback. Other style precedence is explicit `HeaderStyle` values, then the custom template or resolved built-in theme, then fallback defaults. Explicit column width wins over write-level autofit estimation. Alternating rows obtain their color from the active base template instead of from writer logic. The Open XML catalog deduplicates fonts, fills, borders, cell formats, and equivalent custom number formats. This keeps large exports compact while preserving a backend-independent public model. diff --git a/docs/schemas.md b/docs/schemas.md index 0111399..487aa67 100644 --- a/docs/schemas.md +++ b/docs/schemas.md @@ -108,11 +108,12 @@ var schema = Excel.Schema() .Width(14)) .Column(x => x.Total, column => column .Format("#,##0.00") - .Align(ExcelHorizontalAlignment.Right)) + .Align(ExcelHorizontalAlignment.Right) + .HeaderAlign(ExcelHorizontalAlignment.Center)) .Build(); ``` -`Format` accepts an Excel format-code string and writes it to the XLSX style table; CellSharp does not parse it. Format codes are invariant: `#,##0.00` displays `1` as `1,00` in Italian Excel, so decimal placeholders must use `.` rather than `,`. `Width` must be greater than zero and no greater than 255. `Align` accepts the deliberately small `General`, `Left`, `Center`, and `Right` enum. These settings are consumed by `Write` and `CreateTemplate`; the same schema stays reusable for `Read`. See [styling](styling.md) and [generated templates](templates.md) for precedence and workbook-level options. +`Format` accepts an Excel format-code string and writes it to the XLSX style table; CellSharp does not parse it. Format codes are invariant: `#,##0.00` displays `1` as `1,00` in Italian Excel, so decimal placeholders must use `.` rather than `,`. `Width` must be greater than zero and no greater than 255. `Align` and `VerticalAlign` set data-cell alignment and are inherited by the header. `HeaderAlign` and `HeaderVerticalAlign` override only that header. These settings are consumed by `Write` and `CreateTemplate`; the same schema stays reusable for `Read`. See [styling](styling.md) and [generated templates](templates.md) for precedence and workbook-level options. `Formula(...)` is a separate write-time setting: it produces a native formula cell for each data row. The property's converted value is stored as that cell's initial cached result, while the formula remains authoritative when a spreadsheet recalculates it. It can be combined with format, width, alignment, converters, and custom `.Validate(...)` predicates, but not native declarative validation. See [formulas](formulas.md). diff --git a/docs/styling.md b/docs/styling.md index 9c455db..c46656f 100644 --- a/docs/styling.md +++ b/docs/styling.md @@ -60,7 +60,7 @@ Excel.Write("customers.xlsx", customers, options => options Colors use `#RRGGBB` and are validated before the workbook is created. The precedence is: ```text -Column format/alignment > explicit HeaderStyle values > custom template or built-in theme > library defaults +Explicit header alignment > column alignment > explicit HeaderStyle values > custom template or built-in theme > library defaults ``` ## Column formatting @@ -76,12 +76,14 @@ var schema = Excel.Schema() .Column(x => x.Total, column => column .Header("Total") .Format("#,##0.00") - .Align(ExcelHorizontalAlignment.Right)) + .Align(ExcelHorizontalAlignment.Right) + .VerticalAlign(ExcelVerticalAlignment.Center) + .HeaderAlign(ExcelHorizontalAlignment.Center)) .Column(x => x.Discount, column => column.Format("0.00%")) .Build(); ``` -`Format` is an Excel format-code string. CellSharp stores it in the XLSX style table and deliberately does not parse it or use it for input conversion. Decimal properties default to `0.00`, which Excel displays as `1,00` when the workbook is opened with an Italian locale. Use `.` in format codes even for localized output: Excel applies the user's decimal separator when it renders the value. Equivalent format codes are reused in the style catalog. `Width` must be greater than zero and no greater than 255. `Align` supports the small `General`, `Left`, `Center`, and `Right` enum. +`Format` is an Excel format-code string. CellSharp stores it in the XLSX style table and deliberately does not parse it or use it for input conversion. Decimal properties default to `0.00`, which Excel displays as `1,00` when the workbook is opened with an Italian locale. Use `.` in format codes even for localized output: Excel applies the user's decimal separator when it renders the value. Equivalent format codes are reused in the style catalog. `Width` must be greater than zero and no greater than 255. `Align` and `VerticalAlign` apply to data cells and, by default, their matching header cells. Use `HeaderAlign` and `HeaderVerticalAlign` to override just the header. Without either configuration, headers remain centered horizontally and vertically. ## Widths and frozen headers diff --git a/docs/templates.md b/docs/templates.md index 3094a45..d0fcedf 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -44,7 +44,7 @@ Excel.CreateTemplate( The generated worksheet uses the schema's declared order and headers. `Optional()` columns are included: optional means an imported workbook may omit that header, not that the recommended blank template should omit it. `Ignore()` columns are excluded. -`Format(...)`, `Width(...)`, and `Align(...)` are carried into the template. CellSharp stores a default XLSX style on each column rather than adding empty placeholder rows. This keeps the workbook empty while letting cells entered manually inherit column format and alignment in spreadsheet applications. `DateTime` columns receive CellSharp's usual date display format and `decimal` columns receive `0.00` when no explicit format is configured. Format codes use invariant XLSX syntax, so `0.00` displays as `1,00` for a value of `1` in Italian Excel. +`Format(...)`, `Width(...)`, `Align(...)`, and `VerticalAlign(...)` are carried into the template. `Align(...)` and `VerticalAlign(...)` also determine the matching header alignment unless `HeaderAlign(...)` or `HeaderVerticalAlign(...)` overrides it. CellSharp stores a default XLSX style on each column rather than adding empty placeholder rows. This keeps the workbook empty while letting cells entered manually inherit column format and alignment in spreadsheet applications. `DateTime` columns receive CellSharp's usual date display format and `decimal` columns receive `0.00` when no explicit format is configured. Format codes use invariant XLSX syntax, so `0.00` displays as `1,00` for a value of `1` in Italian Excel. ## Write options on empty workbooks diff --git a/src/CellSharp/CellSharp.csproj b/src/CellSharp/CellSharp.csproj index ca42c4e..0a92fa9 100644 --- a/src/CellSharp/CellSharp.csproj +++ b/src/CellSharp/CellSharp.csproj @@ -9,7 +9,7 @@ CellSharp CellSharp CellSharp - 0.6.0-prerelease + 0.6.1-prerelease Angelo Collura Copyright © 2026 Angelo Collura A lightweight, strongly typed .NET library for reading and writing Excel XLSX files with schemas, validation, formulas, templates, and multi-sheet workbooks. diff --git a/src/CellSharp/ExcelSchema.cs b/src/CellSharp/ExcelSchema.cs index 7e37dfb..5d53cbf 100644 --- a/src/CellSharp/ExcelSchema.cs +++ b/src/CellSharp/ExcelSchema.cs @@ -338,6 +338,9 @@ public sealed class ExcelColumnBuilder private string? _format; private double? _width; private ExcelHorizontalAlignment? _alignment; + private ExcelVerticalAlignment? _verticalAlignment; + private ExcelHorizontalAlignment? _headerAlignment; + private ExcelVerticalAlignment? _headerVerticalAlignment; private readonly List _validations = new(); private DeclarativeValidationRule? _declarativeValidation; private ValueConverterDefinition? _converter; @@ -360,6 +363,9 @@ internal ExcelColumnBuilder(PropertyInfo property, SchemaColumn column) _format = column.Format; _width = column.Width; _alignment = column.Alignment; + _verticalAlignment = column.VerticalAlignment; + _headerAlignment = column.HeaderAlignment; + _headerVerticalAlignment = column.HeaderVerticalAlignment; _validations.AddRange(column.Validations); _declarativeValidation = column.DeclarativeValidation; _converter = column.Converter; @@ -462,6 +468,42 @@ public ExcelColumnBuilder Align(ExcelHorizontalAlignment alignment) return this; } + /// Sets the vertical alignment used for exported data cells in this column. + public ExcelColumnBuilder VerticalAlign(ExcelVerticalAlignment alignment) + { + if (!Enum.IsDefined(typeof(ExcelVerticalAlignment), alignment)) + { + throw new ArgumentOutOfRangeException(nameof(alignment)); + } + + _verticalAlignment = alignment; + return this; + } + + /// Overrides the horizontal alignment used for this column's header cell. + public ExcelColumnBuilder HeaderAlign(ExcelHorizontalAlignment alignment) + { + if (!Enum.IsDefined(typeof(ExcelHorizontalAlignment), alignment)) + { + throw new ArgumentOutOfRangeException(nameof(alignment)); + } + + _headerAlignment = alignment; + return this; + } + + /// Overrides the vertical alignment used for this column's header cell. + public ExcelColumnBuilder HeaderVerticalAlign(ExcelVerticalAlignment alignment) + { + if (!Enum.IsDefined(typeof(ExcelVerticalAlignment), alignment)) + { + throw new ArgumentOutOfRangeException(nameof(alignment)); + } + + _headerVerticalAlignment = alignment; + return this; + } + /// Uses a reusable bidirectional converter for this column. public ExcelColumnBuilder ConvertWith(IExcelValueConverter converter) { @@ -634,6 +676,9 @@ internal SchemaColumn ToColumn(bool validateSupportedType = true) _format, _width, _alignment, + _verticalAlignment, + _headerAlignment, + _headerVerticalAlignment, _sourceColumnNumber, _sourceHeader, _formula); diff --git a/src/CellSharp/Internal/CellStyleDefinition.cs b/src/CellSharp/Internal/CellStyleDefinition.cs index 9d6a5ba..0918ace 100644 --- a/src/CellSharp/Internal/CellStyleDefinition.cs +++ b/src/CellSharp/Internal/CellStyleDefinition.cs @@ -9,7 +9,8 @@ internal CellStyleDefinition( string foreground, string background, string border, - ExcelHorizontalAlignment alignment) + ExcelHorizontalAlignment alignment, + ExcelVerticalAlignment verticalAlignment) { FontName = fontName; FontSize = fontSize; @@ -18,6 +19,7 @@ internal CellStyleDefinition( Background = background; Border = border; Alignment = alignment; + VerticalAlignment = verticalAlignment; } internal string FontName { get; } @@ -34,6 +36,8 @@ internal CellStyleDefinition( internal ExcelHorizontalAlignment Alignment { get; } + internal ExcelVerticalAlignment VerticalAlignment { get; } + internal CellStyleDefinition With(HeaderStyleOverride? overrideStyle) { if (overrideStyle is null) @@ -48,7 +52,8 @@ internal CellStyleDefinition With(HeaderStyleOverride? overrideStyle) overrideStyle.Foreground ?? Foreground, overrideStyle.Background ?? Background, Border, - Alignment); + Alignment, + VerticalAlignment); } internal CellStyleDefinition WithBackground(string background) => new( @@ -58,9 +63,10 @@ internal CellStyleDefinition With(HeaderStyleOverride? overrideStyle) Foreground, background, Border, - Alignment); + Alignment, + VerticalAlignment); - internal CellStyleDefinition WithAlignment(ExcelHorizontalAlignment? alignment) => alignment is null + internal CellStyleDefinition WithAlignment(ExcelHorizontalAlignment? alignment, ExcelVerticalAlignment? verticalAlignment) => alignment is null && verticalAlignment is null ? this : new CellStyleDefinition( FontName, @@ -69,7 +75,8 @@ internal CellStyleDefinition WithAlignment(ExcelHorizontalAlignment? alignment) Foreground, Background, Border, - alignment.Value); + alignment ?? Alignment, + verticalAlignment ?? VerticalAlignment); public bool Equals(CellStyleDefinition? other) { @@ -80,7 +87,8 @@ public bool Equals(CellStyleDefinition? other) && Foreground == other.Foreground && Background == other.Background && Border == other.Border - && Alignment == other.Alignment; + && Alignment == other.Alignment + && VerticalAlignment == other.VerticalAlignment; } public override bool Equals(object? obj) => Equals(obj as CellStyleDefinition); @@ -97,6 +105,7 @@ public override int GetHashCode() hash = (hash * 31) + Background.GetHashCode(); hash = (hash * 31) + Border.GetHashCode(); hash = (hash * 31) + Alignment.GetHashCode(); + hash = (hash * 31) + VerticalAlignment.GetHashCode(); return hash; } } diff --git a/src/CellSharp/Internal/ExportProperty.cs b/src/CellSharp/Internal/ExportProperty.cs index 4bcb181..71e1fbe 100644 --- a/src/CellSharp/Internal/ExportProperty.cs +++ b/src/CellSharp/Internal/ExportProperty.cs @@ -10,6 +10,9 @@ private ExportProperty( string? format = null, double? width = null, ExcelHorizontalAlignment? alignment = null, + ExcelVerticalAlignment? verticalAlignment = null, + ExcelHorizontalAlignment? headerAlignment = null, + ExcelVerticalAlignment? headerVerticalAlignment = null, DeclarativeValidationRule? declarativeValidation = null, ValueConverterDefinition? converter = null, Func? formula = null) @@ -19,6 +22,9 @@ private ExportProperty( Format = format; Width = width; Alignment = alignment; + VerticalAlignment = verticalAlignment; + HeaderAlignment = headerAlignment; + HeaderVerticalAlignment = headerVerticalAlignment; DeclarativeValidation = declarativeValidation; Converter = converter; Formula = formula; @@ -34,6 +40,12 @@ private ExportProperty( internal ExcelHorizontalAlignment? Alignment { get; } + internal ExcelVerticalAlignment? VerticalAlignment { get; } + + internal ExcelHorizontalAlignment? HeaderAlignment { get; } + + internal ExcelVerticalAlignment? HeaderVerticalAlignment { get; } + internal DeclarativeValidationRule? DeclarativeValidation { get; } internal ValueConverterDefinition? Converter { get; } @@ -61,6 +73,9 @@ internal static IReadOnlyList For(ExcelSchema? schema, Exc column.Column.Format, column.Column.Width, column.Column.Alignment, + column.Column.VerticalAlignment, + column.Column.HeaderAlignment, + column.Column.HeaderVerticalAlignment, column.Column.DeclarativeValidation, column.Column.Converter, column.Column.Formula)) diff --git a/src/CellSharp/Internal/SchemaColumn.cs b/src/CellSharp/Internal/SchemaColumn.cs index bd630a0..26cc1da 100644 --- a/src/CellSharp/Internal/SchemaColumn.cs +++ b/src/CellSharp/Internal/SchemaColumn.cs @@ -16,6 +16,9 @@ internal SchemaColumn( string? format, double? width, ExcelHorizontalAlignment? alignment, + ExcelVerticalAlignment? verticalAlignment, + ExcelHorizontalAlignment? headerAlignment, + ExcelVerticalAlignment? headerVerticalAlignment, int? sourceColumnNumber, string? sourceHeader, Func? formula) @@ -30,6 +33,9 @@ internal SchemaColumn( Format = format; Width = width; Alignment = alignment; + VerticalAlignment = verticalAlignment; + HeaderAlignment = headerAlignment; + HeaderVerticalAlignment = headerVerticalAlignment; SourceColumnNumber = sourceColumnNumber; SourceHeader = sourceHeader; Formula = formula; @@ -55,6 +61,12 @@ internal SchemaColumn( internal ExcelHorizontalAlignment? Alignment { get; } + internal ExcelVerticalAlignment? VerticalAlignment { get; } + + internal ExcelHorizontalAlignment? HeaderAlignment { get; } + + internal ExcelVerticalAlignment? HeaderVerticalAlignment { get; } + internal int? SourceColumnNumber { get; } internal string? SourceHeader { get; } diff --git a/src/CellSharp/Internal/WorkbookStyleCatalog.cs b/src/CellSharp/Internal/WorkbookStyleCatalog.cs index 9db56b2..256002f 100644 --- a/src/CellSharp/Internal/WorkbookStyleCatalog.cs +++ b/src/CellSharp/Internal/WorkbookStyleCatalog.cs @@ -39,7 +39,7 @@ internal WorkbookStyleCatalog(WorkbookPart workbookPart) _differentialFormats); } - internal uint HeaderStyleIndex(ExcelWriteOptions options) + internal uint HeaderStyleIndex(ExportProperty property, ExcelWriteOptions options) { var template = options.Template ?? WorkbookTheme.For(options.Theme); var header = new CellStyleDefinition( @@ -49,7 +49,8 @@ internal uint HeaderStyleIndex(ExcelWriteOptions options) HexColor.Normalize(template.HeaderTextColor, nameof(template.HeaderTextColor)), HexColor.Normalize(template.HeaderBackgroundColor, nameof(template.HeaderBackgroundColor)), HexColor.Normalize(template.BorderColor, nameof(template.BorderColor)), - ExcelHorizontalAlignment.Center); + property.HeaderAlignment ?? property.Alignment ?? ExcelHorizontalAlignment.Center, + property.HeaderVerticalAlignment ?? property.VerticalAlignment ?? ExcelVerticalAlignment.Center); return Register(header.With(options.HeaderStyle)); } @@ -74,14 +75,15 @@ private uint DataStyleIndex(ExportProperty property, bool isDate, bool isDecimal HexColor.Normalize(template.DataTextColor, nameof(template.DataTextColor)), HexColor.Normalize(template.DataBackgroundColor, nameof(template.DataBackgroundColor)), HexColor.Normalize(template.BorderColor, nameof(template.BorderColor)), - ExcelHorizontalAlignment.Left); + ExcelHorizontalAlignment.Left, + ExcelVerticalAlignment.Center); var alternateBackground = HexColor.Normalize( template.AlternateRowBackgroundColor ?? template.DataBackgroundColor, nameof(template.AlternateRowBackgroundColor)); var style = options.AlternatingRows && dataRowIndex % 2 == 1 ? dataStyle.WithBackground(alternateBackground) : dataStyle; - style = style.WithAlignment(property.Alignment); + style = style.WithAlignment(property.Alignment, property.VerticalAlignment); var numberFormat = property.Format ?? (isDate ? DefaultDateFormat : isDecimal ? DefaultDecimalFormat : null); return Register(style, numberFormat); } @@ -216,7 +218,7 @@ private uint Register(CellStyleDefinition style, string? numberFormat = null) Bold: style.Bold, Italic: false, Underline: ExcelUnderline.None, Strikethrough: false, FontSize: style.FontSize, FontName: style.FontName, FontColor: style.Foreground, FillColor: style.Background, Horizontal: style.Alignment switch { ExcelHorizontalAlignment.General => ExcelCellHorizontalAlignment.General, ExcelHorizontalAlignment.Left => ExcelCellHorizontalAlignment.Left, ExcelHorizontalAlignment.Center => ExcelCellHorizontalAlignment.Center, ExcelHorizontalAlignment.Right => ExcelCellHorizontalAlignment.Right, _ => throw new ArgumentOutOfRangeException(nameof(style)) }, - Vertical: ExcelVerticalAlignment.Center, Rotation: null, VerticalText: false, Wrap: false, Shrink: false, + Vertical: style.VerticalAlignment, Rotation: null, VerticalText: false, Wrap: false, Shrink: false, NumberFormat: numberFormat, Border: new BorderDefinition(new BorderSideDefinition(ExcelBorderStyle.Thin, borderColor), new BorderSideDefinition(ExcelBorderStyle.Thin, borderColor), new BorderSideDefinition(ExcelBorderStyle.Thin, borderColor), new BorderSideDefinition(ExcelBorderStyle.Thin, borderColor))); var index = ComposeStyleIndex(null, resolved); diff --git a/src/CellSharp/Internal/XlsxWorksheetLayout.cs b/src/CellSharp/Internal/XlsxWorksheetLayout.cs index 3d90fec..1ca19ba 100644 --- a/src/CellSharp/Internal/XlsxWorksheetLayout.cs +++ b/src/CellSharp/Internal/XlsxWorksheetLayout.cs @@ -34,12 +34,12 @@ internal static void AddFrozenHeaderView(Worksheet worksheet, ResolvedWorksheetS })); } - internal static Row HeaderRow(IEnumerable properties, uint styleIndex) + internal static Row HeaderRow(IEnumerable properties, WorkbookStyleCatalog styles, ExcelWriteOptions options) { var row = new Row(); foreach (var property in properties) { - row.AppendChild(CellValueWriter.Create(property.Header, styleIndex)); + row.AppendChild(CellValueWriter.Create(property.Header, styles.HeaderStyleIndex(property, options))); } return row; diff --git a/src/CellSharp/Internal/XlsxWorksheetWriter.cs b/src/CellSharp/Internal/XlsxWorksheetWriter.cs index c402a81..3ee33da 100644 --- a/src/CellSharp/Internal/XlsxWorksheetWriter.cs +++ b/src/CellSharp/Internal/XlsxWorksheetWriter.cs @@ -51,7 +51,7 @@ internal static WorksheetValidationContext WriteData( var dataStart = layout?.DataStart; var startRow = dataStart?.FromRow ?? 1; var startColumn = dataStart?.FromColumn ?? 1; - sheetData.AppendChild(HeaderRow(properties, styles.HeaderStyleIndex(options), startRow, startColumn)); + sheetData.AppendChild(HeaderRow(properties, styles, options, startRow, startColumn)); var dataRowIndex = 0; var hasFormulaCells = false; foreach (var item in rows) @@ -124,7 +124,7 @@ internal static WorksheetValidationContext WriteTemplate( var dataStart = layout?.DataStart; var startRow = dataStart?.FromRow ?? 1; var startColumn = dataStart?.FromColumn ?? 1; - sheetData.AppendChild(HeaderRow(properties, styles.HeaderStyleIndex(options), startRow, startColumn)); + sheetData.AppendChild(HeaderRow(properties, styles, options, startRow, startColumn)); worksheetPart.Worksheet = worksheet; XlsxWorksheetLayout.AddSheet(workbookPart, worksheetPart, schema.SheetName); @@ -144,9 +144,9 @@ internal static WorksheetValidationContext WriteLayout(WorkbookPart workbookPart return new WorksheetValidationContext(worksheetPart, worksheet, Array.Empty(), hasFormulaCells, 0, null, layout.Name, WorksheetSettingsDefinition.From(ExcelWriteOptions.Default), 1, 1, layout); } - private static Row HeaderRow(IEnumerable properties, uint styleIndex, int rowNumber, int startColumn) + private static Row HeaderRow(IEnumerable properties, WorkbookStyleCatalog styles, ExcelWriteOptions options, int rowNumber, int startColumn) { - var row = XlsxWorksheetLayout.HeaderRow(properties, styleIndex); + var row = XlsxWorksheetLayout.HeaderRow(properties, styles, options); row.RowIndex = (uint)rowNumber; var cells = row.Elements().ToArray(); for (var index = 0; index < cells.Length; index++) cells[index].CellReference = ExcelRangeReference.ColumnName(startColumn + index) + rowNumber; diff --git a/tests/CellSharp.Tests/ExcelStylingTests.cs b/tests/CellSharp.Tests/ExcelStylingTests.cs index b86ca75..bdc2058 100644 --- a/tests/CellSharp.Tests/ExcelStylingTests.cs +++ b/tests/CellSharp.Tests/ExcelStylingTests.cs @@ -79,6 +79,74 @@ public void WriteAppliesCustomHeaderStylesOverTheTheme() } } + [Fact] + public void SchemaColumnAlignmentFlowsToHeadersUnlessTheHeaderOverridesIt() + { + var path = TemporaryPath(); + var schema = Excel.Schema() + .Column(customer => customer.Name, column => column + .Align(ExcelHorizontalAlignment.Left) + .VerticalAlign(ExcelVerticalAlignment.Top)) + .Column(customer => customer.Id, column => column + .Align(ExcelHorizontalAlignment.Right) + .VerticalAlign(ExcelVerticalAlignment.Bottom) + .HeaderAlign(ExcelHorizontalAlignment.Center) + .HeaderVerticalAlign(ExcelVerticalAlignment.Justify)) + .Build(); + + try + { + Excel.Write(path, Customers(), schema); + + using var document = SpreadsheetDocument.Open(path, false); + var rows = document.WorkbookPart!.WorksheetParts.Single().Worksheet! + .GetFirstChild()! + .Elements() + .ToArray(); + var headers = rows[0].Elements().ToArray(); + var values = rows[1].Elements().ToArray(); + + Assert.Equal(HorizontalAlignmentValues.Left, Format(document, headers[0]).Alignment!.Horizontal!.Value); + Assert.Equal(VerticalAlignmentValues.Top, Format(document, headers[0]).Alignment!.Vertical!.Value); + Assert.Equal(HorizontalAlignmentValues.Center, Format(document, headers[1]).Alignment!.Horizontal!.Value); + Assert.Equal(VerticalAlignmentValues.Justify, Format(document, headers[1]).Alignment!.Vertical!.Value); + Assert.Equal(HorizontalAlignmentValues.Left, Format(document, values[0]).Alignment!.Horizontal!.Value); + Assert.Equal(VerticalAlignmentValues.Top, Format(document, values[0]).Alignment!.Vertical!.Value); + Assert.Equal(HorizontalAlignmentValues.Right, Format(document, values[1]).Alignment!.Horizontal!.Value); + Assert.Equal(VerticalAlignmentValues.Bottom, Format(document, values[1]).Alignment!.Vertical!.Value); + } + finally + { + Delete(path); + } + } + + [Fact] + public void TemplateHeadersUseTheConfiguredColumnAlignment() + { + var path = TemporaryPath(); + var schema = Excel.Schema() + .Column(customer => customer.Name, column => column + .HeaderAlign(ExcelHorizontalAlignment.Right) + .HeaderVerticalAlign(ExcelVerticalAlignment.Bottom)) + .Build(); + + try + { + Excel.CreateTemplate(path, schema); + + using var document = SpreadsheetDocument.Open(path, false); + var header = Cells(document).Single(); + + Assert.Equal(HorizontalAlignmentValues.Right, Format(document, header).Alignment!.Horizontal!.Value); + Assert.Equal(VerticalAlignmentValues.Bottom, Format(document, header).Alignment!.Vertical!.Value); + } + finally + { + Delete(path); + } + } + [Fact] public void WriteRejectsInvalidHeaderColorsBeforeCreatingAWorkbook() {