Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
4 changes: 2 additions & 2 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
5 changes: 3 additions & 2 deletions docs/schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,12 @@ var schema = Excel.Schema<Order>()
.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).

Expand Down
8 changes: 5 additions & 3 deletions docs/styling.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -76,12 +76,14 @@ var schema = Excel.Schema<Order>()
.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

Expand Down
2 changes: 1 addition & 1 deletion docs/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/CellSharp/CellSharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<PackageId>CellSharp</PackageId>
<Title>CellSharp</Title>
<Product>CellSharp</Product>
<Version>0.6.0-prerelease</Version>
<Version>0.6.1-prerelease</Version>
<Authors>Angelo Collura</Authors>
<Copyright>Copyright © 2026 Angelo Collura</Copyright>
<Description>A lightweight, strongly typed .NET library for reading and writing Excel XLSX files with schemas, validation, formulas, templates, and multi-sheet workbooks.</Description>
Expand Down
45 changes: 45 additions & 0 deletions src/CellSharp/ExcelSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,9 @@ public sealed class ExcelColumnBuilder<T, TValue>
private string? _format;
private double? _width;
private ExcelHorizontalAlignment? _alignment;
private ExcelVerticalAlignment? _verticalAlignment;
private ExcelHorizontalAlignment? _headerAlignment;
private ExcelVerticalAlignment? _headerVerticalAlignment;
private readonly List<ValidationRule> _validations = new();
private DeclarativeValidationRule? _declarativeValidation;
private ValueConverterDefinition? _converter;
Expand All @@ -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;
Expand Down Expand Up @@ -462,6 +468,42 @@ public ExcelColumnBuilder<T, TValue> Align(ExcelHorizontalAlignment alignment)
return this;
}

/// <summary>Sets the vertical alignment used for exported data cells in this column.</summary>
public ExcelColumnBuilder<T, TValue> VerticalAlign(ExcelVerticalAlignment alignment)
{
if (!Enum.IsDefined(typeof(ExcelVerticalAlignment), alignment))
{
throw new ArgumentOutOfRangeException(nameof(alignment));
}

_verticalAlignment = alignment;
return this;
}

/// <summary>Overrides the horizontal alignment used for this column's header cell.</summary>
public ExcelColumnBuilder<T, TValue> HeaderAlign(ExcelHorizontalAlignment alignment)
{
if (!Enum.IsDefined(typeof(ExcelHorizontalAlignment), alignment))
{
throw new ArgumentOutOfRangeException(nameof(alignment));
}

_headerAlignment = alignment;
return this;
}

/// <summary>Overrides the vertical alignment used for this column's header cell.</summary>
public ExcelColumnBuilder<T, TValue> HeaderVerticalAlign(ExcelVerticalAlignment alignment)
{
if (!Enum.IsDefined(typeof(ExcelVerticalAlignment), alignment))
{
throw new ArgumentOutOfRangeException(nameof(alignment));
}

_headerVerticalAlignment = alignment;
return this;
}

/// <summary>Uses a reusable bidirectional converter for this column.</summary>
public ExcelColumnBuilder<T, TValue> ConvertWith<TCellValue>(IExcelValueConverter<TValue, TCellValue> converter)
{
Expand Down Expand Up @@ -634,6 +676,9 @@ internal SchemaColumn ToColumn(bool validateSupportedType = true)
_format,
_width,
_alignment,
_verticalAlignment,
_headerAlignment,
_headerVerticalAlignment,
_sourceColumnNumber,
_sourceHeader,
_formula);
Expand Down
21 changes: 15 additions & 6 deletions src/CellSharp/Internal/CellStyleDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ internal CellStyleDefinition(
string foreground,
string background,
string border,
ExcelHorizontalAlignment alignment)
ExcelHorizontalAlignment alignment,
ExcelVerticalAlignment verticalAlignment)
{
FontName = fontName;
FontSize = fontSize;
Expand All @@ -18,6 +19,7 @@ internal CellStyleDefinition(
Background = background;
Border = border;
Alignment = alignment;
VerticalAlignment = verticalAlignment;
}

internal string FontName { get; }
Expand All @@ -34,6 +36,8 @@ internal CellStyleDefinition(

internal ExcelHorizontalAlignment Alignment { get; }

internal ExcelVerticalAlignment VerticalAlignment { get; }

internal CellStyleDefinition With(HeaderStyleOverride? overrideStyle)
{
if (overrideStyle is null)
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -69,7 +75,8 @@ internal CellStyleDefinition WithAlignment(ExcelHorizontalAlignment? alignment)
Foreground,
Background,
Border,
alignment.Value);
alignment ?? Alignment,
verticalAlignment ?? VerticalAlignment);

public bool Equals(CellStyleDefinition? other)
{
Expand All @@ -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);
Expand All @@ -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;
}
}
Expand Down
15 changes: 15 additions & 0 deletions src/CellSharp/Internal/ExportProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExcelFormulaContext, string>? formula = null)
Expand All @@ -19,6 +22,9 @@ private ExportProperty(
Format = format;
Width = width;
Alignment = alignment;
VerticalAlignment = verticalAlignment;
HeaderAlignment = headerAlignment;
HeaderVerticalAlignment = headerVerticalAlignment;
DeclarativeValidation = declarativeValidation;
Converter = converter;
Formula = formula;
Expand All @@ -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; }
Expand Down Expand Up @@ -61,6 +73,9 @@ internal static IReadOnlyList<ExportProperty> For<T>(ExcelSchema<T>? 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))
Expand Down
12 changes: 12 additions & 0 deletions src/CellSharp/Internal/SchemaColumn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ internal SchemaColumn(
string? format,
double? width,
ExcelHorizontalAlignment? alignment,
ExcelVerticalAlignment? verticalAlignment,
ExcelHorizontalAlignment? headerAlignment,
ExcelVerticalAlignment? headerVerticalAlignment,
int? sourceColumnNumber,
string? sourceHeader,
Func<ExcelFormulaContext, string>? formula)
Expand All @@ -30,6 +33,9 @@ internal SchemaColumn(
Format = format;
Width = width;
Alignment = alignment;
VerticalAlignment = verticalAlignment;
HeaderAlignment = headerAlignment;
HeaderVerticalAlignment = headerVerticalAlignment;
SourceColumnNumber = sourceColumnNumber;
SourceHeader = sourceHeader;
Formula = formula;
Expand All @@ -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; }
Expand Down
Loading