diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..f7b9277 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,10 @@ + +# Project structure +This project allows to generat Excel reports using multiple Excel writter libraries (EPPlus and ClosedXML) +Kevull.MultiHeader.Core contains the interfaces and code agnostic to the excel writter library. +There is a parallel structure under tests folder for unit tests. + +# Additional instructions +- when moving files use git mv to trakc changes in git history +- in public and protected methods and properties, use the tag if the class is inheriteing from a parent; otherwise write the xml documenation +- write code and aswer in English, even if the question is in another language diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 46115cc..8bfd027 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,14 +12,14 @@ on: env: NUGET_FOLDER: ${{ github.workspace}}/nuget - DOTNET_VERSION: 8.0.X + DOTNET_VERSION: 10.0.X CONFIGURATION: Release jobs: build: runs-on: ubuntu-latest env: - SOLUTION_ROOT: src/Kevull.EPPLus.MultiHeader.sln + SOLUTION_ROOT: Kevull.MultiHeader.slnx steps: - uses: actions/checkout@v3 diff --git a/Kevull.MultiHeader.slnx b/Kevull.MultiHeader.slnx new file mode 100644 index 0000000..dfb7208 --- /dev/null +++ b/Kevull.MultiHeader.slnx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/README.md b/README.md index 9137e18..06479db 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ [![build library](https://github.com/mnieto/Kevull.EPPLus.MultiHeader/actions/workflows/build.yml/badge.svg)](https://github.com/mnieto/Kevull.EPPLus.MultiHeader/actions/workflows/build.yml) # Kevull.EPPLus.MultiHeader -Extension for the [EPPlus](https://github.com/EPPlusSoftware/EPPlus) library to create reports from complex objects +Extension to create Excel reports from complex objects with two implementation flavours: +- [EPPlus](https://github.com/EPPlusSoftware/EPPlus) +- [ClosedXML](https://github.com/ClosedXML/ClosedXML) Given a list like this: ```csharp @@ -35,7 +37,7 @@ Given a list like this: }; ``` -this code: +With EPPlus: ```csharp using var xls = new ExcelPackage(); var report = new MultiHeaderReport(xls, "Object"); @@ -43,6 +45,14 @@ this code: xls.SaveAs("Report.xlsx"); ``` +Equivalent code with ClosedXML: +```csharp + using var xls = new XLWorkbook(); + var report = new MultiHeaderReport(xls, "Object"); + report.GenerateReport(complexObject); + report.Save("Report.xlsx"); +``` + will render like this: ![image](.github/example.png) @@ -184,4 +194,5 @@ Default top-left cell is A1. With `SetStartingAddress` you can specify any other - Append rows to an existing report ✓ - Posibility to change the top-left starting point ✓ - Target netstandard 2.0 in nuget package ✓ -- Frozen rows and columns ✓ \ No newline at end of file +- Dual implementation with EPPlus and ClosedXML ✓ +- Frozen rows and columns ✓ diff --git a/src/Kevull.EPPLus.MultiHeader.Test/Usings.cs b/src/Kevull.EPPLus.MultiHeader.Test/Usings.cs deleted file mode 100644 index 13ce7ac..0000000 --- a/src/Kevull.EPPLus.MultiHeader.Test/Usings.cs +++ /dev/null @@ -1,2 +0,0 @@ -global using Xunit; -global using OfficeOpenXml; \ No newline at end of file diff --git a/src/Kevull.EPPLus.MultiHeader.sln b/src/Kevull.EPPLus.MultiHeader.sln deleted file mode 100644 index 387cbdd..0000000 --- a/src/Kevull.EPPLus.MultiHeader.sln +++ /dev/null @@ -1,31 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.6.33815.320 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EPPLus.MultiHeader", "Kevull.EPPLus.MultiHeader\Kevull.EPPLus.MultiHeader.csproj", "{FC9DB053-9511-434B-A767-4355475D4066}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EPPLus.MultiHeader.Test", "Kevull.EPPLus.MultiHeader.Test\Kevull.EPPLus.MultiHeader.Test.csproj", "{6FEA8449-BED4-48A0-91A1-48587FFD7604}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {FC9DB053-9511-434B-A767-4355475D4066}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FC9DB053-9511-434B-A767-4355475D4066}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FC9DB053-9511-434B-A767-4355475D4066}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FC9DB053-9511-434B-A767-4355475D4066}.Release|Any CPU.Build.0 = Release|Any CPU - {6FEA8449-BED4-48A0-91A1-48587FFD7604}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6FEA8449-BED4-48A0-91A1-48587FFD7604}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6FEA8449-BED4-48A0-91A1-48587FFD7604}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6FEA8449-BED4-48A0-91A1-48587FFD7604}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {D72DB64B-E939-4091-B848-AA14E0E41399} - EndGlobalSection -EndGlobal diff --git a/src/Kevull.MultiHeader.ClosedXml/ClosedXmlExcelWriter.cs b/src/Kevull.MultiHeader.ClosedXml/ClosedXmlExcelWriter.cs new file mode 100644 index 0000000..79ab805 --- /dev/null +++ b/src/Kevull.MultiHeader.ClosedXml/ClosedXmlExcelWriter.cs @@ -0,0 +1,325 @@ +using ClosedXML.Excel; +using Kevull.MultiHeader.Core; +using System; +using System.Collections.Generic; +using CoreExcelColor = Kevull.MultiHeader.Core.ExcelColor; + +namespace Kevull.MultiHeader.ClosedXml +{ + /// + /// ClosedXML implementation of + /// + public class ClosedXmlExcelWriter : IExcelWriter + { + private readonly IXLWorksheet _sheet; + private readonly XLWorkbook _workbook; + private readonly Dictionary _namedStyles = new Dictionary(StringComparer.Ordinal); + + /// + /// Creates a new instance of ClosedXmlExcelWriter. + /// + /// The to work with. + /// The to work with. + public ClosedXmlExcelWriter(XLWorkbook workbook, IXLWorksheet sheet) + { + _workbook = workbook ?? throw new ArgumentNullException(nameof(workbook)); + _sheet = sheet ?? throw new ArgumentNullException(nameof(sheet)); + } + + /// + public void WriteCell(int row, int col, object? value) + { + _sheet.Cell(row, col).Value = XLCellValue.FromObject(value); + } + + /// + public void WriteCell(int fromRow, int fromCol, int toRow, int toCol, object? value) + { + var cellValue = XLCellValue.FromObject(value); + var range = _sheet.Range(fromRow, fromCol, toRow, toCol); + range.SetValue(cellValue); + } + + /// + public void WriteCellWithHyperlink(int row, int col, object? value, string url) + { + var cell = _sheet.Cell(row, col); + cell.Value = XLCellValue.FromObject(value); + if (!string.IsNullOrWhiteSpace(url)) + { + cell.SetHyperlink(new XLHyperlink(url)); + } + } + + /// + public void WriteFormula(int row, int col, string formula) + { + _sheet.Cell(row, col).FormulaA1 = formula; + } + + /// + public void WriteFormula(int fromRow, int fromCol, int toRow, int toCol, string formula) + { + _sheet.Range(fromRow, fromCol, toRow, toCol).FormulaA1 = formula; + } + + /// + public void ApplyFormat(int row, int col, CellFormat format) + { + ApplyFormat(row, col, row, col, format); + } + + /// + public void ApplyFormat(int fromRow, int fromCol, int toRow, int toCol, CellFormat format) + { + if (format == null) return; + + var range = _sheet.Range(fromRow, fromCol, toRow, toCol); + ExcelStyleExtensions.ApplyFormatToStyle(range.Style, format); + } + + /// + public void ApplyNativeFormat(int row, int col, Action format) + { + if (format == null) return; + var range = _sheet.Range(row, col, row, col); + format(range); + } + + /// + public void ApplyNativeFormat(int fromRow, int fromCol, int toRow, int toCol, Action format) + { + if (format == null) return; + var range = _sheet.Range(fromRow, fromCol, toRow, toCol); + format(range); + } + + /// + public void CreateNamedStyle(string styleName, CellFormat format) + { + if (string.IsNullOrWhiteSpace(styleName)) + throw new ArgumentNullException(nameof(styleName)); + if (format == null) + throw new ArgumentNullException(nameof(format)); + + if (NamedStyleExists(styleName)) + return; + + _namedStyles[styleName] = format; + } + + /// + public void ApplyNamedStyle(int row, int col, string styleName) + { + ApplyNamedStyle(row, col, row, col, styleName); + } + + /// + public void ApplyNamedStyle(int fromRow, int fromCol, int toRow, int toCol, string styleName) + { + if (string.IsNullOrWhiteSpace(styleName)) + throw new ArgumentNullException(nameof(styleName)); + + if (!_namedStyles.TryGetValue(styleName, out var format)) + throw new KeyNotFoundException($"Style '{styleName}' does not exist."); + + var range = _sheet.Range(fromRow, fromCol, toRow, toCol); + ExcelStyleExtensions.ApplyFormatToStyle(range.Style, format); + } + + /// + public bool NamedStyleExists(string styleName) + { + if (string.IsNullOrWhiteSpace(styleName)) + return false; + + return _namedStyles.ContainsKey(styleName); + } + + /// + public void Merge(int fromRow, int fromCol, int toRow, int toCol) + { + _sheet.Range(fromRow, fromCol, toRow, toCol).Merge(); + } + + /// + public void AutoFitColumn(int col) + { + _sheet.Column(col).AdjustToContents(); + } + + /// + public void AutoFitColumn(int col, double minWidth, double maxWidth) + { + _sheet.Column(col).AdjustToContents(1, _sheet.LastRowUsed()?.RowNumber() ?? 1, minWidth, maxWidth); + } + + /// + public void SetColumnWidth(int col, double width) + { + _sheet.Column(col).Width = width; + } + + /// + public void SetColumnHidden(int col, bool hidden) + { + var column = _sheet.Column(col); + if (hidden) + column.Hide(); + else + column.Unhide(); + } + + /// + public void SetAutoFilter(int fromRow, int fromCol, int toRow, int toCol, bool autoFilter) + { + var range = _sheet.Range(fromRow, fromCol, toRow, toCol); + if (autoFilter) + range.SetAutoFilter(); + else if (_sheet.AutoFilter != null) + _sheet.AutoFilter.Clear(); + } + + /// + public void FreezePanes(int row, int col) + { + _sheet.SheetView.Freeze(row - 1, col - 1); + } + + /// + public void Recalculate() + { + _workbook.RecalculateAllFormulas(); + } + } + + /// + /// Extension methods to apply library-agnostic cell format definitions to ClosedXML styles. + /// + public static class ExcelStyleExtensions + { + /// + /// Applies to an . + /// + public static IXLStyle SetBackground(this CellFormat format, IXLStyle style) + { + ApplyFormatToStyle(style, format); + return style; + } + + /// + /// Applies to an . + /// + internal static void ApplyFormatToStyle(IXLStyle style, CellFormat format) + { + if (style == null || format == null) + return; + + if (format.LeftBorder.HasValue) + style.Border.LeftBorder = ConvertBorderStyle(format.LeftBorder.Value); + if (format.RightBorder.HasValue) + style.Border.RightBorder = ConvertBorderStyle(format.RightBorder.Value); + if (format.TopBorder.HasValue) + style.Border.TopBorder = ConvertBorderStyle(format.TopBorder.Value); + if (format.BottomBorder.HasValue) + style.Border.BottomBorder = ConvertBorderStyle(format.BottomBorder.Value); + + if (format.VerticalAlignment.HasValue) + style.Alignment.Vertical = ConvertVerticalAlignment(format.VerticalAlignment.Value); + if (format.HorizontalAlignment.HasValue) + style.Alignment.Horizontal = ConvertHorizontalAlignment(format.HorizontalAlignment.Value); + + if (format.BackgroundColor.HasValue && format.FillStyle.HasValue) + { + style.Fill.BackgroundColor = ConvertColor(format.BackgroundColor.Value); + style.Fill.PatternType = ConvertFillStyle(format.FillStyle.Value); + } + + if (format.Bold.HasValue) + style.Font.Bold = format.Bold.Value; + if (format.Italic.HasValue) + style.Font.Italic = format.Italic.Value; + if (!string.IsNullOrWhiteSpace(format.FontName)) + style.Font.FontName = format.FontName; + if (format.FontSize.HasValue) + style.Font.FontSize = format.FontSize.Value; + if (format.FontColor.HasValue) + style.Font.FontColor = ConvertColor(format.FontColor.Value); + + if (!string.IsNullOrWhiteSpace(format.NumberFormat)) + style.NumberFormat.Format = format.NumberFormat; + + if (format.TextRotation.HasValue) + style.Alignment.TextRotation = format.TextRotation.Value; + + if (format.WrapText.HasValue) + style.Alignment.WrapText = format.WrapText.Value; + } + + private static XLBorderStyleValues ConvertBorderStyle(Core.BorderStyle borderStyle) + { + return borderStyle switch + { + Core.BorderStyle.None => XLBorderStyleValues.None, + Core.BorderStyle.Thin => XLBorderStyleValues.Thin, + Core.BorderStyle.Medium => XLBorderStyleValues.Medium, + Core.BorderStyle.Thick => XLBorderStyleValues.Thick, + Core.BorderStyle.Double => XLBorderStyleValues.Double, + Core.BorderStyle.Dotted => XLBorderStyleValues.Dotted, + Core.BorderStyle.Dashed => XLBorderStyleValues.Dashed, + Core.BorderStyle.DashDot => XLBorderStyleValues.DashDot, + Core.BorderStyle.DashDotDot => XLBorderStyleValues.DashDotDot, + _ => XLBorderStyleValues.None + }; + } + + private static XLAlignmentVerticalValues ConvertVerticalAlignment(Core.VerticalAlignment alignment) + { + return alignment switch + { + Core.VerticalAlignment.Top => XLAlignmentVerticalValues.Top, + Core.VerticalAlignment.Center => XLAlignmentVerticalValues.Center, + Core.VerticalAlignment.Bottom => XLAlignmentVerticalValues.Bottom, + Core.VerticalAlignment.Justify => XLAlignmentVerticalValues.Justify, + Core.VerticalAlignment.Distributed => XLAlignmentVerticalValues.Distributed, + _ => XLAlignmentVerticalValues.Bottom + }; + } + + private static XLAlignmentHorizontalValues ConvertHorizontalAlignment(Core.HorizontalAlignment alignment) + { + return alignment switch + { + Core.HorizontalAlignment.General => XLAlignmentHorizontalValues.General, + Core.HorizontalAlignment.Left => XLAlignmentHorizontalValues.Left, + Core.HorizontalAlignment.Center => XLAlignmentHorizontalValues.Center, + Core.HorizontalAlignment.Right => XLAlignmentHorizontalValues.Right, + Core.HorizontalAlignment.Fill => XLAlignmentHorizontalValues.Fill, + Core.HorizontalAlignment.Justify => XLAlignmentHorizontalValues.Justify, + Core.HorizontalAlignment.CenterContinuous => XLAlignmentHorizontalValues.CenterContinuous, + Core.HorizontalAlignment.Distributed => XLAlignmentHorizontalValues.Distributed, + _ => XLAlignmentHorizontalValues.General + }; + } + + private static XLFillPatternValues ConvertFillStyle(Core.FillStyle fillStyle) + { + return fillStyle switch + { + Core.FillStyle.None => XLFillPatternValues.None, + Core.FillStyle.Solid => XLFillPatternValues.Solid, + Core.FillStyle.DarkGray => XLFillPatternValues.DarkGray, + Core.FillStyle.MediumGray => XLFillPatternValues.MediumGray, + Core.FillStyle.LightGray => XLFillPatternValues.LightGray, + Core.FillStyle.Gray125 => XLFillPatternValues.Gray125, + Core.FillStyle.Gray0625 => XLFillPatternValues.Gray0625, + _ => XLFillPatternValues.None + }; + } + + private static XLColor ConvertColor(CoreExcelColor excelColor) + { + return XLColor.FromArgb(excelColor.A, excelColor.R, excelColor.G, excelColor.B); + } + } +} diff --git a/src/Kevull.MultiHeader.ClosedXml/ConfigurationBuilder.cs b/src/Kevull.MultiHeader.ClosedXml/ConfigurationBuilder.cs new file mode 100644 index 0000000..37ad740 --- /dev/null +++ b/src/Kevull.MultiHeader.ClosedXml/ConfigurationBuilder.cs @@ -0,0 +1,227 @@ +using ClosedXML.Excel; +using DocumentFormat.OpenXml.Wordprocessing; +using Kevull.MultiHeader.Core; +using Kevull.MultiHeader.Core.Columns; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Text.RegularExpressions; + +namespace Kevull.MultiHeader.ClosedXml +{ + /// + /// Helper class to configure the report and column options. + /// + /// + public class ConfigurationBuilder : IConfigurationBuilder + { + private readonly List columns; + private readonly XLWorkbook xls; + private CellAddress StartingAddress { get; set; } = new CellAddress(1, 1); + + /// + /// Collection of named styles available for column and header formatting. + /// + public Dictionary NamedStyles = new Dictionary(); + + /// + public bool AutoFilter { get; set; } = true; + + /// + public bool AppendToExistingReport { get; set; } + + /// + public bool AutoFreezePanes { get; set; } = true; + + /// + public int TopRow => StartingAddress.Row; + + /// + public int LeftColumn => StartingAddress.Column; + + /// + /// Ctor invoked to get default configuration at first step. + /// + /// Excel reference. + public ConfigurationBuilder(XLWorkbook xls) : this(xls, new List()) { } + + /// + /// Ctor intended for testing purposes. + /// + /// Excel reference. + /// List of column configurations. + internal ConfigurationBuilder(XLWorkbook xls, params ColumnInfo[] columns) : this(xls, columns.ToList()) { } + + /// + /// Ctor. + /// + /// Excel reference. + /// List of column configurations. + public ConfigurationBuilder(XLWorkbook xls, IEnumerable columns) + { + this.xls = xls; + this.columns = columns.ToList(); + } + + /// + public IConfigurationBuilder AddColumn(Expression> columnSelector) + { + columns.Add(new ColumnInfo(columnSelector)); + return this; + } + + /// + public IConfigurationBuilder AddColumn(Expression> columnSelector, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null) + { + columns.Add(new ColumnInfo(columnSelector, order, displayName, hidden, styleName)); + return this; + } + + /// + public IConfigurationBuilder AddColumn(Expression> columnSelector, Action cfg) + { + columns.Add(new ColumnInfo(columnSelector, cfg)); + return this; + } + + /// + public IConfigurationBuilder AddEnumeration(Expression> columnSelector, IEnumerable keyValues, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null) + { + columns.Add(new ColumnEnumeration(columnSelector, keyValues, order, displayName, hidden, styleName)); + return this; + } + + /// + public IConfigurationBuilder AddEnumeration(Expression> columnSelector, IEnumerable keyValues, Action cfg) + { + columns.Add(new ColumnEnumeration(columnSelector, keyValues, cfg)); + return this; + } + + /// + public IConfigurationBuilder AddExpression(string name, Func expression, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null) + { + columns.Add(new ColumnExpression(name, expression, order, displayName, hidden, styleName)); + return this; + } + + /// + public IConfigurationBuilder AddExpression(string name, Func expression, Action cfg) + { + columns.Add(new ColumnExpression(name, expression, cfg)); + return this; + } + + /// + public IConfigurationBuilder AddFormula(string name, string formula, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null) + { + columns.Add(new ColumnFormula(name, formula, order, displayName, hidden, styleName)); + return this; + } + + /// + public IConfigurationBuilder AddFormula(string name, string formula, Action cfg) + { + columns.Add(new ColumnFormula(name, formula, cfg)); + return this; + } + + /// + public IConfigurationBuilder AddHyperLinkColumn(Expression> columnSelector, Expression> urlColumnSelector, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null) + { + columns.Add(new ColumnHyperLink(columnSelector, urlColumnSelector, order, displayName, hidden, styleName)); + return this; + } + + /// + public IConfigurationBuilder AddHyperLinkColumn(Expression> columnSelector, Expression> urlColumnSelector, Action cfg) + { + columns.Add(new ColumnHyperLink(columnSelector, urlColumnSelector, cfg)); + return this; + } + + /// + public IConfigurationBuilder IgnoreColumn(Expression> columnSelector) + { + columns.Add(new ColumnInfo(columnSelector, true)); + return this; + } + + /// + public IConfigurationBuilder AddHeaderStyle(Action style) + { + return AddNamedStyle(StyleNames.HeaderStyleName, style); + } + + /// + public IConfigurationBuilder AddNamedStyle(string name, Action style) + { + NamedStyles.Add(name, new CellFormat()); + style?.Invoke(NamedStyles[name]); + return this; + } + + /// + public IConfigurationBuilder SetStartingAddress(string address) + { + StartingAddress = ParseAddress(address); + return this; + } + + /// + public IConfigurationBuilder SetStartingAddres(int row, int column) + { + StartingAddress = new CellAddress(row, column); + return this; + } + + /// + public HeaderManager Build() + { + var headerManager = new HeaderManager(columns); + headerManager.AutoFilter = AutoFilter; + headerManager.AutoFreezePanes = AutoFreezePanes; + headerManager.FirstRow = StartingAddress.Row; + headerManager.FirstColumn = StartingAddress.Column; + headerManager.AppendToExistingReport = AppendToExistingReport; + return headerManager; + } + + private static CellAddress ParseAddress(string address) + { + if (string.IsNullOrWhiteSpace(address)) + throw new ArgumentNullException(nameof(address)); + + var match = Regex.Match(address.Trim(), "^(?[A-Za-z]+)(?[1-9][0-9]*)$"); + if (!match.Success) + throw new FormatException($"Invalid cell address: {address}"); + + var colLetters = match.Groups["col"].Value.ToUpperInvariant(); + var row = int.Parse(match.Groups["row"].Value, CultureInfo.InvariantCulture); + + int col = 0; + foreach (var ch in colLetters) + { + col = (col * 26) + (ch - 'A' + 1); + } + + return new CellAddress(row, col); + } + + private sealed class CellAddress + { + public int Row { get; } + public int Column { get; } + + public CellAddress(int row, int column) + { + if (row <= 0) throw new ArgumentOutOfRangeException(nameof(row)); + if (column <= 0) throw new ArgumentOutOfRangeException(nameof(column)); + Row = row; + Column = column; + } + } + } +} diff --git a/src/Kevull.MultiHeader.ClosedXml/Kevull.MultiHeader.ClosedXml.csproj b/src/Kevull.MultiHeader.ClosedXml/Kevull.MultiHeader.ClosedXml.csproj new file mode 100644 index 0000000..2f9432b --- /dev/null +++ b/src/Kevull.MultiHeader.ClosedXml/Kevull.MultiHeader.ClosedXml.csproj @@ -0,0 +1,58 @@ + + + + net10.0;netstandard2.0 + 14.0 + enable + enable + True + + true + all + + + + true + MNieto + EPlus multi-header report + Extension for the ClosedXML library to create reports from complex objects + https://github.com/mnieto/Kevull.MultiHeader + https://github.com/mnieto/Kevull.MultiHeader + git + ClosedXML;Excel;Multi-header;Report + LGPL-3.0-or-later + README.md + EPlusMultiHeader.png + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + <_Parameter1>Kevull.MultiHeader.ClosedXml.Tests + + + + + True + \ + + + True + \ + + + diff --git a/src/Kevull.MultiHeader.ClosedXml/MultiHeaderReport.cs b/src/Kevull.MultiHeader.ClosedXml/MultiHeaderReport.cs new file mode 100644 index 0000000..8936e06 --- /dev/null +++ b/src/Kevull.MultiHeader.ClosedXml/MultiHeaderReport.cs @@ -0,0 +1,282 @@ +using ClosedXML.Excel; +using Kevull.MultiHeader.Core; +using Kevull.MultiHeader.Core.Columns; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Kevull.MultiHeader.ClosedXml +{ + /// + /// Given an list of objects it creates an in-memory Excel report. + /// + /// Type of objects. + public class MultiHeaderReport : IMultiHeaderReport + { + private readonly IXLWorksheet _sheet; + private readonly XLWorkbook _xls; + private readonly IExcelWriter _writer; + + private int FirstDataRow => (_header == null || !_header.AppendToExistingReport) + ? _header?.FirstRow + _header?.Height ?? 2 + : (_sheet.LastRowUsed()?.RowNumber() ?? 0) + 1; + + private int row; + + /// + /// Internal . + /// + protected HeaderManager? _header; + + /// + /// Custom styles defined by the user to be applied to headers or columns. + /// + protected Dictionary _namedStyles = []; + + /// + /// Object properties associated to the columns. + /// + protected Dictionary? Properties { get; private set; } + + /// + /// Ctor. + /// + /// Initialized . + /// Existing worksheet where to generate the report. + public MultiHeaderReport(XLWorkbook xls, IXLWorksheet sheet) + { + _xls = xls ?? throw new ArgumentNullException(nameof(xls)); + _sheet = sheet ?? throw new ArgumentNullException(nameof(sheet)); + _writer = new ClosedXmlExcelWriter(xls, sheet); + } + + /// + /// Ctor. + /// + /// Initialized . + /// Worksheet name to be created where generate the report. + public MultiHeaderReport(XLWorkbook xls, string sheetName) : this(xls, AddSheet(xls, sheetName)) { } + + /// + public IMultiHeaderReport Configure(Action> options) + { + var builder = new ConfigurationBuilder(_xls); + options?.Invoke(builder); + _header = builder.Build(); + _namedStyles = builder.NamedStyles; + return this; + } + + /// + public void GenerateReport(IEnumerable data) + { + if (_header == null) + { + _header = new HeaderManager(); + } + else + { + _header.BuildHeaders(); + } + + Properties = _header.Properties; + if (!_header.AppendToExistingReport) + WriteHeaders(); + + row = FirstDataRow; + foreach (var item in data) + { + ProcessRow(item); + } + + DoFormatting(); + CalulateFormulas(); + } + + /// + public void Save(string fileName) + { + _xls.SaveAs(fileName); + } + + private static IXLWorksheet AddSheet(XLWorkbook xls, string sheetName) + { + if (!xls.Worksheets.Any(x => x.Name == sheetName)) + { + xls.AddWorksheet(sheetName); + } + return xls.Worksheet(sheetName); + } + + private void ProcessRow(T item) + { + foreach (var columnInfo in _header!.Columns) + { + if (columnInfo.HasChildren) + { + ProcessRow(columnInfo.Header!, Properties![columnInfo.Name].GetValue(item)); + } + else + { + columnInfo.WriteCell(_writer, row, columnInfo.Index, Properties!, item!); + } + } + row++; + } + + private void ProcessRow(HeaderManager header, object? item) + { + if (item == null) + return; + if (header.Properties == null) + throw new ArgumentNullException(nameof(header.Properties)); + + foreach (var columnInfo in header.Columns) + { + if (columnInfo.HasChildren) + { + ProcessRow(columnInfo.Header!, header.Properties[columnInfo.Name].GetValue(item)); + } + else + { + columnInfo.WriteCell(_writer, row, columnInfo.Index, header.Properties, item); + } + } + } + + private void WriteHeaders(HeaderManager? header = null, int? topRow = null) + { + header ??= _header!; + int localRow = topRow ?? _header!.FirstRow; + foreach (var columnInfo in header.Columns) + { + columnInfo.WriteHeader(_writer, localRow, columnInfo.Index); + columnInfo.FormatHeader(_writer, localRow, columnInfo.Index, columnInfo.HasChildren ? 1 : header.Height - (localRow - _header!.FirstRow)); + if (columnInfo.HasChildren) + { + WriteHeaders(columnInfo.Header!, localRow + 1); + } + } + } + + private void DoFormatting() + { + if (_header!.AutoFreezePanes) + _writer.FreezePanes(_header.FirstRow + _header.Height, _header.FirstColumn); + + foreach (var columnInfo in _header.Columns.Where(x => x.Hidden || x.ColumnWidth.Type == WidthType.Hidden)) + { + _writer.SetColumnHidden(columnInfo.Index, true); + } + + int lastHeaderRow = _header.FirstRow + _header.Height - 1; + int lastHeaderColumn = _header.FirstColumn + _header.Width - 1; + _writer.SetAutoFilter(lastHeaderRow, _header.Columns.Min(x => x.Index), lastHeaderRow, lastHeaderColumn, _header.AutoFilter); + + foreach (var columnInfo in _header.Columns.Where(x => x.ColumnWidth.Type == WidthType.Auto)) + { + double minWidth = columnInfo.ColumnWidth.MinimumWidth == double.MinValue ? _sheet.ColumnWidth : columnInfo.ColumnWidth.MinimumWidth; + double maxWidth = columnInfo.ColumnWidth.MaximunWidth; + _writer.AutoFitColumn(columnInfo.Index, minWidth, maxWidth); + } + foreach (var columnInfo in _header.Columns.Where(x => x.ColumnWidth.Type == WidthType.Custom)) + { + _writer.SetColumnWidth(columnInfo.Index, columnInfo.ColumnWidth.Width!.Value); + } + + BuildDateStyle(); + BuildTimeStyle(); + BuildColumnStyles(); + + if (!_header.AppendToExistingReport) + { + BuildDefaultHeaderStyle(); + _writer.ApplyNamedStyle(_header.FirstRow, _header.Columns.Min(x => x.Index), lastHeaderRow, lastHeaderColumn, StyleNames.HeaderStyleName); + } + + int lastDataRow = _sheet.LastRowUsed()?.RowNumber() ?? (FirstDataRow - 1); + if (lastDataRow >= FirstDataRow) + { + foreach (var columnInfo in _header.Columns.Where(x => x.StyleName != null)) + { + _writer.ApplyNamedStyle(FirstDataRow, columnInfo.Index, lastDataRow, columnInfo.Index, columnInfo.StyleName!); + } + } + } + + private void CalulateFormulas() + { + bool needsCalculate = false; + foreach (var columnInfo in _header!.Columns.OfType()) + { + int lastRow = _sheet.LastRowUsed()?.RowNumber() ?? FirstDataRow; + if (lastRow >= FirstDataRow) + { + columnInfo.WriteCell(_writer, FirstDataRow, columnInfo.Index, lastRow, columnInfo.Index, Properties!, null); + needsCalculate = true; + } + } + if (needsCalculate) + _writer.Recalculate(); + } + + private void BuildDefaultHeaderStyle() + { + if (!_writer.NamedStyleExists(StyleNames.HeaderStyleName)) + { + var format = new CellFormat + { + LeftBorder = BorderStyle.Thin, + RightBorder = BorderStyle.Thin, + TopBorder = BorderStyle.Thin, + BottomBorder = BorderStyle.Thin, + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Center, + BackgroundColor = ExcelColor.LightGray, + FillStyle = FillStyle.Solid, + Bold = true + }; + + if (_namedStyles.ContainsKey(StyleNames.HeaderStyleName)) + format.Merge(_namedStyles[StyleNames.HeaderStyleName]); + _writer.CreateNamedStyle(StyleNames.HeaderStyleName, format); + } + } + + private void BuildDateStyle() + { + if (!_writer.NamedStyleExists(StyleNames.DateStyleName)) + { + var format = new CellFormat + { + NumberFormat = StyleNames.DateFormat + }; + _writer.CreateNamedStyle(StyleNames.DateStyleName, format); + } + } + + private void BuildTimeStyle() + { + if (!_writer.NamedStyleExists(StyleNames.TimeStyleName)) + { + var format = new CellFormat + { + NumberFormat = StyleNames.TimeFormat + }; + _writer.CreateNamedStyle(StyleNames.TimeStyleName, format); + } + } + + private void BuildColumnStyles() + { + foreach (var style in _namedStyles.Keys) + { + if (style != StyleNames.HeaderStyleName && !_writer.NamedStyleExists(style)) + { + _writer.CreateNamedStyle(style, _namedStyles[style]); + } + } + } + } +} diff --git a/src/Kevull.MultiHeader.Core/CellFormat.cs b/src/Kevull.MultiHeader.Core/CellFormat.cs new file mode 100644 index 0000000..a56f2f5 --- /dev/null +++ b/src/Kevull.MultiHeader.Core/CellFormat.cs @@ -0,0 +1,119 @@ +namespace Kevull.MultiHeader.Core +{ + /// + /// Represents the formatting properties for an Excel cell + /// + public class CellFormat + { + /// + /// Border style for the left edge + /// + public BorderStyle? LeftBorder { get; set; } + + /// + /// Border style for the right edge + /// + public BorderStyle? RightBorder { get; set; } + + /// + /// Border style for the top edge + /// + public BorderStyle? TopBorder { get; set; } + + /// + /// Border style for the bottom edge + /// + public BorderStyle? BottomBorder { get; set; } + + /// + /// Vertical alignment of cell content + /// + public VerticalAlignment? VerticalAlignment { get; set; } + + /// + /// Horizontal alignment of cell content + /// + public HorizontalAlignment? HorizontalAlignment { get; set; } + + /// + /// Background color of the cell + /// + public ExcelColor? BackgroundColor { get; set; } + + /// + /// Fill style pattern + /// + public FillStyle? FillStyle { get; set; } + + /// + /// Whether the font should be bold + /// + public bool? Bold { get; set; } + + /// + /// Whether the font should be italic + /// + public bool? Italic { get; set; } + + /// + /// Font family name + /// + public string? FontName { get; set; } + + /// + /// Font size in points + /// + public float? FontSize { get; set; } + + /// + /// Font color + /// + public ExcelColor? FontColor { get; set; } + + /// + /// Number format string (e.g., "mm-dd-yy", "0.00") + /// + public string? NumberFormat { get; set; } + + /// + /// Text rotation in degrees + /// + public int? TextRotation { get; set; } + + /// + /// Whether text should wrap in the cell + /// + public bool? WrapText { get; set; } + + /// + /// Merges the current format with another format. + /// Any non-null value in overwrites the current value. + /// + /// Format to merge into the current instance. + /// Thrown when is null. + public void Merge(CellFormat other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + + LeftBorder = other.LeftBorder ?? LeftBorder; + RightBorder = other.RightBorder ?? RightBorder; + TopBorder = other.TopBorder ?? TopBorder; + BottomBorder = other.BottomBorder ?? BottomBorder; + VerticalAlignment = other.VerticalAlignment ?? VerticalAlignment; + HorizontalAlignment = other.HorizontalAlignment ?? HorizontalAlignment; + BackgroundColor = other.BackgroundColor ?? BackgroundColor; + FillStyle = other.FillStyle ?? FillStyle; + Bold = other.Bold ?? Bold; + Italic = other.Italic ?? Italic; + FontName = other.FontName ?? FontName; + FontSize = other.FontSize ?? FontSize; + FontColor = other.FontColor ?? FontColor; + NumberFormat = other.NumberFormat ?? NumberFormat; + TextRotation = other.TextRotation ?? TextRotation; + WrapText = other.WrapText ?? WrapText; + } + } +} diff --git a/src/Kevull.EPPLus.MultiHeader/ColumnWidth.cs b/src/Kevull.MultiHeader.Core/ColumnWidth.cs similarity index 98% rename from src/Kevull.EPPLus.MultiHeader/ColumnWidth.cs rename to src/Kevull.MultiHeader.Core/ColumnWidth.cs index 2ce438e..b00ae57 100644 --- a/src/Kevull.EPPLus.MultiHeader/ColumnWidth.cs +++ b/src/Kevull.MultiHeader.Core/ColumnWidth.cs @@ -1,4 +1,4 @@ -namespace Kevull.EPPLus.MultiHeader +namespace Kevull.MultiHeader.Core { /// /// Allows to configure the colum with diff --git a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnDef.cs b/src/Kevull.MultiHeader.Core/Columns/ColumnDef.cs similarity index 81% rename from src/Kevull.EPPLus.MultiHeader/Columns/ColumnDef.cs rename to src/Kevull.MultiHeader.Core/Columns/ColumnDef.cs index a5269f9..aaf6722 100644 --- a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnDef.cs +++ b/src/Kevull.MultiHeader.Core/Columns/ColumnDef.cs @@ -1,11 +1,10 @@ -using OfficeOpenXml.Style; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Columns +namespace Kevull.MultiHeader.Core.Columns { /// /// Column common properties @@ -45,9 +44,9 @@ public int? Order /// Name of a style defined in the Excel workbook /// /// - /// Style names are not checked at configuration time, but in the method + /// Style names are not checked at configuration time, but in the method /// You can assign the style name during the column creation or use any existing Style in the Excel file. - /// The is a handy method that wraps the EPPlus method + /// The method allows you to define reusable named styles. /// public string? StyleName { get; set; } diff --git a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnEnumeration.cs b/src/Kevull.MultiHeader.Core/Columns/ColumnEnumeration.cs similarity index 80% rename from src/Kevull.EPPLus.MultiHeader/Columns/ColumnEnumeration.cs rename to src/Kevull.MultiHeader.Core/Columns/ColumnEnumeration.cs index 48b3c28..7002bd4 100644 --- a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnEnumeration.cs +++ b/src/Kevull.MultiHeader.Core/Columns/ColumnEnumeration.cs @@ -1,4 +1,4 @@ -using OfficeOpenXml; +using Kevull.MultiHeader.Core; using System; using System.Collections; using System.Collections.Generic; @@ -8,7 +8,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Columns +namespace Kevull.MultiHeader.Core.Columns { /// /// Specialized that renders data from a or . @@ -26,7 +26,7 @@ public class ColumnEnumeration : ColumnInfo /// Is it a property with a single value or is it a or . /// internal override bool IsMultiValue => true; - + /// /// Gets the allowed values for the child columns /// @@ -74,18 +74,22 @@ internal ColumnEnumeration(string name, IEnumerable keyValues, int? orde _keyValues = AddKeyValues(keyValues); } - internal override void FormatHeader(ExcelRange cell, int height) + /// + public override void FormatHeader(IExcelWriter writer, int row, int col, int height) { - cell.Offset(0, 0, 1, Width).Merge = true; - var enumerator = _keyValues.GetEnumerator(); - while (enumerator.MoveNext()) + // Merge the parent header across all columns + writer.Merge(row, col, row, col + Width - 1); + + // Merge each child column header vertically + foreach (var kvp in _keyValues) { - int offset = _keyValues[enumerator.Current.Key]; - cell.Offset(1, offset, height - 1, 1).Merge = true; + int offset = kvp.Value; + writer.Merge(row + 1, col + offset, row + height - 1, col + offset); } } - internal override void WriteCell(ExcelRange cell, Dictionary properties, object? obj) + /// + public override void WriteCell(IExcelWriter writer, int row, int col, Dictionary properties, object? obj) { if (obj == null) return; @@ -98,7 +102,7 @@ internal override void WriteCell(ExcelRange cell, Dictionary + public override void WriteHeader(IExcelWriter writer, int row, int col) { - cell.Value = DisplayName; - var enumerator = _keyValues.GetEnumerator(); - while (enumerator.MoveNext()) + // Write parent header + writer.WriteCell(row, col, DisplayName); + + // Write child headers + foreach (var kvp in _keyValues) { - string key = enumerator.Current.Key; - int offset = _keyValues[key]; - cell.Offset(1, offset).Value = key; + string key = kvp.Key; + int offset = kvp.Value; + writer.WriteCell(row + 1, col + offset, key); } } diff --git a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnExpression.cs b/src/Kevull.MultiHeader.Core/Columns/ColumnExpression.cs similarity index 88% rename from src/Kevull.EPPLus.MultiHeader/Columns/ColumnExpression.cs rename to src/Kevull.MultiHeader.Core/Columns/ColumnExpression.cs index 1b2cd69..e95517d 100644 --- a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnExpression.cs +++ b/src/Kevull.MultiHeader.Core/Columns/ColumnExpression.cs @@ -1,4 +1,4 @@ -using OfficeOpenXml; +using Kevull.MultiHeader.Core; using System; using System.Collections.Generic; using System.Linq; @@ -6,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Columns +namespace Kevull.MultiHeader.Core.Columns { /// /// Add an expression column. That is, each time the report will render a value for this column, it will invoke a lambda expression. @@ -51,17 +51,18 @@ public ColumnExpression(string name, Func expression, int? order = n /// name of the property. In this case, it cannot be infered from the source Type /// Lambda expression to be evaluated to render the column value each row /// Action that will be invoked to configure the ColumnInfo properties using a object - internal ColumnExpression(string name, Func expression, Action cfg) : base(name, cfg) + public ColumnExpression(string name, Func expression, Action cfg) : base(name, cfg) { _expression = expression ?? throw new ArgumentNullException(nameof(expression)); } - internal override void WriteCell(ExcelRange cell, Dictionary properties, object? obj) + /// + public override void WriteCell(IExcelWriter writer, int row, int col, Dictionary properties, object? obj) { if (obj is null) return; - cell.Value = _expression((T)obj); + writer.WriteCell(row, col, _expression((T)obj)); } } diff --git a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnFormula.cs b/src/Kevull.MultiHeader.Core/Columns/ColumnFormula.cs similarity index 79% rename from src/Kevull.EPPLus.MultiHeader/Columns/ColumnFormula.cs rename to src/Kevull.MultiHeader.Core/Columns/ColumnFormula.cs index 02fc540..158188a 100644 --- a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnFormula.cs +++ b/src/Kevull.MultiHeader.Core/Columns/ColumnFormula.cs @@ -1,4 +1,4 @@ -using OfficeOpenXml; +using Kevull.MultiHeader.Core; using System; using System.Collections.Generic; using System.Linq; @@ -6,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Columns +namespace Kevull.MultiHeader.Core.Columns { /// /// Add a formula column. That is, each time the report will render a value for this column, it will use the specified Excel formula @@ -57,9 +57,18 @@ public ColumnFormula(string name, string formula, Action cfg) _formula = formula; } - internal override void WriteCell(ExcelRange cell, Dictionary properties, object? obj) + /// + public override void WriteCell(IExcelWriter writer, int row, int col, Dictionary properties, object? obj) { - cell.Formula = _formula; + writer.WriteFormula(row, col, _formula); + } + + /// + public override void WriteCell(IExcelWriter writer, int fromRow, int fromCol, int toRow, int toCol, Dictionary properties, object? obj) + { + // Optimize: write formula to entire range at once + // Excel will automatically adjust relative references + writer.WriteFormula(fromRow, fromCol, toRow, toCol, _formula); } } diff --git a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnHyperLink.cs b/src/Kevull.MultiHeader.Core/Columns/ColumnHyperLink.cs similarity index 80% rename from src/Kevull.EPPLus.MultiHeader/Columns/ColumnHyperLink.cs rename to src/Kevull.MultiHeader.Core/Columns/ColumnHyperLink.cs index 8950a72..01acf87 100644 --- a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnHyperLink.cs +++ b/src/Kevull.MultiHeader.Core/Columns/ColumnHyperLink.cs @@ -1,4 +1,4 @@ -using OfficeOpenXml; +using Kevull.MultiHeader.Core; using System; using System.Collections.Generic; using System.Linq; @@ -7,7 +7,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Columns +namespace Kevull.MultiHeader.Core.Columns { /// /// Add a column with hyperlink. That is, the Excel column is associated to 2 fields: the url and the display content @@ -51,24 +51,33 @@ public ColumnHyperLink(Expression> columnSelector, Expression properties, object? obj) + /// + public override void WriteCell(IExcelWriter writer, int row, int col, Dictionary properties, object? obj) { if (obj == null) return; - cell.Value = properties[Name].GetValue(obj); - object? url = properties[UrlPropertyName].GetValue(obj); + + var value = properties[Name].GetValue(obj); + var url = properties[UrlPropertyName].GetValue(obj); + if (url != null) { try { - cell.Hyperlink = new Uri(url.ToString()!); - } + writer.WriteCellWithHyperlink(row, col, value ?? "", url.ToString()!); + } catch (Exception) { if (!IgnoreLinkErrors) throw; + // If ignoring errors, just write the value without hyperlink + writer.WriteCell(row, col, value); } } + else + { + writer.WriteCell(row, col, value); + } } } } diff --git a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnInfo.cs b/src/Kevull.MultiHeader.Core/Columns/ColumnInfo.cs similarity index 74% rename from src/Kevull.EPPLus.MultiHeader/Columns/ColumnInfo.cs rename to src/Kevull.MultiHeader.Core/Columns/ColumnInfo.cs index 46ce167..6ca631c 100644 --- a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnInfo.cs +++ b/src/Kevull.MultiHeader.Core/Columns/ColumnInfo.cs @@ -1,5 +1,4 @@ -using OfficeOpenXml; -using OfficeOpenXml.FormulaParsing; +using Kevull.MultiHeader.Core; using System; using System.Collections.Generic; using System.Diagnostics; @@ -9,7 +8,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Columns +namespace Kevull.MultiHeader.Core.Columns { /// /// Base class for columns @@ -65,7 +64,7 @@ public class ColumnInfo /// /// Excel column index where render the data. Do not confuse with . Intended for internal use purposes. /// - internal int Index { get; set; } + public int Index { get; set; } /// /// Diplay order. Order is relative to the other columns. Columns that has no order are added after those that have it. Order starts from 1 @@ -95,7 +94,7 @@ public int? Order /// Number of child levels below this /// internal int Deep => FullName.Split('.').Length; - + /// /// Is it a property with a single value or is it a or . /// @@ -104,12 +103,12 @@ public int? Order /// /// If this column's Type is a complex object, this property will store the child headers /// - internal HeaderManager? Header { get; set; } + public HeaderManager? Header { get; set; } /// /// Has child columns. That is, is it a complex object? /// - internal bool HasChildren => Header != null && Header.Columns.Count > 0; + public bool HasChildren => Header != null && Header.Columns.Count > 0; /// /// Number of Excel columns needed to render this property (and all its children) @@ -120,10 +119,9 @@ public int? Order /// Name of a style defined in the Excel workbook /// /// - /// Style names are not checked at configuration time, but in the method + /// Style names are not checked at configuration time, but in the method. /// You can assign the style name during the column creation or use any existing Style in the Excel file. - /// The is a handy method - /// that wraps the EPPlus method + /// The method can be used to define reusable styles. /// public string? StyleName { get; set; } @@ -205,20 +203,66 @@ internal ColumnInfo(PropertyNames names, int? order = null, string? displayName _displayName = displayName; } - internal virtual void FormatHeader(ExcelRange cell, int height) + /// + /// Applies header formatting for this column. + /// + /// The writer used to apply Excel operations. + /// The starting row index of the header cell. + /// The starting column index of the header cell. + /// The number of rows spanned by the header. + public virtual void FormatHeader(IExcelWriter writer, int row, int col, int height) { - cell.Offset(0, 0, height, Width).Merge = true; + writer.Merge(row, col, row + height - 1, col + Width - 1); } - internal virtual void WriteCell(ExcelRange cell, Dictionary properties, object? obj) + /// + /// Writes the column value for a single cell. + /// + /// The writer used to output cell values. + /// The destination row index. + /// The destination column index. + /// The property map used to resolve values from the source object. + /// The source object containing values for the current row. + public virtual void WriteCell(IExcelWriter writer, int row, int col, Dictionary properties, object? obj) { if (obj != null) - cell.Value = properties[Name].GetValue(obj); + writer.WriteCell(row, col, properties[Name].GetValue(obj)); } - internal virtual void WriteHeader(ExcelRange cell) + /// + /// Writes the column value for a potentially merged cell range. + /// + /// The writer used to output cell values. + /// The starting row index of the range. + /// The starting column index of the range. + /// The ending row index of the range. + /// The ending column index of the range. + /// The property map used to resolve values from the source object. + /// The source object containing values for the current row. + public virtual void WriteCell(IExcelWriter writer, int fromRow, int fromCol, int toRow, int toCol, Dictionary properties, object? obj) + { + // Default implementation: merge cells and write value to merged range + // This is appropriate for headers or properties spanning multiple columns + // Can be overridden for specific behaviors (e.g., formulas copy to each cell) + if (fromRow != toRow || fromCol != toCol) + { + writer.Merge(fromRow, fromCol, toRow, toCol); + } + + // Write value to the merged cell (EPPlus writes to first cell of merged range) + if (obj != null) + writer.WriteCell(fromRow, fromCol, properties[Name].GetValue(obj)); + } + + /// + /// Writes the header text for this column. + /// + /// The writer used to output cell values. + /// The destination row index. + /// The destination column index. + public virtual void WriteHeader(IExcelWriter writer, int row, int col) { - cell.Value = DisplayName; + writer.WriteCell(row, col, DisplayName); } private string GetName(string fullName) @@ -270,7 +314,7 @@ public ColumnInfo(Expression> columnSelector, Action /// /// name for this column /// Action that will be invoked to configure the ColumnInfo properties using a object - public ColumnInfo(string name, Action cfg) : base(name, cfg) { } + internal ColumnInfo(string name, Action cfg) : base(name, cfg) { } /// /// Ctor. Used internally in nested properties and for testing purposes. Use diff --git a/src/Kevull.EPPLus.MultiHeader/Columns/PropertyNameBuilder.cs b/src/Kevull.MultiHeader.Core/Columns/PropertyNameBuilder.cs similarity index 97% rename from src/Kevull.EPPLus.MultiHeader/Columns/PropertyNameBuilder.cs rename to src/Kevull.MultiHeader.Core/Columns/PropertyNameBuilder.cs index 14b05a5..c2c5b92 100644 --- a/src/Kevull.EPPLus.MultiHeader/Columns/PropertyNameBuilder.cs +++ b/src/Kevull.MultiHeader.Core/Columns/PropertyNameBuilder.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Columns +namespace Kevull.MultiHeader.Core.Columns { internal class PropertyNameBuilder { diff --git a/src/Kevull.EPPLus.MultiHeader/Columns/PropertyNames.cs b/src/Kevull.MultiHeader.Core/Columns/PropertyNames.cs similarity index 89% rename from src/Kevull.EPPLus.MultiHeader/Columns/PropertyNames.cs rename to src/Kevull.MultiHeader.Core/Columns/PropertyNames.cs index 71fd022..48fa779 100644 --- a/src/Kevull.EPPLus.MultiHeader/Columns/PropertyNames.cs +++ b/src/Kevull.MultiHeader.Core/Columns/PropertyNames.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Columns +namespace Kevull.MultiHeader.Core.Columns { internal class PropertyNames { diff --git a/src/Kevull.MultiHeader.Core/ExcelFormats.cs b/src/Kevull.MultiHeader.Core/ExcelFormats.cs new file mode 100644 index 0000000..4fe7a35 --- /dev/null +++ b/src/Kevull.MultiHeader.Core/ExcelFormats.cs @@ -0,0 +1,317 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Kevull.MultiHeader.Core +{ + /// + /// Represents a color using ARGB values, library-agnostic + /// + public struct ExcelColor + { + /// + /// Alpha component (transparency) of the color (0-255) + /// + public byte A { get; set; } + + /// + /// Red component of the color (0-255) + /// + public byte R { get; set; } + + /// + /// Green component of the color (0-255) + /// + public byte G { get; set; } + + /// + /// Blue component of the color (0-255) + /// + public byte B { get; set; } + + /// + /// Creates a new ExcelColor with full opacity (alpha = 255) + /// + /// Red component (0-255) + /// Green component (0-255) + /// Blue component (0-255) + public ExcelColor(byte r, byte g, byte b) : this(255, r, g, b) { } + + /// + /// Creates a new ExcelColor with specified ARGB values + /// + /// Alpha component (0-255, where 0 is transparent and 255 is opaque) + /// Red component (0-255) + /// Green component (0-255) + /// Blue component (0-255) + public ExcelColor(byte a, byte r, byte g, byte b) + { + A = a; + R = r; + G = g; + B = b; + } + + /// + /// Creates a color from a hex string (e.g., "#FF0000" or "FF0000") + /// + /// Hex color string in format "#RRGGBB", "RRGGBB", "#AARRGGBB", or "AARRGGBB" + /// Thrown when hex string is not in a valid format + /// Thrown when hex string is null or empty + public ExcelColor(string hex) + { + if (string.IsNullOrEmpty(hex)) + throw new ArgumentNullException(nameof(hex)); + + hex = hex.TrimStart('#'); + try + { + if (hex.Length == 6) + { + A = 255; + R = Convert.ToByte(hex.Substring(0, 2), 16); + G = Convert.ToByte(hex.Substring(2, 2), 16); + B = Convert.ToByte(hex.Substring(4, 2), 16); + return; + } + else if (hex.Length == 8) + { + A = Convert.ToByte(hex.Substring(0, 2), 16); + R = Convert.ToByte(hex.Substring(2, 2), 16); + G = Convert.ToByte(hex.Substring(4, 2), 16); + B = Convert.ToByte(hex.Substring(6, 2), 16); + return; + } + } + catch (FormatException ex) + { + throw new ArgumentException($"Invalid hex color format: {hex}", nameof(hex), ex); + } + throw new ArgumentException($"Invalid hex color format: {hex}", nameof(hex)); + } + + /// + /// Converts the color to a hex string (e.g., "#RRGGBB") + /// + public string Rgb => $"{R:X2}{G:X2}{B:X2}"; + + /// + /// Gets the color value as an ARGB hexadecimal string in the format AARRGGBB. + /// + public string Argb => $"{A:X2}{R:X2}{G:X2}{B:X2}"; + + /// + /// Gets a predefined black color (RGB: 0, 0, 0) + /// + public static ExcelColor Black => new ExcelColor(0, 0, 0); + + /// + /// Gets a predefined white color (RGB: 255, 255, 255) + /// + public static ExcelColor White => new ExcelColor(255, 255, 255); + + /// + /// Gets a predefined red color (RGB: 255, 0, 0) + /// + public static ExcelColor Red => new ExcelColor(255, 0, 0); + + /// + /// Gets a predefined green color (RGB: 0, 255, 0) + /// + public static ExcelColor Green => new ExcelColor(0, 255, 0); + + /// + /// Gets a predefined blue color (RGB: 0, 0, 255) + /// + public static ExcelColor Blue => new ExcelColor(0, 0, 255); + + /// + /// Gets a predefined yellow color (RGB: 255, 255, 0) + /// + public static ExcelColor Yellow => new ExcelColor(255, 255, 0); + + /// + /// Gets a predefined light gray color (RGB: 211, 211, 211) + /// + public static ExcelColor LightGray => new ExcelColor(211, 211, 211); + + /// + /// Gets a predefined gray color (RGB: 128, 128, 128) + /// + public static ExcelColor Gray => new ExcelColor(128, 128, 128); + + /// + /// Gets a predefined dark gray color (RGB: 169, 169, 169) + /// + public static ExcelColor DarkGray => new ExcelColor(169, 169, 169); + } + + /// + /// Border styles for cell edges + /// + public enum BorderStyle + { + /// + /// No border + /// + None = 0, + + /// + /// Thin border line + /// + Thin = 1, + + /// + /// Medium thickness border line + /// + Medium = 2, + + /// + /// Thick border line + /// + Thick = 3, + + /// + /// Double border line + /// + Double = 4, + + /// + /// Dotted border line + /// + Dotted = 5, + + /// + /// Dashed border line + /// + Dashed = 6, + + /// + /// Dash-dot pattern border line + /// + DashDot = 7, + + /// + /// Dash-dot-dot pattern border line + /// + DashDotDot = 8 + } + + /// + /// Vertical alignment options for cell content + /// + public enum VerticalAlignment + { + /// + /// Align content to the top of the cell + /// + Top = 0, + + /// + /// Align content to the center (middle) of the cell + /// + Center = 1, + + /// + /// Align content to the bottom of the cell + /// + Bottom = 2, + + /// + /// Justify content vertically with equal spacing + /// + Justify = 3, + + /// + /// Distribute content vertically with equal spacing between lines + /// + Distributed = 4 + } + + /// + /// Horizontal alignment options for cell content + /// + public enum HorizontalAlignment + { + /// + /// General (default) alignment based on content type + /// + General = 0, + + /// + /// Align content to the left of the cell + /// + Left = 1, + + /// + /// Align content to the center of the cell + /// + Center = 2, + + /// + /// Align content to the right of the cell + /// + Right = 3, + + /// + /// Fill the cell by repeating the content + /// + Fill = 4, + + /// + /// Justify content horizontally with equal spacing + /// + Justify = 5, + + /// + /// Center content across selection (multiple cells) + /// + CenterContinuous = 6, + + /// + /// Distribute content horizontally with equal spacing + /// + Distributed = 7 + } + + /// + /// Fill style patterns for cell backgrounds + /// + public enum FillStyle + { + /// + /// No fill pattern + /// + None = 0, + + /// + /// Solid color fill + /// + Solid = 1, + + /// + /// Dark gray pattern fill + /// + DarkGray = 2, + + /// + /// Medium gray pattern fill + /// + MediumGray = 3, + + /// + /// Light gray pattern fill + /// + LightGray = 4, + + /// + /// 12.5% gray pattern fill + /// + Gray125 = 5, + + /// + /// 6.25% gray pattern fill + /// + Gray0625 = 6 + } +} diff --git a/src/Kevull.EPPLus.MultiHeader/HeaderManager.cs b/src/Kevull.MultiHeader.Core/HeaderManager.cs similarity index 97% rename from src/Kevull.EPPLus.MultiHeader/HeaderManager.cs rename to src/Kevull.MultiHeader.Core/HeaderManager.cs index 90001d5..3417e80 100644 --- a/src/Kevull.EPPLus.MultiHeader/HeaderManager.cs +++ b/src/Kevull.MultiHeader.Core/HeaderManager.cs @@ -1,12 +1,13 @@ -using Kevull.EPPLus.MultiHeader.Columns; +using Kevull.MultiHeader.Core; +using Kevull.MultiHeader.Core.Columns; using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; -using System.Security.Cryptography.Pkcs; -namespace Kevull.EPPLus.MultiHeader + +namespace Kevull.MultiHeader.Core { /// /// Stores information about the columns to be shown and build the needed header structure @@ -45,7 +46,7 @@ public class HeaderManager /// /// If true the configuration will find the end of a previous report and /// - public bool AppendToExistingReport { get; internal set; } + public bool AppendToExistingReport { get; set; } /// /// Porperties, by property name, of the source Type @@ -121,7 +122,7 @@ protected HeaderManager(Type type, int index, int deep, List? column /// /// Build the and header structure /// - internal void BuildHeaders() + public void BuildHeaders() { BuildHeaders(FirstColumn, 1); } diff --git a/src/Kevull.MultiHeader.Core/IConfigurationBuilder.cs b/src/Kevull.MultiHeader.Core/IConfigurationBuilder.cs new file mode 100644 index 0000000..689ac52 --- /dev/null +++ b/src/Kevull.MultiHeader.Core/IConfigurationBuilder.cs @@ -0,0 +1,191 @@ +using Kevull.MultiHeader.Core; +using Kevull.MultiHeader.Core.Columns; +using System.Linq.Expressions; + +namespace Kevull.MultiHeader.Core +{ + /// + /// Defines the fluent configuration contract for building a multi-header report definition. + /// + /// The type of the source data used to generate report rows. + public interface IConfigurationBuilder + { + /// + /// Gets or sets a value indicating whether the generated output should be appended to an existing report. + /// + bool AppendToExistingReport { get; set; } + + /// + /// Gets or sets a value indicating whether auto-filter should be enabled for the generated header row. + /// + bool AutoFilter { get; set; } + + /// + /// Gets or sets a value indicating whether panes should be frozen automatically at the starting position. + /// + bool AutoFreezePanes { get; set; } + + /// + /// Gets the starting left column index for writing the report. + /// + int LeftColumn { get; } + + /// + /// Gets the starting top row index for writing the report. + /// + int TopRow { get; } + + /// + /// Adds a column mapped to the specified model property. + /// + /// The expression selecting the model property to map as a column. + /// The current configuration builder instance. + IConfigurationBuilder AddColumn(Expression> columnSelector); + + /// + /// Adds a column mapped to the specified model property with optional metadata. + /// + /// The expression selecting the model property to map as a column. + /// The zero-based display order of the column. + /// The display name shown in the header. + /// A value indicating whether the column is hidden. + /// The name of a style to apply to the column. + /// The current configuration builder instance. + IConfigurationBuilder AddColumn(Expression> columnSelector, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null); + + /// + /// Adds a column mapped to the specified model property and configures it using a custom action. + /// + /// The expression selecting the model property to map as a column. + /// The configuration action for the created column definition. + /// The current configuration builder instance. + IConfigurationBuilder AddColumn(Expression> columnSelector, Action cfg); + + /// + /// Adds an enumeration-based column mapped to the specified model property. + /// + /// The expression selecting the model property to map as a column. + /// The ordered list of allowed values for the enumeration column. + /// The zero-based display order of the column. + /// The display name shown in the header. + /// A value indicating whether the column is hidden. + /// The name of a style to apply to the column. + /// The current configuration builder instance. + IConfigurationBuilder AddEnumeration(Expression> columnSelector, IEnumerable keyValues, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null); + + /// + /// Adds an enumeration-based column mapped to the specified model property and configures it using a custom action. + /// + /// The expression selecting the model property to map as a column. + /// The ordered list of allowed values for the enumeration column. + /// The configuration action for the created column definition. + /// The current configuration builder instance. + IConfigurationBuilder AddEnumeration(Expression> columnSelector, IEnumerable keyValues, Action cfg); + + /// + /// Adds a calculated expression column. + /// + /// The internal column name. + /// The value expression evaluated for each data item. + /// The zero-based display order of the column. + /// The display name shown in the header. + /// A value indicating whether the column is hidden. + /// The name of a style to apply to the column. + /// The current configuration builder instance. + IConfigurationBuilder AddExpression(string name, Func expression, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null); + + /// + /// Adds a calculated expression column and configures it using a custom action. + /// + /// The internal column name. + /// The value expression evaluated for each data item. + /// The configuration action for the created column definition. + /// The current configuration builder instance. + IConfigurationBuilder AddExpression(string name, Func expression, Action cfg); + + /// + /// Adds a formula column. + /// + /// The internal column name. + /// The Excel formula to apply. + /// The zero-based display order of the column. + /// The display name shown in the header. + /// A value indicating whether the column is hidden. + /// The name of a style to apply to the column. + /// The current configuration builder instance. + IConfigurationBuilder AddFormula(string name, string formula, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null); + + /// + /// Adds a formula column and configures it using a custom action. + /// + /// The internal column name. + /// The Excel formula to apply. + /// The configuration action for the created column definition. + /// The current configuration builder instance. + IConfigurationBuilder AddFormula(string name, string formula, Action cfg); + + /// + /// Adds a style to be applied to header cells. + /// + /// The action that configures header cell formatting. + /// The current configuration builder instance. + IConfigurationBuilder AddHeaderStyle(Action style); + + /// + /// Adds a hyperlink column using one property for display text and another for URL. + /// + /// The expression selecting the model property used as hyperlink display text. + /// The expression selecting the model property used as hyperlink URL. + /// The zero-based display order of the column. + /// The display name shown in the header. + /// A value indicating whether the column is hidden. + /// The name of a style to apply to the column. + /// The current configuration builder instance. + IConfigurationBuilder AddHyperLinkColumn(Expression> columnSelector, Expression> urlColumnSelector, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null); + + /// + /// Adds a hyperlink column and configures it using a custom action. + /// + /// The expression selecting the model property used as hyperlink display text. + /// The expression selecting the model property used as hyperlink URL. + /// The configuration action for the created column definition. + /// The current configuration builder instance. + IConfigurationBuilder AddHyperLinkColumn(Expression> columnSelector, Expression> urlColumnSelector, Action cfg); + + /// + /// Adds a named reusable style. + /// + /// The style name. + /// The action that configures the style format. + /// The current configuration builder instance. + IConfigurationBuilder AddNamedStyle(string name, Action style); + + /// + /// Marks the specified model property to be ignored during report generation. + /// + /// The expression selecting the model property to ignore. + /// The current configuration builder instance. + IConfigurationBuilder IgnoreColumn(Expression> columnSelector); + + /// + /// Sets the starting position used to place the report in the worksheet. + /// + /// The starting row index. + /// The starting column index. + /// The current configuration builder instance. + IConfigurationBuilder SetStartingAddres(int row, int column); + + /// + /// Sets the starting position used to place the report in the worksheet. + /// + /// The Excel address (for example, A1) for the top-left report cell. + /// The current configuration builder instance. + IConfigurationBuilder SetStartingAddress(string address); + + /// + /// Builds the configured header manager used for report generation. + /// + /// The built instance. + HeaderManager Build(); + } +} \ No newline at end of file diff --git a/src/Kevull.MultiHeader.Core/IExcelWritter.cs b/src/Kevull.MultiHeader.Core/IExcelWritter.cs new file mode 100644 index 0000000..f98d78f --- /dev/null +++ b/src/Kevull.MultiHeader.Core/IExcelWritter.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Kevull.MultiHeader.Core +{ + + /// + /// Interface for writing and formatting Excel cells in a library-agnostic way + /// + public interface IExcelWriter + { + + /// + /// Writes a value to a specific cell + /// + /// Row number (1-based) + /// Column number (1-based) + /// Value to write to the cell (can be null for empty cells) + void WriteCell(int row, int col, object? value); + + /// + /// Writes a value to a range of cells + /// + /// Starting row number (1-based) + /// Starting column number (1-based) + /// Ending row number (1-based) + /// Ending column number (1-based) + /// Value to write to all cells in the range (can be null for empty cells) + void WriteCell(int fromRow, int fromCol, int toRow, int toCol, object? value); + + /// + /// Writes a value with a hyperlink to a specific cell + /// + /// Row number (1-based) + /// Column number (1-based) + /// Value to display in the cell (can be null) + /// URL for the hyperlink + void WriteCellWithHyperlink(int row, int col, object? value, string url); + + /// + /// Writes a formula to a specific cell + /// Row number (1-based) + /// Column number (1-based) + /// Excel formula (without the leading '=') + void WriteFormula(int row, int col, string formula); + + /// + /// Writes a formula to a range of cells + /// + /// Starting row number (1-based) + /// Starting column number (1-based) + /// Ending row number (1-based) + /// Ending column number (1-based) + /// Excel formula (without the leading '=') + void WriteFormula(int fromRow, int fromCol, int toRow, int toCol, string formula); + + /// + /// Applies formatting to a specific cell using the library-agnostic + /// + /// Row number (1-based) + /// Column number (1-based) + /// Cell format to apply + void ApplyFormat(int row, int col, CellFormat format); + + /// + /// Applies formatting to a range of cells using the library-agnostic + /// + /// Starting row number (1-based) + /// Starting column number (1-based) + /// Ending row number (1-based) + /// Ending column number (1-based) + /// Cell format to apply + void ApplyFormat(int fromRow, int fromCol, int toRow, int toCol, CellFormat format); + + /// + /// Applies native library-specific formatting to a specific cell + /// + /// Row number (1-based) + /// Column number (1-based) + /// Action that receives the native cell/range object for direct manipulation + void ApplyNativeFormat(int row, int col, Action format); + + /// + /// Applies native library-specific formatting to a range of cells + /// + /// Starting row number (1-based) + /// Starting column number (1-based) + /// Ending row number (1-based) + /// Ending column number (1-based) + /// Action that receives the native cell/range object for direct manipulation + void ApplyNativeFormat(int fromRow, int fromCol, int toRow, int toCol, Action format); + + /// + /// Creates a named style that can be reused across multiple cells + /// + /// Unique name for the style + /// Cell format defining the style properties + void CreateNamedStyle(string styleName, CellFormat format); + + /// + /// Applies a previously created named style to a specific cell + /// + /// Row number (1-based) + /// Column number (1-based) + /// Name of the style to apply + void ApplyNamedStyle(int row, int col, string styleName); + + /// + /// Applies a previously created named style to a range of cells + /// + /// Starting row number (1-based) + /// Starting column number (1-based) + /// Ending row number (1-based) + /// Ending column number (1-based) + /// Name of the style to apply + void ApplyNamedStyle(int fromRow, int fromCol, int toRow, int toCol, string styleName); + + /// + /// Checks if a named style exists + /// + /// Name of the style to check + /// True if the style exists, false otherwise + bool NamedStyleExists(string styleName); + + /// + /// Merges a range of cells into a single cell + /// + /// Starting row number (1-based) + /// Starting column number (1-based) + /// Ending row number (1-based) + /// Ending column number (1-based) + void Merge(int fromRow, int fromCol, int toRow, int toCol); + + /// + /// Auto-fits a column width to its content + /// + /// Column number (1-based) + void AutoFitColumn(int col); + + /// + /// Auto-fits a column width to its content with minimum and maximum width constraints + /// + /// Column number (1-based) + /// Minimum width for the column + /// Maximum width for the column + void AutoFitColumn(int col, double minWidth, double maxWidth); + + /// + /// Sets the width of a column + /// + /// Column number (1-based) + /// Width value for the column + void SetColumnWidth(int col, double width); + + /// + /// Hides or shows a column + /// + /// Column number (1-based) + /// True to hide the column, false to show it + void SetColumnHidden(int col, bool hidden); + + /// + /// Enables or disables AutoFilter for a range of cells + /// + /// Starting row number (1-based) + /// Starting column number (1-based) + /// Ending row number (1-based) + /// Ending column number (1-based) + /// True to enable AutoFilter, false to disable it + void SetAutoFilter(int fromRow, int fromCol, int toRow, int toCol, bool autoFilter); + + /// + /// Freezes panes at the specified position + /// + /// Row number where the freeze should occur (1-based) + /// Column number where the freeze should occur (1-based) + void FreezePanes(int row, int col); + + /// + /// Forces recalculation of all formulas in the worksheet + /// + void Recalculate(); + + } +} diff --git a/src/Kevull.MultiHeader.Core/IMultiHeaderReport.cs b/src/Kevull.MultiHeader.Core/IMultiHeaderReport.cs new file mode 100644 index 0000000..5b8e555 --- /dev/null +++ b/src/Kevull.MultiHeader.Core/IMultiHeaderReport.cs @@ -0,0 +1,28 @@ +namespace Kevull.MultiHeader.Core +{ + /// + /// Defines the operations required to configure, generate, and save a multi-header report. + /// + /// The type of the data items used to populate the report. + public interface IMultiHeaderReport + { + /// + /// Configures the report using the specified configuration action. + /// + /// An action that configures the report columns and options. + /// The current report instance to allow fluent configuration. + IMultiHeaderReport Configure(Action> options); + + /// + /// Generates the report content from the provided data. + /// + /// The sequence of data items to include in the report. + void GenerateReport(IEnumerable data); + + /// + /// Saves the generated report to the specified file. + /// + /// The destination file name or path. + void Save(string fileName); + } +} \ No newline at end of file diff --git a/src/Kevull.MultiHeader.Core/Kevull.MultiHeader.Core.csproj b/src/Kevull.MultiHeader.Core/Kevull.MultiHeader.Core.csproj new file mode 100644 index 0000000..28cce1c --- /dev/null +++ b/src/Kevull.MultiHeader.Core/Kevull.MultiHeader.Core.csproj @@ -0,0 +1,57 @@ + + + + net10.0;netstandard2.0 + 14.0 + enable + enable + True + + true + all + + + + true + MNieto + EPlus multi-header report + Extension for Excel libraries to create reports from complex objects + https://github.com/mnieto/Kevull.MultiHeader + https://github.com/mnieto/Kevull.MultiHeader + git + EPPlus;Excel;Multi-header;Report + LGPL-3.0-or-later + README.md + EPlusMultiHeader.png + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + <_Parameter1>Kevull.MultiHeader.EPPLus.Tests + + + <_Parameter1>Kevull.MultiHeader.Core.Tests + + + <_Parameter1>Kevull.MultiHeader.ClosedXml.Tests + + + + + True + \ + + + True + \ + + + diff --git a/src/Kevull.MultiHeader.Core/StyleNames.cs b/src/Kevull.MultiHeader.Core/StyleNames.cs new file mode 100644 index 0000000..f2367b7 --- /dev/null +++ b/src/Kevull.MultiHeader.Core/StyleNames.cs @@ -0,0 +1,39 @@ +namespace Kevull.MultiHeader.Core +{ + /// + /// Contains names and number formats used by default report styles. + /// + public class StyleNames + { + /// + /// Name of the default header style. + /// + public const string HeaderStyleName = "__Headers__"; + + /// + /// Name of the default date style. + /// + public const string DateStyleName = "__date__"; + + /// + /// Name of the default time style. + /// + public const string TimeStyleName = "__time__"; + + /// + /// Default number format for time values. + /// + /// + /// This format depends on local system settings. + /// + public const string TimeFormat = "[$-x-systime]h:mm:ss AM/PM"; + + /// + /// Default number format for date values. + /// + /// + /// This format depends on local system settings. + /// + public const string DateFormat = "mm-dd-yy"; + } +} \ No newline at end of file diff --git a/src/Kevull.EPPLus.MultiHeader/ConfigurationBuilder.cs b/src/Kevull.MultiHeader.EPPLus/ConfigurationBuilder.cs similarity index 80% rename from src/Kevull.EPPLus.MultiHeader/ConfigurationBuilder.cs rename to src/Kevull.MultiHeader.EPPLus/ConfigurationBuilder.cs index 0c27e2c..36db356 100644 --- a/src/Kevull.EPPLus.MultiHeader/ConfigurationBuilder.cs +++ b/src/Kevull.MultiHeader.EPPLus/ConfigurationBuilder.cs @@ -1,22 +1,28 @@ -using Kevull.EPPLus.MultiHeader.Columns; +using Kevull.MultiHeader.Core; +using Kevull.MultiHeader.Core.Columns; using OfficeOpenXml; using OfficeOpenXml.Style; using System.Linq.Expressions; using System.Net; using System.Reflection; -namespace Kevull.EPPLus.MultiHeader +namespace Kevull.MultiHeader.EPPLus { /// /// Helper class to configure the report and column options /// /// - public class ConfigurationBuilder + public class ConfigurationBuilder : IConfigurationBuilder { private List columns; private ExcelPackage xls; private ExcelCellAddress StartingAddress { get; set; } = new ExcelCellAddress(); + /// + /// Collection of named styles available for column and header formatting. + /// + public Dictionary NamedStyles = new Dictionary(); + /// /// Shows or not autofilter on last header row /// @@ -73,7 +79,7 @@ public ConfigurationBuilder(ExcelPackage xls, IEnumerable columns) /// /// Adds a column with default configuration /// - public ConfigurationBuilder AddColumn(Expression> columnSelector) + public IConfigurationBuilder AddColumn(Expression> columnSelector) { columns.Add(new ColumnInfo(columnSelector)); return this; @@ -87,7 +93,7 @@ public ConfigurationBuilder AddColumn(Expression> columnSele /// Human friendly name for the column. If not specified, the property Name is used /// Column is written to the Excel, but it's hidden /// Name of a style defined in the Excel workbook - public ConfigurationBuilder AddColumn(Expression> columnSelector, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null) + public IConfigurationBuilder AddColumn(Expression> columnSelector, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null) { columns.Add(new ColumnInfo(columnSelector, order, displayName, hidden, styleName)); return this; @@ -98,7 +104,7 @@ public ConfigurationBuilder AddColumn(Expression> columnSele /// /// Lambda expression to specify the property /// Action that will be invoked to configure the ColumnInfo properties using a object - public ConfigurationBuilder AddColumn(Expression> columnSelector, Action cfg) + public IConfigurationBuilder AddColumn(Expression> columnSelector, Action cfg) { columns.Add(new ColumnInfo(columnSelector, cfg)); return this; @@ -113,7 +119,7 @@ public ConfigurationBuilder AddColumn(Expression> columnSele /// Human friendly name for the column. If not specified, the property Name is used /// Column is written to the Excel, but it's hidden /// Name of a style defined in the Excel workbook - public ConfigurationBuilder AddEnumeration(Expression> columnSelector, IEnumerable keyValues, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null) + public IConfigurationBuilder AddEnumeration(Expression> columnSelector, IEnumerable keyValues, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null) { columns.Add(new ColumnEnumeration(columnSelector, keyValues, order, displayName, hidden, styleName)); return this; @@ -125,7 +131,7 @@ public ConfigurationBuilder AddEnumeration(Expression> colum /// Lambda expression to specify the property /// Allowed key values. This is used to allocate a specific number of columns /// Action that will be invoked to configure the ColumnInfo properties using a object - public ConfigurationBuilder AddEnumeration(Expression> columnSelector, IEnumerable keyValues, Action cfg) + public IConfigurationBuilder AddEnumeration(Expression> columnSelector, IEnumerable keyValues, Action cfg) { columns.Add(new ColumnEnumeration(columnSelector, keyValues, cfg)); return this; @@ -140,7 +146,7 @@ public ConfigurationBuilder AddEnumeration(Expression> colum /// Human friendly name for the column. If not specified, the property Name is used /// Column is written to the Excel, but it's hidden /// Name of a style defined in the Excel workbook - public ConfigurationBuilder AddExpression(string name, Func expression, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null) + public IConfigurationBuilder AddExpression(string name, Func expression, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null) { columns.Add(new ColumnExpression(name, expression, order, displayName, hidden, styleName)); return this; @@ -152,7 +158,7 @@ public ConfigurationBuilder AddExpression(string name, Func expre /// name of the property. In this case, it cannot be infered from the source Type /// Lambda expression to be evaluated to render the column value each row /// Action that will be invoked to configure the ColumnInfo properties using a object - public ConfigurationBuilder AddExpression(string name, Func expression, Action cfg) + public IConfigurationBuilder AddExpression(string name, Func expression, Action cfg) { columns.Add(new ColumnExpression(name, expression, cfg)); return this; @@ -167,7 +173,7 @@ public ConfigurationBuilder AddExpression(string name, Func expre /// Human friendly name for the column. If not specified, the property Name is used /// Column is written to the Excel, but it's hidden /// Name of a style defined in the Excel workbook - public ConfigurationBuilder AddFormula(string name, string formula, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null) + public IConfigurationBuilder AddFormula(string name, string formula, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null) { columns.Add(new ColumnFormula(name, formula, order, displayName, hidden, styleName)); return this; @@ -179,7 +185,7 @@ public ConfigurationBuilder AddFormula(string name, string formula, int? orde /// name of the property. In this case, it cannot be infered from the source Type /// Formula used for this column. Be sure to use the correct absulte/relative references in the formula /// Action that will be invoked to configure the ColumnInfo properties using a object - public ConfigurationBuilder AddFormula(string name, string formula, Action cfg) + public IConfigurationBuilder AddFormula(string name, string formula, Action cfg) { columns.Add(new ColumnFormula(name, formula, cfg)); return this; @@ -194,7 +200,7 @@ public ConfigurationBuilder AddFormula(string name, string formula, ActionHuman friendly name for the column. If not specified, the property Name is used /// Column is written to the Excel, but it's hidden /// Name of a style defined in the Excel workbook - public ConfigurationBuilder AddHyperLinkColumn(Expression> columnSelector, Expression> urlColumnSelector, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null) + public IConfigurationBuilder AddHyperLinkColumn(Expression> columnSelector, Expression> urlColumnSelector, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null) { columns.Add(new ColumnHyperLink(columnSelector, urlColumnSelector, order, displayName, hidden, styleName)); return this; @@ -206,7 +212,7 @@ public ConfigurationBuilder AddHyperLinkColumn(Expression> c /// Lambda expression to specify the property /// Allows specify the column wich will contain the url /// Action that will be invoked to configure the ColumnInfo properties using a object - public ConfigurationBuilder AddHyperLinkColumn(Expression> columnSelector, Expression> urlColumnSelector, Action cfg) + public IConfigurationBuilder AddHyperLinkColumn(Expression> columnSelector, Expression> urlColumnSelector, Action cfg) { columns.Add(new ColumnHyperLink(columnSelector, urlColumnSelector, cfg)); return this; @@ -216,7 +222,7 @@ public ConfigurationBuilder AddHyperLinkColumn(Expression> c /// Ignore this property. This column will not be rendered /// /// Allows specify the column name - public ConfigurationBuilder IgnoreColumn(Expression> columnSelector) + public IConfigurationBuilder IgnoreColumn(Expression> columnSelector) { columns.Add(new ColumnInfo(columnSelector, true)); return this; @@ -240,20 +246,32 @@ public ConfigurationBuilder IgnoreColumn(Expression> columnS /// namedStyle.Style.Font.Bold = true; /// /// - public ConfigurationBuilder AddHeaderStyle(Action style) + public IConfigurationBuilder AddHeaderStyle(Action style) { - return AddNamedStyle(MultiHeaderReport.HeaderStyleName, style); + return AddNamedStyle(StyleNames.HeaderStyleName, style); } + ///// + ///// Adds a named style that can be used later by any column. + ///// + ///// Name of the style + ///// Action that allows to configure the style + //public ConfigurationBuilder AddNamedStyle(string name, Action style) + //{ + // var namedStyle = xls.Workbook.Styles.CreateNamedStyle(name); + // style?.Invoke(namedStyle.Style); + // return this; + //} + /// /// Adds a named style that can be used later by any column. /// /// Name of the style /// Action that allows to configure the style - public ConfigurationBuilder AddNamedStyle(string name, Action style) + public IConfigurationBuilder AddNamedStyle(string name, Action style) { - var namedStyle = xls.Workbook.Styles.CreateNamedStyle(name); - style?.Invoke(namedStyle.Style); + NamedStyles.Add(name, new CellFormat()); + style?.Invoke(NamedStyles[name]); return this; } @@ -261,7 +279,7 @@ public ConfigurationBuilder AddNamedStyle(string name, Action sty /// Configure the TopLeft starting cell of the report /// /// Address of the top-left cell - public ConfigurationBuilder SetStartingAddress(string address) + public IConfigurationBuilder SetStartingAddress(string address) { StartingAddress = new ExcelCellAddress(address); return this; @@ -272,7 +290,7 @@ public ConfigurationBuilder SetStartingAddress(string address) /// /// top row number /// left column number - public ConfigurationBuilder SetStartingAddres(int row, int column) + public IConfigurationBuilder SetStartingAddres(int row, int column) { StartingAddress = new ExcelCellAddress(row, column); return this; diff --git a/src/Kevull.MultiHeader.EPPLus/EPPlusExcelWriter.cs b/src/Kevull.MultiHeader.EPPLus/EPPlusExcelWriter.cs new file mode 100644 index 0000000..4f60414 --- /dev/null +++ b/src/Kevull.MultiHeader.EPPLus/EPPlusExcelWriter.cs @@ -0,0 +1,495 @@ +using Kevull.MultiHeader.Core; +using OfficeOpenXml; +using OfficeOpenXml.Style; +using System; +using System.Drawing; +using System.Linq; +using CoreExcelColor = Kevull.MultiHeader.Core.ExcelColor; + +namespace Kevull.MultiHeader.EPPLus +{ + /// + /// EPPlus implementation of + /// + public class EPPlusExcelWriter : IExcelWriter + { + private readonly ExcelWorksheet _sheet; + private readonly ExcelPackage _package; + + /// + /// Creates a new instance of EPPlusExcelWriter + /// + /// The ExcelPackage to work with + /// The ExcelWorksheet to work with + public EPPlusExcelWriter(ExcelPackage package, ExcelWorksheet sheet) + { + _package = package ?? throw new ArgumentNullException(nameof(package)); + _sheet = sheet ?? throw new ArgumentNullException(nameof(sheet)); + } + + #region Basic Writing + + /// + public void WriteCell(int row, int col, object? value) + { + _sheet.Cells[row, col].Value = value; + } + + /// + public void WriteCell(int fromRow, int fromCol, int toRow, int toCol, object? value) + { + _sheet.Cells[fromRow, fromCol, toRow, toCol].Value = value; + } + + /// + public void WriteCellWithHyperlink(int row, int col, object? value, string url) + { + var cell = _sheet.Cells[row, col]; + cell.Value = value; + if (!string.IsNullOrWhiteSpace(url)) + { + cell.Hyperlink = new Uri(url); + } + } + + /// + public void WriteFormula(int row, int col, string formula) + { + _sheet.Cells[row, col].Formula = formula; + } + + /// + public void WriteFormula(int fromRow, int fromCol, int toRow, int toCol, string formula) + { + _sheet.Cells[fromRow, fromCol, toRow, toCol].Formula = formula; + } + + #endregion + + #region Formatting + + /// + public void ApplyFormat(int row, int col, CellFormat format) + { + ApplyFormat(row, col, row, col, format); + } + + /// + public void ApplyFormat(int fromRow, int fromCol, int toRow, int toCol, CellFormat format) + { + if (format == null) return; + + var range = _sheet.Cells[fromRow, fromCol, toRow, toCol]; + ApplyFormatToRange(range.Style, format); + } + + /// + public void ApplyNativeFormat(int row, int col, Action format) + { + if (format == null) return; + var range = _sheet.Cells[row, col]; + format(range); + } + + /// + public void ApplyNativeFormat(int fromRow, int fromCol, int toRow, int toCol, Action format) + { + if (format == null) return; + var range = _sheet.Cells[fromRow, fromCol, toRow, toCol]; + format(range); + } + + #endregion + + #region Named Styles + + /// + public void CreateNamedStyle(string styleName, CellFormat format) + { + if (string.IsNullOrWhiteSpace(styleName)) + throw new ArgumentNullException(nameof(styleName)); + if (format == null) + throw new ArgumentNullException(nameof(format)); + + // Check if style already exists + if (NamedStyleExists(styleName)) + return; + + var namedStyle = _package.Workbook.Styles.CreateNamedStyle(styleName); + ApplyFormatToRange(namedStyle.Style, format); + } + + /// + public void ApplyNamedStyle(int row, int col, string styleName) + { + ApplyNamedStyle(row, col, row, col, styleName); + } + + /// + public void ApplyNamedStyle(int fromRow, int fromCol, int toRow, int toCol, string styleName) + { + if (string.IsNullOrWhiteSpace(styleName)) + throw new ArgumentNullException(nameof(styleName)); + + var range = _sheet.Cells[fromRow, fromCol, toRow, toCol]; + range.StyleName = styleName; + } + + /// + public bool NamedStyleExists(string styleName) + { + if (string.IsNullOrWhiteSpace(styleName)) + return false; + + return _package.Workbook.Styles.NamedStyles.Any(x => x.Name == styleName); + } + + #endregion + + #region Cell Operations + + /// + public void Merge(int fromRow, int fromCol, int toRow, int toCol) + { + _sheet.Cells[fromRow, fromCol, toRow, toCol].Merge = true; + } + + #endregion + + #region Column Operations + + /// + public void AutoFitColumn(int col) + { + _sheet.Column(col).AutoFit(); + } + + /// + public void AutoFitColumn(int col, double minWidth, double maxWidth) + { + _sheet.Column(col).AutoFit(minWidth, maxWidth); + } + + /// + public void SetColumnWidth(int col, double width) + { + _sheet.Column(col).Width = width; + } + + /// + public void SetColumnHidden(int col, bool hidden) + { + _sheet.Column(col).Hidden = hidden; + } + + /// + public void SetAutoFilter(int fromRow, int fromCol, int toRow, int toCol, bool autoFilter) + { + _sheet.Cells[fromRow, fromCol, toRow, toCol].AutoFilter = autoFilter; + } + + /// + public void FreezePanes(int row, int col) + { + _sheet.View.FreezePanes(row, col); + } + + /// + public void Recalculate() + { + _sheet.Calculate(); + } + + #endregion + + #region Private Helper Methods + + /// + /// Applies CellFormat to an ExcelStyle + /// + private void ApplyFormatToRange(ExcelStyle style, CellFormat format) + { + // Borders + if (format.LeftBorder.HasValue) + style.Border.Left.Style = ConvertBorderStyle(format.LeftBorder.Value); + if (format.RightBorder.HasValue) + style.Border.Right.Style = ConvertBorderStyle(format.RightBorder.Value); + if (format.TopBorder.HasValue) + style.Border.Top.Style = ConvertBorderStyle(format.TopBorder.Value); + if (format.BottomBorder.HasValue) + style.Border.Bottom.Style = ConvertBorderStyle(format.BottomBorder.Value); + + // Alignment + if (format.VerticalAlignment.HasValue) + style.VerticalAlignment = ConvertVerticalAlignment(format.VerticalAlignment.Value); + if (format.HorizontalAlignment.HasValue) + style.HorizontalAlignment = ConvertHorizontalAlignment(format.HorizontalAlignment.Value); + + // Fill + if (format.BackgroundColor.HasValue && format.FillStyle.HasValue) + { + var color = ConvertColor(format.BackgroundColor.Value); + var fillStyle = ConvertFillStyle(format.FillStyle.Value); + style.Fill.SetBackground(color, fillStyle); + } + + // Font + if (format.Bold.HasValue) + style.Font.Bold = format.Bold.Value; + if (format.Italic.HasValue) + style.Font.Italic = format.Italic.Value; + if (!string.IsNullOrWhiteSpace(format.FontName)) + style.Font.Name = format.FontName; + if (format.FontSize.HasValue) + style.Font.Size = format.FontSize.Value; + if (format.FontColor.HasValue) + style.Font.Color.SetColor(ConvertColor(format.FontColor.Value)); + + // Number Format + if (!string.IsNullOrWhiteSpace(format.NumberFormat)) + style.Numberformat.Format = format.NumberFormat; + + // Text Rotation + if (format.TextRotation.HasValue) + style.TextRotation = format.TextRotation.Value; + + // Wrap Text + if (format.WrapText.HasValue) + style.WrapText = format.WrapText.Value; + } + + /// + /// Converts library-agnostic BorderStyle to EPPlus ExcelBorderStyle + /// + private ExcelBorderStyle ConvertBorderStyle(Core.BorderStyle borderStyle) + { + return borderStyle switch + { + Core.BorderStyle.None => ExcelBorderStyle.None, + Core.BorderStyle.Thin => ExcelBorderStyle.Thin, + Core.BorderStyle.Medium => ExcelBorderStyle.Medium, + Core.BorderStyle.Thick => ExcelBorderStyle.Thick, + Core.BorderStyle.Double => ExcelBorderStyle.Double, + Core.BorderStyle.Dotted => ExcelBorderStyle.Dotted, + Core.BorderStyle.Dashed => ExcelBorderStyle.Dashed, + Core.BorderStyle.DashDot => ExcelBorderStyle.DashDot, + Core.BorderStyle.DashDotDot => ExcelBorderStyle.DashDotDot, + _ => ExcelBorderStyle.None + }; + } + + /// + /// Converts library-agnostic VerticalAlignment to EPPlus ExcelVerticalAlignment + /// + private ExcelVerticalAlignment ConvertVerticalAlignment(Core.VerticalAlignment alignment) + { + return alignment switch + { + Core.VerticalAlignment.Top => ExcelVerticalAlignment.Top, + Core.VerticalAlignment.Center => ExcelVerticalAlignment.Center, + Core.VerticalAlignment.Bottom => ExcelVerticalAlignment.Bottom, + Core.VerticalAlignment.Justify => ExcelVerticalAlignment.Justify, + Core.VerticalAlignment.Distributed => ExcelVerticalAlignment.Distributed, + _ => ExcelVerticalAlignment.Bottom + }; + } + + /// + /// Converts library-agnostic HorizontalAlignment to EPPlus ExcelHorizontalAlignment + /// + private ExcelHorizontalAlignment ConvertHorizontalAlignment(Core.HorizontalAlignment alignment) + { + return alignment switch + { + Core.HorizontalAlignment.General => ExcelHorizontalAlignment.General, + Core.HorizontalAlignment.Left => ExcelHorizontalAlignment.Left, + Core.HorizontalAlignment.Center => ExcelHorizontalAlignment.Center, + Core.HorizontalAlignment.Right => ExcelHorizontalAlignment.Right, + Core.HorizontalAlignment.Fill => ExcelHorizontalAlignment.Fill, + Core.HorizontalAlignment.Justify => ExcelHorizontalAlignment.Justify, + Core.HorizontalAlignment.CenterContinuous => ExcelHorizontalAlignment.CenterContinuous, + Core.HorizontalAlignment.Distributed => ExcelHorizontalAlignment.Distributed, + _ => ExcelHorizontalAlignment.General + }; + } + + /// + /// Converts library-agnostic FillStyle to EPPlus ExcelFillStyle + /// + private ExcelFillStyle ConvertFillStyle(Core.FillStyle fillStyle) + { + return fillStyle switch + { + Core.FillStyle.None => ExcelFillStyle.None, + Core.FillStyle.Solid => ExcelFillStyle.Solid, + Core.FillStyle.DarkGray => ExcelFillStyle.DarkGray, + Core.FillStyle.MediumGray => ExcelFillStyle.MediumGray, + Core.FillStyle.LightGray => ExcelFillStyle.LightGray, + Core.FillStyle.Gray125 => ExcelFillStyle.Gray125, + Core.FillStyle.Gray0625 => ExcelFillStyle.Gray0625, + _ => ExcelFillStyle.None + }; + } + + /// + /// Converts library-agnostic ExcelColor to System.Drawing.Color + /// + private Color ConvertColor(CoreExcelColor excelColor) + { + return Color.FromArgb(excelColor.A, excelColor.R, excelColor.G, excelColor.B); + } + + #endregion + } + + /// + /// Extension methods to apply library-agnostic cell format definitions to EPPlus styles. + /// + public static class ExcelStyleExtensions + { + /// + /// Sets the background color and fill style for an ExcelStyle + /// + public static ExcelStyle SetBackground(this CellFormat format, ExcelStyle style) + { + ApplyFormatToRange(style, format); + return style; + } + + + /// + /// Applies CellFormat to an ExcelStyle + /// + private static void ApplyFormatToRange(ExcelStyle style, CellFormat format) + { + // Borders + if (format.LeftBorder.HasValue) + style.Border.Left.Style = ConvertBorderStyle(format.LeftBorder.Value); + if (format.RightBorder.HasValue) + style.Border.Right.Style = ConvertBorderStyle(format.RightBorder.Value); + if (format.TopBorder.HasValue) + style.Border.Top.Style = ConvertBorderStyle(format.TopBorder.Value); + if (format.BottomBorder.HasValue) + style.Border.Bottom.Style = ConvertBorderStyle(format.BottomBorder.Value); + + // Alignment + if (format.VerticalAlignment.HasValue) + style.VerticalAlignment = ConvertVerticalAlignment(format.VerticalAlignment.Value); + if (format.HorizontalAlignment.HasValue) + style.HorizontalAlignment = ConvertHorizontalAlignment(format.HorizontalAlignment.Value); + + // Fill + if (format.BackgroundColor.HasValue && format.FillStyle.HasValue) + { + var color = ConvertColor(format.BackgroundColor.Value); + var fillStyle = ConvertFillStyle(format.FillStyle.Value); + style.Fill.SetBackground(color, fillStyle); + } + + // Font + if (format.Bold.HasValue) + style.Font.Bold = format.Bold.Value; + if (format.Italic.HasValue) + style.Font.Italic = format.Italic.Value; + if (!string.IsNullOrWhiteSpace(format.FontName)) + style.Font.Name = format.FontName; + if (format.FontSize.HasValue) + style.Font.Size = format.FontSize.Value; + if (format.FontColor.HasValue) + style.Font.Color.SetColor(ConvertColor(format.FontColor.Value)); + + // Number Format + if (!string.IsNullOrWhiteSpace(format.NumberFormat)) + style.Numberformat.Format = format.NumberFormat; + + // Text Rotation + if (format.TextRotation.HasValue) + style.TextRotation = format.TextRotation.Value; + + // Wrap Text + if (format.WrapText.HasValue) + style.WrapText = format.WrapText.Value; + } + + /// + /// Converts library-agnostic BorderStyle to EPPlus ExcelBorderStyle + /// + private static ExcelBorderStyle ConvertBorderStyle(Core.BorderStyle borderStyle) + { + return borderStyle switch + { + Core.BorderStyle.None => ExcelBorderStyle.None, + Core.BorderStyle.Thin => ExcelBorderStyle.Thin, + Core.BorderStyle.Medium => ExcelBorderStyle.Medium, + Core.BorderStyle.Thick => ExcelBorderStyle.Thick, + Core.BorderStyle.Double => ExcelBorderStyle.Double, + Core.BorderStyle.Dotted => ExcelBorderStyle.Dotted, + Core.BorderStyle.Dashed => ExcelBorderStyle.Dashed, + Core.BorderStyle.DashDot => ExcelBorderStyle.DashDot, + Core.BorderStyle.DashDotDot => ExcelBorderStyle.DashDotDot, + _ => ExcelBorderStyle.None + }; + } + + /// + /// Converts library-agnostic VerticalAlignment to EPPlus ExcelVerticalAlignment + /// + private static ExcelVerticalAlignment ConvertVerticalAlignment(Core.VerticalAlignment alignment) + { + return alignment switch + { + Core.VerticalAlignment.Top => ExcelVerticalAlignment.Top, + Core.VerticalAlignment.Center => ExcelVerticalAlignment.Center, + Core.VerticalAlignment.Bottom => ExcelVerticalAlignment.Bottom, + Core.VerticalAlignment.Justify => ExcelVerticalAlignment.Justify, + Core.VerticalAlignment.Distributed => ExcelVerticalAlignment.Distributed, + _ => ExcelVerticalAlignment.Bottom + }; + } + + /// + /// Converts library-agnostic HorizontalAlignment to EPPlus ExcelHorizontalAlignment + /// + private static ExcelHorizontalAlignment ConvertHorizontalAlignment(Core.HorizontalAlignment alignment) + { + return alignment switch + { + Core.HorizontalAlignment.General => ExcelHorizontalAlignment.General, + Core.HorizontalAlignment.Left => ExcelHorizontalAlignment.Left, + Core.HorizontalAlignment.Center => ExcelHorizontalAlignment.Center, + Core.HorizontalAlignment.Right => ExcelHorizontalAlignment.Right, + Core.HorizontalAlignment.Fill => ExcelHorizontalAlignment.Fill, + Core.HorizontalAlignment.Justify => ExcelHorizontalAlignment.Justify, + Core.HorizontalAlignment.CenterContinuous => ExcelHorizontalAlignment.CenterContinuous, + Core.HorizontalAlignment.Distributed => ExcelHorizontalAlignment.Distributed, + _ => ExcelHorizontalAlignment.General + }; + } + + /// + /// Converts library-agnostic FillStyle to EPPlus ExcelFillStyle + /// + private static ExcelFillStyle ConvertFillStyle(Core.FillStyle fillStyle) + { + return fillStyle switch + { + Core.FillStyle.None => ExcelFillStyle.None, + Core.FillStyle.Solid => ExcelFillStyle.Solid, + Core.FillStyle.DarkGray => ExcelFillStyle.DarkGray, + Core.FillStyle.MediumGray => ExcelFillStyle.MediumGray, + Core.FillStyle.LightGray => ExcelFillStyle.LightGray, + Core.FillStyle.Gray125 => ExcelFillStyle.Gray125, + Core.FillStyle.Gray0625 => ExcelFillStyle.Gray0625, + _ => ExcelFillStyle.None + }; + } + + /// + /// Converts library-agnostic ExcelColor to System.Drawing.Color + /// + private static Color ConvertColor(CoreExcelColor excelColor) + { + return Color.FromArgb(excelColor.A, excelColor.R, excelColor.G, excelColor.B); + } + } +} diff --git a/src/Kevull.EPPLus.MultiHeader/Kevull.EPPLus.MultiHeader.csproj b/src/Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj similarity index 69% rename from src/Kevull.EPPLus.MultiHeader/Kevull.EPPLus.MultiHeader.csproj rename to src/Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj index 2f14887..b2f63bb 100644 --- a/src/Kevull.EPPLus.MultiHeader/Kevull.EPPLus.MultiHeader.csproj +++ b/src/Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj @@ -1,8 +1,8 @@  - net8.0;netstandard2.0 - 12.0 + net10.0;netstandard2.0 + 14.0 enable enable True @@ -16,8 +16,8 @@ MNieto EPlus multi-header report Extension for the EPPlus library to create reports from complex objects - https://github.com/mnieto/EPPlus.MultiHeader - https://github.com/mnieto/EPPlus.MultiHeader + https://github.com/mnieto/Kevull.MultiHeader + https://github.com/mnieto/Kevull.MultiHeader git EPPlus;Excel;Multi-header;Report LGPL-3.0-or-later @@ -28,15 +28,20 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + - <_Parameter1>Kevull.EPPLus.MultiHeader.Test + <_Parameter1>Kevull.MultiHeader.EPPLus.Tests diff --git a/src/Kevull.EPPLus.MultiHeader/MultiHeaderReport.cs b/src/Kevull.MultiHeader.EPPLus/MultiHeaderReport.cs similarity index 56% rename from src/Kevull.EPPLus.MultiHeader/MultiHeaderReport.cs rename to src/Kevull.MultiHeader.EPPLus/MultiHeaderReport.cs index 0f4e78b..5b1d87b 100644 --- a/src/Kevull.EPPLus.MultiHeader/MultiHeaderReport.cs +++ b/src/Kevull.MultiHeader.EPPLus/MultiHeaderReport.cs @@ -1,32 +1,38 @@ -using Kevull.EPPLus.MultiHeader.Columns; +using Kevull.MultiHeader.Core; +using Kevull.MultiHeader.Core.Columns; using OfficeOpenXml; -using OfficeOpenXml.Style; -using System.Drawing; using System.Linq; using System.Reflection; -namespace Kevull.EPPLus.MultiHeader +namespace Kevull.MultiHeader.EPPLus { /// /// Given an list of objects it creates an in-memory Excel report /// /// Type of objects - public class MultiHeaderReport + public class MultiHeaderReport : IMultiHeaderReport { private ExcelWorksheet _sheet; private ExcelPackage _xls; + private IExcelWriter _writer; private int FirstDataRow => (_header == null || !_header.AppendToExistingReport) ? _header?.FirstRow + _header?.Height ?? 2 : _sheet.Dimension.End.Row + 1; private int row; - + /// /// Internal /// protected HeaderManager? _header; - internal const string HeaderStyleName = "__Headers__"; + /// + /// Custom styles defined by the user to be applied to headers or columns. + /// The key is the style name and the value is the style definition agnostic format. See + /// + protected Dictionary _namedStyles = []; + + //internal const string HeaderStyleName = StyleNames.HeaderStyleName; /// /// Object properties associated to the columns @@ -42,6 +48,7 @@ public MultiHeaderReport(ExcelPackage xls, ExcelWorksheet sheet) { _xls = xls; _sheet = sheet; + _writer = new EPPlusExcelWriter(xls, sheet); } /// @@ -49,18 +56,19 @@ public MultiHeaderReport(ExcelPackage xls, ExcelWorksheet sheet) /// /// Initialized /// Worksheet name to be created where generate the report - public MultiHeaderReport(ExcelPackage xls, string sheetName): this(xls, AddSheet(xls, sheetName)) { } + public MultiHeaderReport(ExcelPackage xls, string sheetName) : this(xls, AddSheet(xls, sheetName)) { } /// /// Customize the columns and formats during the report generation. See . /// /// Lambda expresion to configure the report /// This allows a fluent style to configure and generate the report - public MultiHeaderReport Configure(Action> options) + public IMultiHeaderReport Configure(Action> options) { var builder = new ConfigurationBuilder(_xls); options?.Invoke(builder); _header = builder.Build(); + _namedStyles = builder.NamedStyles; return this; } @@ -76,7 +84,8 @@ public void GenerateReport(IEnumerable data) if (_header == null) { _header = new HeaderManager(); - } else + } + else { _header.BuildHeaders(); } @@ -93,7 +102,8 @@ public void GenerateReport(IEnumerable data) CalulateFormulas(); } - internal void Save(string fileName) + /// + public void Save(string fileName) { _xls.SaveAs(fileName); } @@ -117,7 +127,7 @@ private void ProcessRow(T item) } else { - columnInfo.WriteCell(_sheet.Cells[row, columnInfo.Index], Properties!, item!); + columnInfo.WriteCell(_writer, row, columnInfo.Index, Properties!, item!); } } row++; @@ -129,7 +139,7 @@ private void ProcessRow(HeaderManager header, object? item) return; if (header.Properties == null) throw new ArgumentNullException(nameof(header.Properties)); - foreach(var columnInfo in header.Columns) + foreach (var columnInfo in header.Columns) { if (columnInfo.HasChildren) { @@ -137,7 +147,7 @@ private void ProcessRow(HeaderManager header, object? item) } else { - columnInfo.WriteCell(_sheet.Cells[row, columnInfo.Index], header.Properties, item); + columnInfo.WriteCell(_writer, row, columnInfo.Index, header.Properties, item); } } @@ -149,9 +159,8 @@ private void WriteHeaders(HeaderManager? header = null, int? topRow = null) int row = topRow ?? _header!.FirstRow; foreach (var columnInfo in header.Columns) { - var cell = _sheet.Cells[row, columnInfo.Index]; - columnInfo.WriteHeader(cell); - columnInfo.FormatHeader(cell, columnInfo.HasChildren ? 1 : header.Height - (row - _header!.FirstRow)); + columnInfo.WriteHeader(_writer, row, columnInfo.Index); + columnInfo.FormatHeader(_writer, row, columnInfo.Index, columnInfo.HasChildren ? 1 : header.Height - (row - _header!.FirstRow)); if (columnInfo.HasChildren) { WriteHeaders(columnInfo.Header!, row + 1); @@ -162,46 +171,45 @@ private void WriteHeaders(HeaderManager? header = null, int? topRow = null) private void DoFormatting() { if (_header!.AutoFreezePanes) - _sheet.View.FreezePanes(_header.FirstRow + _header!.Height, _header.FirstColumn); + _writer.FreezePanes(_header.FirstRow + _header!.Height, _header.FirstColumn); //Hide columns if needed - foreach (var columnInfo in _header!.Columns.Where(x => x.Hidden || x.ColumnWidth.Type == WidthType.Hidden )) + foreach (var columnInfo in _header!.Columns.Where(x => x.Hidden || x.ColumnWidth.Type == WidthType.Hidden)) { - _sheet.Column(columnInfo.Index).Hidden = true; + _writer.SetColumnHidden(columnInfo.Index, true); } //Autofilter int lastHeaderRow = _header.FirstRow + _header.Height - 1; int lastHeaderColumn = _header.FirstColumn + _header.Width - 1; - _sheet.Cells[lastHeaderRow, _header!.Columns.Min(x => x.Index), lastHeaderRow, lastHeaderColumn].AutoFilter = _header.AutoFilter; + _writer.SetAutoFilter(lastHeaderRow, _header!.Columns.Min(x => x.Index), lastHeaderRow, lastHeaderColumn, _header.AutoFilter); //Width - foreach(var columnInfo in _header!.Columns.Where(x => x.ColumnWidth.Type == WidthType.Auto)) + foreach (var columnInfo in _header!.Columns.Where(x => x.ColumnWidth.Type == WidthType.Auto)) { double minWidth = columnInfo.ColumnWidth.MinimumWidth == double.MinValue ? _sheet.DefaultColWidth : columnInfo.ColumnWidth.MinimumWidth; double maxWidth = columnInfo.ColumnWidth.MaximunWidth; - _sheet.Column(columnInfo.Index).AutoFit(minWidth, maxWidth); + _writer.AutoFitColumn(columnInfo.Index, minWidth, maxWidth); } foreach (var columnInfo in _header!.Columns.Where(x => x.ColumnWidth.Type == WidthType.Custom)) { - _sheet.Column(columnInfo.Index).Width = columnInfo.ColumnWidth.Width!.Value; + _writer.SetColumnWidth(columnInfo.Index, columnInfo.ColumnWidth.Width!.Value); } //Styles - BuildDefaultHeaderStyle(); BuildDateStyle(); BuildTimeStyle(); + BuildColumnStyles(); if (!_header!.AppendToExistingReport) { - var rangeHeader = _sheet.Cells[_header.FirstRow, _header!.Columns.Min(x => x.Index), lastHeaderRow, lastHeaderColumn]; - rangeHeader.StyleName = StyleNames.HeaderStyleName; + BuildDefaultHeaderStyle(); + _writer.ApplyNamedStyle(_header.FirstRow, _header!.Columns.Min(x => x.Index), lastHeaderRow, lastHeaderColumn, StyleNames.HeaderStyleName); } foreach (var columnInfo in _header!.Columns.Where(x => x.StyleName != null)) { - var range = _sheet.Cells[FirstDataRow, columnInfo.Index, _sheet.Dimension.End.Row, columnInfo.Index]; - range.StyleName = columnInfo.StyleName; + _writer.ApplyNamedStyle(FirstDataRow, columnInfo.Index, _sheet.Dimension.End.Row, columnInfo.Index, columnInfo.StyleName!); } } @@ -210,57 +218,77 @@ private void CalulateFormulas() bool NeedsCalculate = false; foreach (var columnInfo in _header!.Columns.OfType()) { - var range = _sheet.Cells[FirstDataRow, columnInfo.Index, _sheet.Dimension.End.Row, columnInfo.Index]; - columnInfo.WriteCell(range, Properties!, null); - NeedsCalculate = true; + // Optimized: write formula to entire range at once instead of row by row + int lastRow = _sheet.Dimension?.End.Row ?? FirstDataRow; + if (lastRow >= FirstDataRow) + { + columnInfo.WriteCell(_writer, FirstDataRow, columnInfo.Index, lastRow, columnInfo.Index, Properties!, null); + NeedsCalculate = true; + } } if (NeedsCalculate) - _sheet.Calculate(); + _writer.Recalculate(); } private void BuildDefaultHeaderStyle() { - if (_xls.Workbook.Styles.NamedStyles.FirstOrDefault(x => x.Name == StyleNames.HeaderStyleName) == null) + if (!_writer.NamedStyleExists(StyleNames.HeaderStyleName)) { - var namedStyle = _xls.Workbook.Styles.CreateNamedStyle(StyleNames.HeaderStyleName); - namedStyle.Style.Border.Left.Style = ExcelBorderStyle.Thin; - namedStyle.Style.Border.Right.Style = ExcelBorderStyle.Thin; - namedStyle.Style.Border.Top.Style = ExcelBorderStyle.Thin; - namedStyle.Style.Border.Bottom.Style = ExcelBorderStyle.Thin; - namedStyle.Style.VerticalAlignment = ExcelVerticalAlignment.Center; - namedStyle.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; - namedStyle.Style.Fill.SetBackground(Color.LightGray, ExcelFillStyle.Solid); - namedStyle.Style.Font.Bold = true; + var format = new CellFormat + { + LeftBorder = BorderStyle.Thin, + RightBorder = BorderStyle.Thin, + TopBorder = BorderStyle.Thin, + BottomBorder = BorderStyle.Thin, + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Center, + BackgroundColor = ExcelColor.LightGray, + FillStyle = FillStyle.Solid, + Bold = true + }; + + //If the user has defined a custom header style, merge it with the default one + if (_namedStyles.ContainsKey(StyleNames.HeaderStyleName)) + format.Merge(_namedStyles[StyleNames.HeaderStyleName]); + _writer.CreateNamedStyle(StyleNames.HeaderStyleName, format); } } private void BuildDateStyle() { - if (_xls.Workbook.Styles.NamedStyles.FirstOrDefault(x => x.Name == StyleNames.DateStyleName) == null) + if (!_writer.NamedStyleExists(StyleNames.DateStyleName)) { - var namedStyle = _xls.Workbook.Styles.CreateNamedStyle(StyleNames.DateStyleName); - namedStyle.Style.Numberformat.Format = StyleNames.DateFormat; + var format = new CellFormat + { + NumberFormat = StyleNames.DateFormat + }; + _writer.CreateNamedStyle(StyleNames.DateStyleName, format); } } private void BuildTimeStyle() { - if (_xls.Workbook.Styles.NamedStyles.FirstOrDefault(x => x.Name == StyleNames.TimeStyleName) == null) + if (!_writer.NamedStyleExists(StyleNames.TimeStyleName)) { - var namedStyle = _xls.Workbook.Styles.CreateNamedStyle(StyleNames.TimeStyleName); - namedStyle.Style.Numberformat.Format = StyleNames.TimeFormat; + var format = new CellFormat + { + NumberFormat = StyleNames.TimeFormat + }; + _writer.CreateNamedStyle(StyleNames.TimeStyleName, format); } } - } - - internal class StyleNames - { - public const string HeaderStyleName = "__Headers__"; - public const string DateStyleName = "__date__"; - public const string TimeStyleName = "__time__"; + private void BuildColumnStyles() + { + foreach (var style in _namedStyles.Keys) + { + //The header sytyle is built separately and it is applied to all header rows, so we need to make sure it is not overridden by a user defined style with the same name. + if (style != StyleNames.HeaderStyleName && !_writer.NamedStyleExists(style)) + { + _writer.CreateNamedStyle(style, _namedStyles[style]); + } + } + } - internal const string TimeFormat = "[$-x-systime]h:mm:ss AM/PM"; //This format depends on local system settings - internal const string DateFormat = "mm-dd-yy"; //This format depends on local system settings } } \ No newline at end of file diff --git a/tests/Kevull.MultiHeader.ClosedXml.Tests/ClosedXmlExcelWriterTests.cs b/tests/Kevull.MultiHeader.ClosedXml.Tests/ClosedXmlExcelWriterTests.cs new file mode 100644 index 0000000..feff3ef --- /dev/null +++ b/tests/Kevull.MultiHeader.ClosedXml.Tests/ClosedXmlExcelWriterTests.cs @@ -0,0 +1,299 @@ +using ClosedXML.Excel; +using Kevull.MultiHeader.Core; + +namespace Kevull.MultiHeader.ClosedXml.Test +{ + /// + /// Unit tests for . + /// + public class ClosedXmlExcelWriterTests : IDisposable + { + private readonly XLWorkbook _workbook; + private readonly IXLWorksheet _worksheet; + private readonly ClosedXmlExcelWriter _writer; + + public ClosedXmlExcelWriterTests() + { + _workbook = new XLWorkbook(); + _worksheet = _workbook.Worksheets.Add("TestSheet"); + _writer = new ClosedXmlExcelWriter(_workbook, _worksheet); + } + + public void Dispose() + { + _workbook.Dispose(); + } + + [Fact] + public void ApplyNativeFormat_NullFormat_ReturnsWithoutError() + { + _writer.ApplyNativeFormat(1, 1, null!); + } + + [Fact] + public void ApplyNativeFormat_ActionModifiesRange_RangeIsModified() + { + _writer.ApplyNativeFormat(2, 3, range => + { + var xlRange = Assert.IsAssignableFrom(range); + xlRange.Value = "TestValue"; + }); + + Assert.Equal("TestValue", _worksheet.Cell(2, 3).GetValue()); + } + + [Fact] + public void ApplyNativeFormat_ActionParameterType_IsRange() + { + Type? capturedType = null; + + _writer.ApplyNativeFormat(1, 1, range => + { + capturedType = range?.GetType(); + }); + + Assert.NotNull(capturedType); + Assert.True(typeof(IXLRange).IsAssignableFrom(capturedType)); + } + + [Fact] + public void ApplyFormat_NullFormat_DoesNothing() + { + _writer.ApplyFormat(1, 1, 2, 2, null!); + + Assert.False(_worksheet.Cell(1, 1).Style.Font.Bold); + } + + [Fact] + public void ApplyFormat_SingleCell_AppliesFormat() + { + var format = new CellFormat + { + Bold = true, + FontSize = 14.0f, + HorizontalAlignment = HorizontalAlignment.Center + }; + + _writer.ApplyFormat(1, 1, 1, 1, format); + + var style = _worksheet.Cell(1, 1).Style; + Assert.True(style.Font.Bold); + Assert.Equal(14.0, style.Font.FontSize); + Assert.Equal(XLAlignmentHorizontalValues.Center, style.Alignment.Horizontal); + } + + [Fact] + public void ApplyFormat_MultiCellRange_AppliesFormatToAllCells() + { + var format = new CellFormat + { + Italic = true, + FontSize = 12.0f + }; + + _writer.ApplyFormat(1, 1, 3, 3, format); + + Assert.True(_worksheet.Cell(1, 1).Style.Font.Italic); + Assert.True(_worksheet.Cell(2, 2).Style.Font.Italic); + Assert.True(_worksheet.Cell(3, 3).Style.Font.Italic); + Assert.Equal(12.0, _worksheet.Cell(1, 1).Style.Font.FontSize); + Assert.Equal(12.0, _worksheet.Cell(2, 2).Style.Font.FontSize); + Assert.Equal(12.0, _worksheet.Cell(3, 3).Style.Font.FontSize); + } + + [Fact] + public void ApplyFormat_WithBorders_AppliesBorderStyles() + { + var format = new CellFormat + { + LeftBorder = BorderStyle.Thin, + RightBorder = BorderStyle.Thin, + TopBorder = BorderStyle.Thick, + BottomBorder = BorderStyle.Thick + }; + + _writer.ApplyFormat(2, 2, 4, 4, format); + + var border = _worksheet.Cell(2, 2).Style.Border; + Assert.Equal(XLBorderStyleValues.Thin, border.LeftBorder); + Assert.Equal(XLBorderStyleValues.Thin, border.RightBorder); + Assert.Equal(XLBorderStyleValues.Thick, border.TopBorder); + Assert.Equal(XLBorderStyleValues.Thick, border.BottomBorder); + } + + [Fact] + public void ApplyFormat_WithTextRotation_AppliesRotation() + { + var format = new CellFormat + { + TextRotation = 45 + }; + + _writer.ApplyFormat(1, 1, 1, 1, format); + + Assert.Equal(45, _worksheet.Cell(1, 1).Style.Alignment.TextRotation); + } + + [Fact] + public void ApplyFormat_WithWrapText_AppliesWrapText() + { + var format = new CellFormat + { + WrapText = true + }; + + _writer.ApplyFormat(1, 1, 3, 3, format); + + Assert.True(_worksheet.Cell(1, 1).Style.Alignment.WrapText); + Assert.True(_worksheet.Cell(2, 2).Style.Alignment.WrapText); + } + + [Fact] + public void ApplyFormat_WithNumberFormat_AppliesNumberFormat() + { + var format = new CellFormat + { + NumberFormat = "0.00" + }; + + _writer.ApplyFormat(1, 1, 2, 2, format); + + Assert.Equal("0.00", _worksheet.Cell(1, 1).Style.NumberFormat.Format); + } + + [Fact] + public void ApplyFormat_WithVerticalAlignment_AppliesAlignment() + { + var format = new CellFormat + { + VerticalAlignment = VerticalAlignment.Bottom + }; + + _writer.ApplyFormat(1, 1, 2, 2, format); + + Assert.Equal(XLAlignmentVerticalValues.Bottom, _worksheet.Cell(1, 1).Style.Alignment.Vertical); + } + + [Theory] + [InlineData("Calibri", 11d, "#FF0000")] + [InlineData("Arial", 12.5d, "112233")] + [InlineData("Consolas", 10d, "CC445566")] + public void ApplyFormat_WithFontFormat_AppliesFontFormat(string fontName, double fontSize, string fontColorHex) + { + var expectedColor = new Core.ExcelColor(fontColorHex); + var format = new CellFormat + { + FontName = fontName, + FontSize = (float)fontSize, + FontColor = expectedColor + }; + + _writer.ApplyFormat(1, 1, format); + + var style = _worksheet.Cell(1, 1).Style; + Assert.Equal(fontName, style.Font.FontName); + Assert.Equal(fontSize, style.Font.FontSize); + Assert.Equal(expectedColor.Argb, style.Font.FontColor.Color.ToArgb().ToString("X8")); + } + + [Theory] + [InlineData("FF55")] + [InlineData("GGHHII")] + [InlineData("#12345")] + [InlineData("#123456789")] + [InlineData(null)] + [InlineData("")] + public void ExcelColor_WithInvalidColor_ThowsException(string? invalidColor) + { + if (string.IsNullOrEmpty(invalidColor)) + { + Assert.Throws(() => new Core.ExcelColor(invalidColor!)); + } + else + { + Assert.Throws(() => new Core.ExcelColor(invalidColor)); + } + } + + [Fact] + public void WriteCell_SingleCellRange_SetsValue() + { + _writer.WriteCell(5, 3, 5, 3, 42); + + Assert.Equal(42, _worksheet.Cell(5, 3).GetValue()); + } + + [Fact] + public void WriteCell_NullValue_SetsNullOnAllCells() + { + _writer.WriteCell(1, 1, 2, 2, null); + + Assert.True(_worksheet.Cell(1, 1).IsEmpty()); + Assert.True(_worksheet.Cell(1, 2).IsEmpty()); + Assert.True(_worksheet.Cell(2, 1).IsEmpty()); + Assert.True(_worksheet.Cell(2, 2).IsEmpty()); + } + + [Fact] + public void WriteCell_HorizontalRange_SetsValueOnAllCells() + { + _writer.WriteCell(3, 1, 3, 5, "Horizontal"); + + for (int col = 1; col <= 5; col++) + { + Assert.Equal("Horizontal", _worksheet.Cell(3, col).GetValue()); + } + } + + [Fact] + public void WriteCell_VerticalRange_SetsValueOnAllCells() + { + _writer.WriteCell(1, 2, 4, 2, "Vertical"); + + for (int row = 1; row <= 4; row++) + { + Assert.Equal("Vertical", _worksheet.Cell(row, 2).GetValue()); + } + } + + [Fact] + public void CreateNamedStyle_NullStyleName_ThrowsArgumentNullException() + { + var exception = Assert.Throws(() => _writer.CreateNamedStyle(null!, new CellFormat())); + Assert.Equal("styleName", exception.ParamName); + } + + [Fact] + public void CreateNamedStyle_NullFormat_ThrowsArgumentNullException() + { + var exception = Assert.Throws(() => _writer.CreateNamedStyle("Style1", null!)); + Assert.Equal("format", exception.ParamName); + } + + [Fact] + public void CreateNamedStyle_ValidInputs_CreatesNamedStyle() + { + _writer.CreateNamedStyle("ValidStyle", new CellFormat { Bold = true }); + + Assert.True(_writer.NamedStyleExists("ValidStyle")); + } + + [Fact] + public void ApplyNamedStyle_UnknownStyle_ThrowsKeyNotFoundException() + { + Assert.Throws(() => _writer.ApplyNamedStyle(1, 1, 1, 1, "Missing")); + } + + [Fact] + public void ApplyNamedStyle_ValidStyleName_AppliesStyleToRange() + { + const string styleName = "MyStyle"; + _writer.CreateNamedStyle(styleName, new CellFormat { Bold = true }); + + _writer.ApplyNamedStyle(1, 1, 2, 2, styleName); + + Assert.True(_worksheet.Cell(1, 1).Style.Font.Bold); + Assert.True(_worksheet.Cell(2, 2).Style.Font.Bold); + } + } +} diff --git a/tests/Kevull.MultiHeader.ClosedXml.Tests/ColumnInfoTest.cs b/tests/Kevull.MultiHeader.ClosedXml.Tests/ColumnInfoTest.cs new file mode 100644 index 0000000..6bbde14 --- /dev/null +++ b/tests/Kevull.MultiHeader.ClosedXml.Tests/ColumnInfoTest.cs @@ -0,0 +1,109 @@ +using Kevull.MultiHeader.Core.Columns; + +namespace Kevull.MultiHeader.ClosedXml.Test +{ + public class ColumnInfoTest + { + [Fact] + public void ColumnEnumeration_Dicitionary_WritesOneColumPerKey() + { + using var workbook = new ClosedXML.Excel.XLWorkbook(); + var sheet = workbook.Worksheets.Add("Enummeration"); + var writer = new ClosedXmlExcelWriter(workbook, sheet); + + var data = new RiskDict + { + Name = "TestRisk", + Levels = new Dictionary + { + { "LOW", 10 }, + { "MED", 20 }, + { "HIGh", 30 } + } + }; + var properties = typeof(RiskDict).GetProperties().ToDictionary(x => x.Name, x => x); + + var column = new ColumnEnumeration>("Levels", data.Levels.Keys); + column.WriteCell(writer, 2, 2, properties, data); + + Assert.Equal(10, sheet.Cell(2, 2).GetValue()); + Assert.Equal(20, sheet.Cell(2, 3).GetValue()); + Assert.Equal(30, sheet.Cell(2, 4).GetValue()); + } + + [Fact] + public void ColumnEnumeration_Dicitionary_ThrowsWhenNotExpectedKey() + { + using var workbook = new ClosedXML.Excel.XLWorkbook(); + var sheet = workbook.Worksheets.Add("Enummeration"); + var writer = new ClosedXmlExcelWriter(workbook, sheet); + + var data = new RiskDict + { + Name = "TestRisk", + Levels = new Dictionary + { + { "LOW", 10 }, + { "MED", 20 }, + { "HIGh", 30 } + } + }; + var properties = typeof(RiskDict).GetProperties().ToDictionary(x => x.Name, x => x); + + var column = new ColumnEnumeration>("Levels", data.Levels.Keys.Take(2)); + Assert.Throws(() => column.WriteCell(writer, 2, 2, properties, data)); + } + + [Fact] + public void ColumnEnumeration_Enumberable_WritesOneColumPerKey() + { + using var workbook = new ClosedXML.Excel.XLWorkbook(); + var sheet = workbook.Worksheets.Add("Enummeration"); + var writer = new ClosedXmlExcelWriter(workbook, sheet); + + var data = new RiskList + { + Name = "TestRisk", + Levels = new List { 10, 20, 30 } + }; + var properties = typeof(RiskList).GetProperties().ToDictionary(x => x.Name, x => x); + + var column = new ColumnEnumeration>("Levels", data.Levels.ConvertAll(x => x.ToString())); + column.WriteCell(writer, 2, 2, properties, data); + + Assert.Equal(10, sheet.Cell(2, 2).GetValue()); + Assert.Equal(20, sheet.Cell(2, 3).GetValue()); + Assert.Equal(30, sheet.Cell(2, 4).GetValue()); + } + + [Fact] + public void ColumnEnumeration_Enumerable_ThrowsWhenNotExpectedKey() + { + using var workbook = new ClosedXML.Excel.XLWorkbook(); + var sheet = workbook.Worksheets.Add("Enummeration"); + var writer = new ClosedXmlExcelWriter(workbook, sheet); + + var data = new RiskList + { + Name = "TestRisk", + Levels = new List { 10, 20, 30 } + }; + var properties = typeof(RiskList).GetProperties().ToDictionary(x => x.Name, x => x); + + var column = new ColumnEnumeration>("Levels", data.Levels.ConvertAll(x => x.ToString()).Take(2)); + Assert.Throws(() => column.WriteCell(writer, 2, 2, properties, data)); + } + } + + internal class RiskDict + { + public string Name { get; set; } = ""; + public Dictionary Levels { get; set; } = new Dictionary(); + } + + internal class RiskList + { + public string Name { get; set; } = ""; + public List Levels { get; set; } = new List(); + } +} diff --git a/tests/Kevull.MultiHeader.ClosedXml.Tests/FormatTest.cs b/tests/Kevull.MultiHeader.ClosedXml.Tests/FormatTest.cs new file mode 100644 index 0000000..59791a8 --- /dev/null +++ b/tests/Kevull.MultiHeader.ClosedXml.Tests/FormatTest.cs @@ -0,0 +1,126 @@ +using Kevull.MultiHeader.Core; +using Kevull.MultiHeader.TestCommon; + +namespace Kevull.MultiHeader.ClosedXml.Test +{ + public class FormatTest : BaseTest + { + [Fact] + public void PropertiesWithoutChildren_HasVerticalMerge() + { + var complexObject = RootLevel.CreateTest(); + using var workbook = new ClosedXML.Excel.XLWorkbook(); + + var report = new MultiHeaderReport(workbook, "Object"); + report.GenerateReport(complexObject); + var sheet = workbook.Worksheet("Object"); + + Assert.True(sheet.Range("A1:A3").IsMerged()); + } + + [Fact] + public void Configuration_WithHeaderStyle_HasOverridenDefaultStyle() + { + var complexObject = RootLevelDictionary.CreateTest(); + using var workbook = new ClosedXML.Excel.XLWorkbook(); + + var report = new MultiHeaderReport(workbook, "Object"); + report.Configure(options => options + .AddEnumeration(x => x.ComplexProperty.RightColumn, complexObject.First().ComplexProperty.RightColumn.Keys) + .AddHeaderStyle(x => + { + x.HorizontalAlignment = HorizontalAlignment.Center; + x.VerticalAlignment = VerticalAlignment.Center; + x.BackgroundColor = ExcelColor.Black; + }) + ); + report.GenerateReport(complexObject); + var sheet = workbook.Worksheet("Object"); + + Assert.Equal(ClosedXML.Excel.XLAlignmentHorizontalValues.Center, sheet.Cell("A1").Style.Alignment.Horizontal); + Assert.Equal(ExcelColor.Black.Argb, sheet.Cell("A1").Style.Fill.BackgroundColor.Color.ToArgb().ToString("X8")); + } + + [Fact] + public void Headers_WithAutoFilter_SetAutoFilterInLeafLevelHeader() + { + var complexObject = RootLevel.CreateTest(); + using var workbook = new ClosedXML.Excel.XLWorkbook(); + + var report = new MultiHeaderReport(workbook, "Object"); + report.GenerateReport(complexObject); + var sheet = workbook.Worksheet("Object"); + + Assert.NotNull(sheet.AutoFilter.Range); + Assert.Equal(3, sheet.AutoFilter.Range.RangeAddress.FirstAddress.RowNumber); + Assert.Equal(1, sheet.AutoFilter.Range.RangeAddress.FirstAddress.ColumnNumber); + Assert.Equal(3, sheet.AutoFilter.Range.RangeAddress.LastAddress.RowNumber); + Assert.Equal(5, sheet.AutoFilter.Range.RangeAddress.LastAddress.ColumnNumber); + } + + [Fact] + public void DateOrTimeColumns_HasByDefault_DateTimeNumberFormat() + { + var people = Person.BuildPeopleList(); + using var workbook = new ClosedXML.Excel.XLWorkbook(); + + var report = new MultiHeaderReport(workbook, "People"); + report.GenerateReport(people); + var sheet = workbook.Worksheet("People"); + + int birthDateColumn = sheet.Row(1).CellsUsed().First(x => x.GetValue() == nameof(Person.BirthDate)).Address.ColumnNumber; + int alarmTimeColumn = sheet.Row(1).CellsUsed().First(x => x.GetValue() == nameof(Person.AlarmTime)).Address.ColumnNumber; + + Assert.Equal(StyleNames.DateFormat, sheet.Cell(2, birthDateColumn).Style.NumberFormat.Format); + Assert.Equal(StyleNames.TimeFormat, sheet.Cell(2, alarmTimeColumn).Style.NumberFormat.Format); + } + + [Fact] + public void DateColumns_WithAppliedStyle_HasSpecifiedFormat() + { + var people = Person.BuildPeopleList(); + using var workbook = new ClosedXML.Excel.XLWorkbook(); + + var report = new MultiHeaderReport(workbook, "People"); + report.Configure(options => options + .AddNamedStyle("BirthDay", s => + { + s.Italic = true; + s.NumberFormat = "dd/mm"; + }) + .AddColumn(x => x.BirthDate, styleName: "BirthDay") + ); + report.GenerateReport(people); + var sheet = workbook.Worksheet("People"); + + Assert.Equal("dd/mm", sheet.Cell("C2").Style.NumberFormat.Format); + Assert.True(sheet.Cell("C2").Style.Font.Italic); + } + + [Fact] + public void Columns_WithSpecifiedWidth_ApplyDefinedConfiguration() + { + var people = Person.BuildPeopleList(); + using var workbook = new ClosedXML.Excel.XLWorkbook(); + + var report = new MultiHeaderReport(workbook, "People"); + report.Configure(options => options + .AddColumn(x => x.Name, cfg => + cfg.ColumnWidth.SetWidth(WidthType.Auto)) + .AddColumn(x => x.Surname, cfg => + cfg.ColumnWidth.SetWidth(8.0)) + .AddColumn(x => x.BirthDate, cfg => + cfg.ColumnWidth.SetWidth(WidthType.Hidden)) + .AddColumn(x => x.NumOfComputers, cfg => + cfg.ColumnWidth.SetWidth(WidthType.Auto, 12.0, 20.0)) + ); + report.GenerateReport(people); + var sheet = workbook.Worksheet("People"); + + Assert.NotEqual(sheet.ColumnWidth, sheet.Column(1).Width); + Assert.Equal(8.0, sheet.Column(2).Width); + Assert.True(sheet.Column(3).IsHidden); + Assert.InRange(sheet.Column(5).Width, 12.0, 20.0); + } + } +} diff --git a/tests/Kevull.MultiHeader.ClosedXml.Tests/GeneralConfigurationOptionsTest.cs b/tests/Kevull.MultiHeader.ClosedXml.Tests/GeneralConfigurationOptionsTest.cs new file mode 100644 index 0000000..d893411 --- /dev/null +++ b/tests/Kevull.MultiHeader.ClosedXml.Tests/GeneralConfigurationOptionsTest.cs @@ -0,0 +1,83 @@ +using Kevull.MultiHeader.TestCommon; + +namespace Kevull.MultiHeader.ClosedXml.Test +{ + public class GeneralConfigurationOptionsTest : BaseTest + { + [Fact] + public void ReportStatsAt_TopLeftStartingPoint() + { + var people = Person.BuildPeopleList(); + using var workbook = new ClosedXML.Excel.XLWorkbook(); + + var report = new MultiHeaderReport(workbook, "People"); + report.Configure(config => + config.SetStartingAddres(3, 2) + ); + report.GenerateReport(people); + + var sheet = workbook.Worksheet("People"); + Assert.Equal("Name", sheet.Cell(3, 2).GetValue()); + Assert.Equal("Médiamass", sheet.Cell(4, 2).GetValue()); + + Assert.True(sheet.Cell(3, 8).Style.Font.Bold); + Assert.NotNull(sheet.AutoFilter.Range); + Assert.Equal(3, sheet.AutoFilter.Range.RangeAddress.FirstAddress.RowNumber); + Assert.Equal(2, sheet.AutoFilter.Range.RangeAddress.FirstAddress.ColumnNumber); + Assert.Equal(3, sheet.AutoFilter.Range.RangeAddress.LastAddress.RowNumber); + Assert.Equal(8, sheet.AutoFilter.Range.RangeAddress.LastAddress.ColumnNumber); + } + + [Fact] + public void Report_WithAppendToExistingReport_AppendsNewRowsAtBottom() + { + var people = Person.BuildPeopleList(); + using var workbook = new ClosedXML.Excel.XLWorkbook(); + + var report = new MultiHeaderReport(workbook, "People"); + report.GenerateReport(people); + + people = Person.BuildPeopleList(2, 3); + report = new MultiHeaderReport(workbook, "People"); + report.Configure(config => + config.AppendToExistingReport = true + ); + report.GenerateReport(people); + + var sheet = workbook.Worksheet("People"); + Assert.Equal("Michelle", sheet.Cell(4, 1).GetValue()); + } + + [Fact] + public void Report_WithAutoFreeze_ProperlyFreezes() + { + var people = Person.BuildPeopleList(); + using var workbook = new ClosedXML.Excel.XLWorkbook(); + var report = new MultiHeaderReport(workbook, "People"); + report.Configure(config => + config.AutoFreezePanes = true + ); + report.GenerateReport(people); + var sheet = workbook.Worksheet("People"); + + Assert.Equal(1, sheet.SheetView.SplitRow); + Assert.Equal(0, sheet.SheetView.SplitColumn); + } + + [Fact] + public void Report_WithoutAutoFreeze_DoNotHasFrozenPanes() + { + var people = Person.BuildPeopleList(); + using var workbook = new ClosedXML.Excel.XLWorkbook(); + var report = new MultiHeaderReport(workbook, "People"); + report.Configure(config => + config.AutoFreezePanes = false + ); + report.GenerateReport(people); + var sheet = workbook.Worksheet("People"); + + Assert.Equal(0, sheet.SheetView.SplitRow); + Assert.Equal(0, sheet.SheetView.SplitColumn); + } + } +} diff --git a/tests/Kevull.MultiHeader.ClosedXml.Tests/Kevull.MultiHeader.ClosedXml.Tests.csproj b/tests/Kevull.MultiHeader.ClosedXml.Tests/Kevull.MultiHeader.ClosedXml.Tests.csproj new file mode 100644 index 0000000..5ffb3ae --- /dev/null +++ b/tests/Kevull.MultiHeader.ClosedXml.Tests/Kevull.MultiHeader.ClosedXml.Tests.csproj @@ -0,0 +1,31 @@ + + + + net10.0 + enable + enable + true + all + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/tests/Kevull.MultiHeader.ClosedXml.Tests/OneHeaderRenderTest.cs b/tests/Kevull.MultiHeader.ClosedXml.Tests/OneHeaderRenderTest.cs new file mode 100644 index 0000000..cec6380 --- /dev/null +++ b/tests/Kevull.MultiHeader.ClosedXml.Tests/OneHeaderRenderTest.cs @@ -0,0 +1,131 @@ +using Kevull.MultiHeader.TestCommon; + +namespace Kevull.MultiHeader.ClosedXml.Test +{ + public class OneHeaderRenderTest : BaseTest + { + private readonly int _maxColumns; + + public OneHeaderRenderTest() + { + _maxColumns = typeof(Person).GetProperties().Length; + } + + [Fact] + public void Write2Rows() + { + var people = Person.BuildPeopleList(); + using var workbook = new ClosedXML.Excel.XLWorkbook(); + + var report = new MultiHeaderReport(workbook, "People"); + report.GenerateReport(people); + + var sheet = workbook.Worksheet("People"); + Assert.Equal(_maxColumns, sheet.LastColumnUsed()!.ColumnNumber()); + Assert.Equal(3, sheet.LastRowUsed()!.RowNumber()); + + Assert.Equal(nameof(Person.NumOfComputers), sheet.Cell(1, 5).GetValue()); + Assert.Equal(Gender.Female.ToString(), sheet.Cell(3, 4).GetValue()); + Assert.True(sheet.Cell(2, 5).IsEmpty()); + Assert.Equal(2, sheet.Cell(3, 5).GetValue()); + Assert.Equal("https://github.com/", sheet.Cell(3, 6).GetValue()); + } + + [Fact] + public void Config_SetupOrder_ColumnsAreOrdered() + { + var people = Person.BuildPeopleList(); + using var workbook = new ClosedXML.Excel.XLWorkbook(); + var report = new MultiHeaderReport(workbook, "People"); + + report.Configure(options => options + .AddColumn(x => x.NumOfComputers, 1) + ).GenerateReport(people); + + var sheet = workbook.Worksheet("People"); + Assert.Equal(nameof(Person.NumOfComputers), sheet.Cell(1, 1).GetValue()); + Assert.Equal(nameof(Person.Name), sheet.Cell(1, 2).GetValue()); + } + + [Fact] + public void Config_IgnoredColumns_AreNotInTheList() + { + var people = Person.BuildPeopleList(); + using var workbook = new ClosedXML.Excel.XLWorkbook(); + var report = new MultiHeaderReport(workbook, "People"); + + report.Configure(options => options + .AddColumn(x => x.Surname, 1) + .IgnoreColumn(x => x.NumOfComputers) + ).GenerateReport(people); + + var sheet = workbook.Worksheet("People"); + Assert.Equal(_maxColumns - 1, sheet.LastColumnUsed()!.ColumnNumber()); + } + + [Fact] + public void HiddenColumns_AreRendered_AsHidden() + { + var people = Person.BuildPeopleList(); + using var workbook = new ClosedXML.Excel.XLWorkbook(); + var report = new MultiHeaderReport(workbook, "People"); + + report.Configure(options => options + .AddColumn(x => x.NumOfComputers, hidden: true) + ).GenerateReport(people); + + var sheet = workbook.Worksheet("People"); + Assert.True(sheet.Column(5).IsHidden); + } + + [Fact] + public void HyperLinkColumns_UseAntherColumnTo_BuildTheLink() + { + var people = Person.BuildPeopleList(); + using var workbook = new ClosedXML.Excel.XLWorkbook(); + var report = new MultiHeaderReport(workbook, "People"); + + report.Configure(options => options + .AddHyperLinkColumn(x => x.Name, x => x.Profile) + .IgnoreColumn(x => x.Profile) + ).GenerateReport(people); + + var sheet = workbook.Worksheet("People"); + Assert.True(sheet.Cell(3, 1).HasHyperlink); + } + + [Fact] + public void FormulaColumns_Write_Formulas() + { + var people = Person.BuildPeopleList(); + using var workbook = new ClosedXML.Excel.XLWorkbook(); + + var report = new MultiHeaderReport(workbook, "People"); + report.Configure(options => options + .AddColumn(x => x.Name, 1) + .AddColumn(x => x.Surname, 2) + .AddFormula("CompleteName", "CONCATENATE(B2,\", \",A2)", 3) + ).GenerateReport(people); + + var sheet = workbook.Worksheet("People"); + Assert.Contains("CONCATENATE", sheet.Cell(2, 3).FormulaA1); + } + + [Fact] + public void ExpressionColumns_Write_ExpressionResults() + { + var people = Person.BuildPeopleList(); + using var workbook = new ClosedXML.Excel.XLWorkbook(); + + var report = new MultiHeaderReport(workbook, "People"); + report.Configure(options => options + .AddColumn(x => x.Name, 1) + .AddColumn(x => x.Surname, 2) + .AddExpression("Initials", x => string.Concat(x.Name[0], '.', x.Surname[0], '.'), 3) + ).GenerateReport(people); + + var sheet = workbook.Worksheet("People"); + Assert.Equal("A.B.", sheet.Cell(3, 3).GetValue()); + } + } +} diff --git a/tests/Kevull.MultiHeader.ClosedXml.Tests/TwoHeaderRenderTest.cs b/tests/Kevull.MultiHeader.ClosedXml.Tests/TwoHeaderRenderTest.cs new file mode 100644 index 0000000..00d46f2 --- /dev/null +++ b/tests/Kevull.MultiHeader.ClosedXml.Tests/TwoHeaderRenderTest.cs @@ -0,0 +1,80 @@ +using Kevull.MultiHeader.TestCommon; + +namespace Kevull.MultiHeader.ClosedXml.Test +{ + public class TwoHeaderRenderTest : BaseTest + { + [Fact] + public void ComposedObjects_AreRendered_InSecondRow() + { + var complexObject = RootLevel.CreateTest(); + using var workbook = new ClosedXML.Excel.XLWorkbook(); + + var report = new MultiHeaderReport(workbook, "Object"); + report.GenerateReport(complexObject); + var sheet = workbook.Worksheet("Object"); + + Assert.Equal(nameof(RootLevel.SimpleProperty), sheet.Cell(1, 1).GetValue()); + Assert.Equal(nameof(RootLevel.ComplexProperty), sheet.Cell(1, 2).GetValue()); + Assert.True(sheet.Cell(1, 3).IsEmpty()); + + Assert.True(sheet.Cell(2, 1).IsEmpty()); + Assert.Equal(nameof(RootLevel.ComplexProperty.LeftColumn), sheet.Cell(2, 2).GetValue()); + Assert.Equal(nameof(RootLevel.ComplexProperty.RightColumn), sheet.Cell(2, 3).GetValue()); + Assert.True(sheet.Cell(2, 4).IsEmpty()); + + Assert.True(sheet.Cell(3, 1).IsEmpty()); + Assert.True(sheet.Cell(3, 2).IsEmpty()); + Assert.Equal(nameof(RootLevel.ComplexProperty.RightColumn.CatA), sheet.Cell(3, 3).GetValue()); + + Assert.Equal("String1", sheet.Cell(4, 1).GetValue()); + Assert.Equal("Left side 1", sheet.Cell(4, 2).GetValue()); + Assert.Equal(11, sheet.Cell(4, 3).GetValue()); + Assert.Equal(12, sheet.Cell(4, 4).GetValue()); + Assert.Equal(13, sheet.Cell(4, 5).GetValue()); + } + + [Fact] + public void ComposedObjects_WithEnumerables_NeedsToBeConfigured() + { + var complexObject = RootLevelDictionary.CreateTest(); + using var workbook = new ClosedXML.Excel.XLWorkbook(); + + var report = new MultiHeaderReport(workbook, "Object"); + Assert.Throws(() => report.GenerateReport(complexObject)); + } + + [Fact] + public void ComposedObjects_WithEnumerables_HasWithEqualsToCountOfKeys() + { + var complexObject = RootLevelDictionary.CreateTest(); + using var workbook = new ClosedXML.Excel.XLWorkbook(); + + var report = new MultiHeaderReport(workbook, "Object"); + report.Configure(options => + options.AddEnumeration(x => x.ComplexProperty.RightColumn, complexObject.First().ComplexProperty.RightColumn.Keys) + ); + report.GenerateReport(complexObject); + var sheet = workbook.Worksheet("Object"); + + Assert.Equal(nameof(RootLevel.SimpleProperty), sheet.Cell(1, 1).GetValue()); + Assert.Equal(nameof(RootLevel.ComplexProperty), sheet.Cell(1, 2).GetValue()); + Assert.True(sheet.Cell(1, 3).IsEmpty()); + + Assert.True(sheet.Cell(2, 1).IsEmpty()); + Assert.Equal(nameof(RootLevel.ComplexProperty.LeftColumn), sheet.Cell(2, 2).GetValue()); + Assert.Equal(nameof(RootLevel.ComplexProperty.RightColumn), sheet.Cell(2, 3).GetValue()); + Assert.True(sheet.Cell(2, 4).IsEmpty()); + + Assert.True(sheet.Cell(3, 1).IsEmpty()); + Assert.True(sheet.Cell(3, 2).IsEmpty()); + Assert.Equal(nameof(RootLevel.ComplexProperty.RightColumn.CatA), sheet.Cell(3, 3).GetValue()); + + Assert.Equal("String1", sheet.Cell(4, 1).GetValue()); + Assert.Equal("Left side 1", sheet.Cell(4, 2).GetValue()); + Assert.Equal(11, sheet.Cell(4, 3).GetValue()); + Assert.Equal(12, sheet.Cell(4, 4).GetValue()); + Assert.Equal(13, sheet.Cell(4, 5).GetValue()); + } + } +} diff --git a/tests/Kevull.MultiHeader.ClosedXml.Tests/Usings.cs b/tests/Kevull.MultiHeader.ClosedXml.Tests/Usings.cs new file mode 100644 index 0000000..6819282 --- /dev/null +++ b/tests/Kevull.MultiHeader.ClosedXml.Tests/Usings.cs @@ -0,0 +1,2 @@ +global using Xunit; +global using Kevull.MultiHeader.TestCommon; diff --git a/tests/Kevull.MultiHeader.Core.Tests/ColumnInfoTest.cs b/tests/Kevull.MultiHeader.Core.Tests/ColumnInfoTest.cs new file mode 100644 index 0000000..5eea44c --- /dev/null +++ b/tests/Kevull.MultiHeader.Core.Tests/ColumnInfoTest.cs @@ -0,0 +1,39 @@ +using Kevull.MultiHeader.Core.Columns; +using Kevull.MultiHeader.TestCommon; +using System; +using System.Linq; + +namespace Kevull.MultiHeader.Core.Tests +{ + public class ColumnInfoTest + { + [Fact] + public void Order_MustBeOneOrUpper() + { + var sut = new ColumnInfo(nameof(Person.Name)); + Action act = () => sut.Order = 0; + Assert.Throws(act); + } + + [Fact] + public void DisplayName_IsName_IfNotAssigned() + { + var sut = new ColumnInfo(nameof(Person.BirthDate)); + Assert.Equal(sut.Name, sut.DisplayName); + } + + [Fact] + public void Deep_InDirectProprties_IsOne() + { + var sut = new ColumnInfo(x => x.SimpleProperty); + Assert.Equal(1, sut.Deep); + } + + [Fact] + public void Deep_InDirectChildProperties_IsTwo() + { + var sut = new ColumnInfo(x => x.ComplexProperty.RightColumn); + Assert.Equal(2, sut.Deep); + } + } +} diff --git a/src/Kevull.EPPLus.MultiHeader.Test/HeaderManagerTest.cs b/tests/Kevull.MultiHeader.Core.Tests/HeaderManagerTest.cs similarity index 90% rename from src/Kevull.EPPLus.MultiHeader.Test/HeaderManagerTest.cs rename to tests/Kevull.MultiHeader.Core.Tests/HeaderManagerTest.cs index 39abf58..c73f1c4 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/HeaderManagerTest.cs +++ b/tests/Kevull.MultiHeader.Core.Tests/HeaderManagerTest.cs @@ -1,11 +1,13 @@ -using System; +using Kevull.MultiHeader.Core; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection.PortableExecutable; using System.Text; using System.Threading.Tasks; +using Kevull.MultiHeader.TestCommon; -namespace Kevull.EPPLus.MultiHeader.Test +namespace Kevull.MultiHeader.Core.Tests { diff --git a/src/Kevull.EPPLus.MultiHeader.Test/Kevull.EPPLus.MultiHeader.Test.csproj b/tests/Kevull.MultiHeader.Core.Tests/Kevull.MultiHeader.Core.Tests.csproj similarity index 76% rename from src/Kevull.EPPLus.MultiHeader.Test/Kevull.EPPLus.MultiHeader.Test.csproj rename to tests/Kevull.MultiHeader.Core.Tests/Kevull.MultiHeader.Core.Tests.csproj index 1948bda..49c156e 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/Kevull.EPPLus.MultiHeader.Test.csproj +++ b/tests/Kevull.MultiHeader.Core.Tests/Kevull.MultiHeader.Core.Tests.csproj @@ -1,18 +1,16 @@  - net8.0 + net10.0 enable enable - true - all false - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -23,7 +21,9 @@ - + + + diff --git a/src/Kevull.EPPLus.MultiHeader.Test/PropertyNameBuilderTest.cs b/tests/Kevull.MultiHeader.Core.Tests/PropertyNameBuilderTest.cs similarity index 94% rename from src/Kevull.EPPLus.MultiHeader.Test/PropertyNameBuilderTest.cs rename to tests/Kevull.MultiHeader.Core.Tests/PropertyNameBuilderTest.cs index 5aacd16..c0dd70a 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/PropertyNameBuilderTest.cs +++ b/tests/Kevull.MultiHeader.Core.Tests/PropertyNameBuilderTest.cs @@ -1,11 +1,12 @@ -using Kevull.EPPLus.MultiHeader.Columns; +using Kevull.MultiHeader.Core.Columns; +using Kevull.MultiHeader.TestCommon; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Test +namespace Kevull.MultiHeader.Core.Tests { public class PropertyNameBuilderTest { diff --git a/tests/Kevull.MultiHeader.Core.Tests/Usings.cs b/tests/Kevull.MultiHeader.Core.Tests/Usings.cs new file mode 100644 index 0000000..c802f44 --- /dev/null +++ b/tests/Kevull.MultiHeader.Core.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/src/Kevull.EPPLus.MultiHeader.Test/ColumnInfoTest.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/ColumnInfoTest.cs similarity index 72% rename from src/Kevull.EPPLus.MultiHeader.Test/ColumnInfoTest.cs rename to tests/Kevull.MultiHeader.EPPLus.Tests/ColumnInfoTest.cs index 3472025..a13ce3d 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/ColumnInfoTest.cs +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/ColumnInfoTest.cs @@ -1,53 +1,21 @@ -using Kevull.EPPLus.MultiHeader.Columns; +using Kevull.MultiHeader.Core.Columns; using OfficeOpenXml; using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Xml.Linq; -namespace Kevull.EPPLus.MultiHeader.Test +namespace Kevull.MultiHeader.EPPLus.Test { public class ColumnInfoTest : BaseTest { - [Fact] - public void Order_MustBeOneOrUpper() - { - var property = typeof(Person).GetProperties().First(x => x.Name == nameof(Person.Name)); - var sut = new ColumnInfo(nameof(Person.Name)); - Action act = () => sut.Order = 0; - Assert.Throws(act); - } - - [Fact] - public void DisplayName_IsName_IfNotAssigned() - { - var property = typeof(Person).GetProperties().First(x => x.Name == nameof(Person.BirthDate)); - var sut = new ColumnInfo(nameof(Person.BirthDate)); - Assert.Equal(sut.Name, sut.DisplayName); - } - - [Fact] - public void Deep_InDirectProprties_IsOne() - { - var sut = new ColumnInfo(x => x.SimpleProperty); - Assert.Equal(1, sut.Deep); - } - - [Fact] - public void Deep_InDirectChildProperties_IsTwo() - { - var sut = new ColumnInfo(x => x.ComplexProperty.RightColumn); - Assert.Equal(2, sut.Deep); - } - [Fact] public void ColumnEnumeration_Dicitionary_WritesOneColumPerKey() { var xls = new ExcelPackage(); xls.Workbook.Worksheets.Add("Enummeration"); var sheet = xls.Workbook.Worksheets["Enummeration"]; + var writer = new EPPlusExcelWriter(xls, sheet); + var data = new RiskDict { Name = "TestRisk", @@ -62,7 +30,7 @@ public void ColumnEnumeration_Dicitionary_WritesOneColumPerKey() .ToDictionary(x => x.Name, x => x); var column = new ColumnEnumeration>("Levels", data.Levels.Keys); - column.WriteCell(sheet.Cells["B2"], properties, data); + column.WriteCell(writer, 2, 2, properties, data); Assert.Equal(10, sheet.GetValue(2, 2)); Assert.Equal(20, sheet.GetValue(2, 3)); @@ -76,6 +44,8 @@ public void ColumnEnumeration_Dicitionary_ThrowsWhenNotExpectedKey() var xls = new ExcelPackage(); xls.Workbook.Worksheets.Add("Enummeration"); var sheet = xls.Workbook.Worksheets["Enummeration"]; + var writer = new EPPlusExcelWriter(xls, sheet); + var data = new RiskDict { Name = "TestRisk", @@ -90,7 +60,7 @@ public void ColumnEnumeration_Dicitionary_ThrowsWhenNotExpectedKey() .ToDictionary(x => x.Name, x => x); var column = new ColumnEnumeration>("Levels", data.Levels.Keys.Take(2)); - Assert.Throws(() => column.WriteCell(sheet.Cells["B2"], properties, data)); + Assert.Throws(() => column.WriteCell(writer, 2, 2, properties, data)); } [Fact] @@ -99,6 +69,8 @@ public void ColumnEnumeration_Enumberable_WritesOneColumPerKey() var xls = new ExcelPackage(); xls.Workbook.Worksheets.Add("Enummeration"); var sheet = xls.Workbook.Worksheets["Enummeration"]; + var writer = new EPPlusExcelWriter(xls, sheet); + var data = new RiskList { Name = "TestRisk", @@ -108,7 +80,7 @@ public void ColumnEnumeration_Enumberable_WritesOneColumPerKey() .ToDictionary(x => x.Name, x => x); var column = new ColumnEnumeration>("Levels", data.Levels.ConvertAll(x => x.ToString())); - column.WriteCell(sheet.Cells["B2"], properties, data); + column.WriteCell(writer, 2, 2, properties, data); Assert.Equal(10, sheet.GetValue(2, 2)); Assert.Equal(20, sheet.GetValue(2, 3)); @@ -122,6 +94,8 @@ public void ColumnEnumeration_Enumerable_ThrowsWhenNotExpectedKey() var xls = new ExcelPackage(); xls.Workbook.Worksheets.Add("Enummeration"); var sheet = xls.Workbook.Worksheets["Enummeration"]; + var writer = new EPPlusExcelWriter(xls, sheet); + var data = new RiskList { Name = "TestRisk", @@ -131,7 +105,7 @@ public void ColumnEnumeration_Enumerable_ThrowsWhenNotExpectedKey() .ToDictionary(x => x.Name, x => x); var column = new ColumnEnumeration>("Levels", data.Levels.ConvertAll(x => x.ToString()).Take(2)); - Assert.Throws(() => column.WriteCell(sheet.Cells["B2"], properties, data)); + Assert.Throws(() => column.WriteCell(writer, 2, 2, properties, data)); } } diff --git a/tests/Kevull.MultiHeader.EPPLus.Tests/EPPLusBaseTest.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/EPPLusBaseTest.cs new file mode 100644 index 0000000..bf0656f --- /dev/null +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/EPPLusBaseTest.cs @@ -0,0 +1,13 @@ +using OfficeOpenXml; +using System.Reflection; + +namespace Kevull.MultiHeader.EPPLus.Test +{ + public class EPPLusBaseTest : BaseTest + { + public EPPLusBaseTest() + { + ExcelPackage.License.SetNonCommercialPersonal("Kevull"); + } + } +} \ No newline at end of file diff --git a/tests/Kevull.MultiHeader.EPPLus.Tests/EPPlusExcelWriterTests.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/EPPlusExcelWriterTests.cs new file mode 100644 index 0000000..c740947 --- /dev/null +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/EPPlusExcelWriterTests.cs @@ -0,0 +1,1044 @@ +using Kevull.MultiHeader.Core; +using OfficeOpenXml; +using OfficeOpenXml.Style; +using OfficeOpenXml.Style.XmlAccess; +using System; +using System.Collections.Generic; +using System.Linq; + + +namespace Kevull.MultiHeader.EPPLus.Test +{ + /// + /// Unit tests for EPPlusExcelWriter class + /// + public partial class EPPlusExcelWriterTests : BaseTest, IDisposable + { + private readonly ExcelPackage _package; + private readonly ExcelWorksheet _worksheet; + private readonly EPPlusExcelWriter _writer; + + public EPPlusExcelWriterTests() + { + _package = new ExcelPackage(); + _worksheet = _package.Workbook.Worksheets.Add("TestSheet"); + _writer = new EPPlusExcelWriter(_package, _worksheet); + } + + public void Dispose() + { + _package?.Dispose(); + } + + /// + /// Tests that ApplyNativeFormat with null format parameter returns without error + /// Input: row=1, col=1, format=null + /// Expected: Method returns without throwing exception + /// + [Fact] + public void ApplyNativeFormat_NullFormat_ReturnsWithoutError() + { + // Arrange + var xls = new ExcelPackage(); + xls.Workbook.Worksheets.Add("NullFormatTest"); + var sheet = xls.Workbook.Worksheets["NullFormatTest"]; + var writer = new EPPlusExcelWriter(xls, sheet); + + // Act & Assert - should not throw + writer.ApplyNativeFormat(1, 1, null!); + } + + /// + /// Tests that ApplyNativeFormat allows the action to modify the cell + /// Input: row=2, col=3, action that sets cell value to "TestValue" + /// Expected: Cell C2 contains "TestValue" after action execution + /// + [Fact] + public void ApplyNativeFormat_ActionModifiesCell_CellIsModified() + { + // Arrange + var xls = new ExcelPackage(); + xls.Workbook.Worksheets.Add("ModifyTest"); + var sheet = xls.Workbook.Worksheets["ModifyTest"]; + var writer = new EPPlusExcelWriter(xls, sheet); + + // Act + writer.ApplyNativeFormat(2, 3, range => + { + var excelRange = (ExcelRange)range; + excelRange.Value = "TestValue"; + }); + + // Assert + Assert.Equal("TestValue", sheet.Cells[2, 3].Value); + } + + /// + /// Tests that ApplyNativeFormat passes ExcelRange as object type to the action + /// Input: row=1, col=1, action expecting object parameter + /// Expected: Action receives an object that is an ExcelRange instance + /// + [Fact] + public void ApplyNativeFormat_ActionParameterType_IsObject() + { + // Arrange + var xls = new ExcelPackage(); + xls.Workbook.Worksheets.Add("TypeTest"); + var sheet = xls.Workbook.Worksheets["TypeTest"]; + var writer = new EPPlusExcelWriter(xls, sheet); + Type? capturedType = null; + + // Act + writer.ApplyNativeFormat(1, 1, range => + { + capturedType = range?.GetType(); + }); + + // Assert + Assert.NotNull(capturedType); + Assert.Equal(typeof(ExcelRange), capturedType); + } + + /// + /// Tests that ApplyFormat with null format parameter returns early without throwing exceptions. + /// Input: null format + /// Expected: Method returns without error, no formatting applied + /// + [Fact] + public void ApplyFormat_NullFormat_DoesNothing() + { + // Arrange + var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, sheet); + + // Act + writer.ApplyFormat(1, 1, 2, 2, null!); + + // Assert + // No exception should be thrown, and the cells should remain unformatted + Assert.False(sheet.Cells[1, 1].Style.Font.Bold); + } + + /// + /// Tests that ApplyFormat applies the format to a single cell. + /// Input: Single cell coordinates with valid format + /// Expected: Format is applied to the specified cell + /// + [Fact] + public void ApplyFormat_SingleCell_AppliesFormat() + { + // Arrange + var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, sheet); + var format = new CellFormat + { + Bold = true, + FontSize = 14.0f, + HorizontalAlignment = HorizontalAlignment.Center + }; + + // Act + writer.ApplyFormat(1, 1, 1, 1, format); + + // Assert + Assert.True(sheet.Cells[1, 1].Style.Font.Bold); + Assert.Equal(14.0f, sheet.Cells[1, 1].Style.Font.Size); + Assert.Equal(ExcelHorizontalAlignment.Center, sheet.Cells[1, 1].Style.HorizontalAlignment); + } + + /// + /// Tests that ApplyFormat applies the format to a range of cells. + /// Input: Multi-cell range coordinates with valid format + /// Expected: Format is applied to all cells in the range + /// + [Fact] + public void ApplyFormat_MultiCellRange_AppliesFormatToAllCells() + { + // Arrange + var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, sheet); + var format = new CellFormat + { + Italic = true, + FontSize = 12.0f + }; + + // Act + writer.ApplyFormat(1, 1, 3, 3, format); + + // Assert + Assert.True(sheet.Cells[1, 1].Style.Font.Italic); + Assert.True(sheet.Cells[2, 2].Style.Font.Italic); + Assert.True(sheet.Cells[3, 3].Style.Font.Italic); + Assert.Equal(12.0f, sheet.Cells[1, 1].Style.Font.Size); + Assert.Equal(12.0f, sheet.Cells[2, 2].Style.Font.Size); + Assert.Equal(12.0f, sheet.Cells[3, 3].Style.Font.Size); + } + + /// + /// Tests that ApplyFormat applies border styles correctly. + /// Input: Format with border styles specified + /// Expected: Border styles are applied to the range + /// + [Fact] + public void ApplyFormat_WithBorders_AppliesBorderStyles() + { + // Arrange + var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, sheet); + var format = new CellFormat + { + LeftBorder = BorderStyle.Thin, + RightBorder = BorderStyle.Thin, + TopBorder = BorderStyle.Thick, + BottomBorder = BorderStyle.Thick + }; + + // Act + writer.ApplyFormat(2, 2, 4, 4, format); + + // Assert + Assert.Equal(ExcelBorderStyle.Thin, sheet.Cells[2, 2].Style.Border.Left.Style); + Assert.Equal(ExcelBorderStyle.Thin, sheet.Cells[2, 2].Style.Border.Right.Style); + Assert.Equal(ExcelBorderStyle.Thick, sheet.Cells[2, 2].Style.Border.Top.Style); + Assert.Equal(ExcelBorderStyle.Thick, sheet.Cells[2, 2].Style.Border.Bottom.Style); + } + + /// + /// Tests that ApplyFormat applies text rotation correctly. + /// Input: Format with text rotation + /// Expected: Text rotation is applied to the range + /// + [Fact] + public void ApplyFormat_WithTextRotation_AppliesRotation() + { + // Arrange + var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, sheet); + var format = new CellFormat + { + TextRotation = 45 + }; + + // Act + writer.ApplyFormat(1, 1, 1, 1, format); + + // Assert + Assert.Equal(45, sheet.Cells[1, 1].Style.TextRotation); + } + + /// + /// Tests that ApplyFormat applies wrap text setting correctly. + /// Input: Format with wrap text enabled + /// Expected: Wrap text is applied to the range + /// + [Fact] + public void ApplyFormat_WithWrapText_AppliesWrapText() + { + // Arrange + var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, sheet); + var format = new CellFormat + { + WrapText = true + }; + + // Act + writer.ApplyFormat(1, 1, 3, 3, format); + + // Assert + Assert.True(sheet.Cells[1, 1].Style.WrapText); + Assert.True(sheet.Cells[2, 2].Style.WrapText); + } + + /// + /// Tests that ApplyFormat applies number format correctly. + /// Input: Format with number format string + /// Expected: Number format is applied to the range + /// + [Fact] + public void ApplyFormat_WithNumberFormat_AppliesNumberFormat() + { + // Arrange + var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, sheet); + var format = new CellFormat + { + NumberFormat = "0.00" + }; + + // Act + writer.ApplyFormat(1, 1, 2, 2, format); + + // Assert + Assert.Equal("0.00", sheet.Cells[1, 1].Style.Numberformat.Format); + } + + /// + /// Tests that ApplyFormat applies vertical alignment correctly. + /// Input: Format with vertical alignment + /// Expected: Vertical alignment is applied to the range + /// + [Fact] + public void ApplyFormat_WithVerticalAlignment_AppliesAlignment() + { + // Arrange + var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, sheet); + var format = new CellFormat + { + VerticalAlignment = VerticalAlignment.Bottom + }; + + // Act + writer.ApplyFormat(1, 1, 2, 2, format); + + // Assert + Assert.Equal(ExcelVerticalAlignment.Bottom, sheet.Cells[1, 1].Style.VerticalAlignment); + } + + /// + /// Tests that ApplyFormat with empty format object does not throw exceptions. + /// Input: Empty CellFormat object with no properties set + /// Expected: Method executes without error + /// + [Fact] + public void ApplyFormat_EmptyFormat_DoesNotThrow() + { + // Arrange + var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, sheet); + var format = new CellFormat(); + + // Act & Assert + writer.ApplyFormat(1, 1, 2, 2, format); + // No exception expected + } + + /// + /// Tests that ApplyFormat handles null format parameter gracefully by delegating + /// to the range-based overload which returns early without applying any format. + /// + [Fact] + public void ApplyFormat_NullFormat_HandlesGracefully() + { + // Arrange + var package = new ExcelPackage(); + var worksheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, worksheet); + int row = 1; + int col = 1; + + // Act + writer.ApplyFormat(row, col, null!); + + // Assert + // When format is null, the overload returns early, so no exception should be thrown + // and no format should be applied (verify default state) + var cell = worksheet.Cells[row, col]; + Assert.False(cell.Style.Font.Bold); + + package.Dispose(); + } + + /// + /// Tests that ApplyFormat correctly applies various format properties by delegating + /// to the range-based overload for a single cell. + /// + [Fact] + public void ApplyFormat_ComplexFormat_AppliesAllProperties() + { + // Arrange + var package = new ExcelPackage(); + var worksheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, worksheet); + int row = 2; + int col = 3; + var format = new CellFormat + { + Bold = true, + Italic = true, + FontSize = 14, + WrapText = true, + TextRotation = 45, + FontColor = Core.ExcelColor.Red + }; + + // Act + writer.ApplyFormat(row, col, format); + + // Assert + var cell = worksheet.Cells[row, col]; + Assert.True(cell.Style.Font.Bold); + Assert.True(cell.Style.Font.Italic); + Assert.Equal(14, cell.Style.Font.Size); + Assert.True(cell.Style.WrapText); + Assert.Equal(45, cell.Style.TextRotation); + Assert.Equal(cell.Style.Font.Color.Rgb, Core.ExcelColor.Red.Argb); + + package.Dispose(); + } + + /// + /// Tests that ApplyFormat applies font family, size and color combinations correctly. + /// + [Theory] + [InlineData("Calibri", 11d, "#FF0000")] + [InlineData("Arial", 12.5d, "112233")] + [InlineData("Consolas", 10d, "CC445566")] + public void ApplyFormat_WithFontFormat_AppliesFontFormat(string fontName, double fontSize, string fontColorHex) + { + // Arrange + using var package = new ExcelPackage(); + var worksheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, worksheet); + var expectedColor = new Core.ExcelColor(fontColorHex); + var format = new CellFormat + { + FontName = fontName, + FontSize = (float)fontSize, + FontColor = expectedColor + }; + + // Act + writer.ApplyFormat(1, 1, format); + + // Assert + var cell = worksheet.Cells[1, 1]; + Assert.Equal(fontName, cell.Style.Font.Name); + Assert.Equal((float)fontSize, cell.Style.Font.Size); + Assert.Equal(expectedColor.Argb, cell.Style.Font.Color.Rgb); + } + + /// + /// Tests that ApplyFormat with boundary row and column values delegates correctly + /// without throwing exceptions for typical Excel ranges. + /// + [Theory] + [InlineData(1, 1)] + [InlineData(1, 16384)] + [InlineData(1048576, 1)] + public void ApplyFormat_TypicalExcelBoundaries_AppliesFormat(int row, int col) + { + // Arrange + var package = new ExcelPackage(); + var worksheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, worksheet); + var format = new CellFormat + { + Bold = true + }; + + // Act + writer.ApplyFormat(row, col, format); + + // Assert + var cell = worksheet.Cells[row, col]; + Assert.True(cell.Style.Font.Bold); + + package.Dispose(); + } + + [Theory] + [InlineData("FF55")] //Invalid hex color string (too short) + [InlineData("GGHHII")] //Invalid hex color string (non-hex characters) + [InlineData("#12345")] //Invalid hex color string (too short with #) + [InlineData("#123456789")] //Invalid hex color string (too long with #) + [InlineData(null)] //Null hex color string + [InlineData("")] //Empty hex color string + public void ExcelColor_WithInvalidColor_ThowsException(string? invalidColor) + { + // Arrange & Act & Assert + if (string.IsNullOrEmpty(invalidColor)) + { + Assert.Throws(() => new Core.ExcelColor(invalidColor!)); + } + else + { + Assert.Throws(() => new Core.ExcelColor(invalidColor)); + } + } + + /// + /// Tests that AutoFitColumn executes successfully with a valid column index. + /// Input: Valid column index (1) + /// Expected: Method executes without throwing an exception + /// + [Fact] + public void AutoFitColumn_ValidColumnIndex_DoesNotThrow() + { + // Arrange + using var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, sheet); + sheet.Cells[1, 1].Value = "Test Data"; + + // Act & Assert + var exception = Record.Exception(() => writer.AutoFitColumn(1)); + Assert.Null(exception); + } + + /// + /// Tests that AutoFitColumn handles zero column index. + /// Input: Column index of 0 + /// Expected: Throws ArgumentException + /// + [Fact] + public void AutoFitColumn_ZeroColumnIndex_ThrowsException() + { + // Arrange + using var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, sheet); + + // Act & Assert + Assert.Throws(() => writer.AutoFitColumn(0)); + } + + /// + /// Tests that AutoFitColumn works correctly on empty column. + /// Input: Valid column index with no data + /// Expected: Method executes without throwing (AutoFit on empty column is valid) + /// + [Fact] + public void AutoFitColumn_EmptyColumn_DoesNotThrow() + { + // Arrange + using var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, sheet); + + // Act & Assert + var exception = Record.Exception(() => writer.AutoFitColumn(1)); + Assert.Null(exception); + } + + /// + /// Tests that ApplyNamedStyle throws ArgumentNullException when styleName is null. + /// + [Fact] + public void ApplyNamedStyle_NullStyleName_ThrowsArgumentNullException() + { + // Arrange + using var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, sheet); + const int row = 1; + const int col = 1; + + // Act & Assert + var exception = Assert.Throws(() => writer.ApplyNamedStyle(row, col, null!)); + Assert.Equal("styleName", exception.ParamName); + } + + /// + /// Tests that ApplyNamedStyle throws ArgumentNullException when styleName is empty. + /// + [Fact] + public void ApplyNamedStyle_EmptyStyleName_ThrowsArgumentNullException() + { + // Arrange + using var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, sheet); + const int row = 1; + const int col = 1; + + // Act & Assert + var exception = Assert.Throws(() => writer.ApplyNamedStyle(row, col, string.Empty)); + Assert.Equal("styleName", exception.ParamName); + } + + /// + /// Tests that ApplyNativeFormat allows the format action to modify the ExcelRange + /// properties such as style. + /// + [Fact] + public void ApplyNativeFormat_ValidFormat_ActionCanModifyRange() + { + // Arrange + var xls = new ExcelPackage(); + xls.Workbook.Worksheets.Add("TestSheet"); + var sheet = xls.Workbook.Worksheets["TestSheet"]; + var writer = new EPPlusExcelWriter(xls, sheet); + + Action formatAction = (range) => + { + var excelRange = (ExcelRange)range; + excelRange.Style.Font.Bold = true; + }; + + // Act + writer.ApplyNativeFormat(1, 1, 2, 2, formatAction); + + // Assert + Assert.True(sheet.Cells[1, 1].Style.Font.Bold); + Assert.True(sheet.Cells[2, 2].Style.Font.Bold); + } + + /// + /// Tests that NamedStyleExists returns false when the named style does not exist in the workbook. + /// This test validates the search logic when the style is not present in the collection. + /// + [Fact] + public void NamedStyleExists_StyleDoesNotExist_ReturnsFalse() + { + // Arrange + var package = new ExcelPackage(); + package.Workbook.Worksheets.Add("TestSheet"); + var sheet = package.Workbook.Worksheets["TestSheet"]; + var writer = new EPPlusExcelWriter(package, sheet); + + // Act + var result = writer.NamedStyleExists("NonExistentStyle"); + + // Assert + Assert.False(result); + } + + /// + /// Tests that NamedStyleExists returns true when the named style exists in the workbook. + /// This test validates the search logic when the style is present in the collection. + /// + [Fact] + public void NamedStyleExists_StyleExists_ReturnsTrue() + { + // Arrange + var package = new ExcelPackage(); + package.Workbook.Worksheets.Add("TestSheet"); + var sheet = package.Workbook.Worksheets["TestSheet"]; + var writer = new EPPlusExcelWriter(package, sheet); + package.Workbook.Styles.CreateNamedStyle("TestStyle"); + + // Act + var result = writer.NamedStyleExists("TestStyle"); + + // Assert + Assert.True(result); + } + + /// + /// Tests that NamedStyleExists returns false when the style name does not match exactly (case sensitivity). + /// This test validates that the search is case-sensitive. + /// + [Fact] + public void NamedStyleExists_StyleExistsWithDifferentCase_ReturnsFalse() + { + // Arrange + var package = new ExcelPackage(); + package.Workbook.Worksheets.Add("TestSheet"); + var sheet = package.Workbook.Worksheets["TestSheet"]; + var writer = new EPPlusExcelWriter(package, sheet); + package.Workbook.Styles.CreateNamedStyle("TestStyle"); + + // Act + var result = writer.NamedStyleExists("teststyle"); + + // Assert + Assert.False(result); + } + + /// + /// Tests that NamedStyleExists works correctly with multiple named styles in the collection. + /// This test validates the search logic when multiple styles exist. + /// + [Fact] + public void NamedStyleExists_MultipleStyles_ReturnsCorrectResult() + { + // Arrange + var package = new ExcelPackage(); + package.Workbook.Worksheets.Add("TestSheet"); + var sheet = package.Workbook.Worksheets["TestSheet"]; + var writer = new EPPlusExcelWriter(package, sheet); + package.Workbook.Styles.CreateNamedStyle("Style1"); + package.Workbook.Styles.CreateNamedStyle("Style2"); + package.Workbook.Styles.CreateNamedStyle("Style3"); + + // Act + var result1 = writer.NamedStyleExists("Style1"); + var result2 = writer.NamedStyleExists("Style2"); + var result3 = writer.NamedStyleExists("Style3"); + var result4 = writer.NamedStyleExists("Style4"); + + // Assert + Assert.True(result1); + Assert.True(result2); + Assert.True(result3); + Assert.False(result4); + } + + /// + /// Tests that NamedStyleExists handles style names with special characters correctly. + /// This test validates behavior with non-alphanumeric style names. + /// + [Theory] + [InlineData("Style-1")] + [InlineData("Style_2")] + [InlineData("Style.3")] + [InlineData("Style@4")] + [InlineData("Style#5")] + public void NamedStyleExists_StyleNameWithSpecialCharacters_WorksCorrectly(string styleName) + { + // Arrange + var package = new ExcelPackage(); + package.Workbook.Worksheets.Add("TestSheet"); + var sheet = package.Workbook.Worksheets["TestSheet"]; + var writer = new EPPlusExcelWriter(package, sheet); + package.Workbook.Styles.CreateNamedStyle(styleName); + + // Act + var result = writer.NamedStyleExists(styleName); + + // Assert + Assert.True(result); + } + + /// + /// Tests that WriteCell with a single cell range sets the value correctly. + /// Input: A single cell range (fromRow == toRow, fromCol == toCol) with an integer value. + /// Expected: The single cell contains the specified value. + /// + [Fact] + public void WriteCell_SingleCellRange_SetsValue() + { + // Arrange + using var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, sheet); + var value = 42; + int row = 5, col = 3; + + // Act + writer.WriteCell(row, col, row, col, value); + + // Assert + Assert.Equal(value, sheet.GetValue(5, 3)); + } + + /// + /// Tests that WriteCell with a null value sets null on all cells in the range. + /// Input: A multi-cell range with null value. + /// Expected: All cells in the range have null values. + /// + [Fact] + public void WriteCell_NullValue_SetsNullOnAllCells() + { + // Arrange + using var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, sheet); + int fromRow = 1, fromCol = 1, toRow = 2, toCol = 2; + + // Act + writer.WriteCell(fromRow, fromCol, toRow, toCol, null); + + // Assert + Assert.Null(sheet.GetValue(1, 1)); + Assert.Null(sheet.GetValue(1, 2)); + Assert.Null(sheet.GetValue(2, 1)); + Assert.Null(sheet.GetValue(2, 2)); + } + + /// + /// Tests that WriteCell correctly handles various value types. + /// Input: Different value types (string, int, double, DateTime, bool). + /// Expected: Each value type is correctly set on the range. + /// + [Theory] + [InlineData("StringValue")] + [InlineData(123)] + [InlineData(45.67)] + [InlineData(true)] + [InlineData(0)] + [InlineData(-999)] + public void WriteCell_VariousValueTypes_SetsCorrectly(object value) + { + // Arrange + using var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, sheet); + int fromRow = 1, fromCol = 1, toRow = 1, toCol = 1; + + // Act + writer.WriteCell(fromRow, fromCol, toRow, toCol, value); + + // Assert + Assert.Equal(value, sheet.GetValue(1, 1)); + } + + /// + /// Tests that WriteCell with a horizontal range sets the value on all cells in the row. + /// Input: A horizontal range spanning multiple columns in a single row. + /// Expected: All cells in the horizontal range contain the specified value. + /// + [Fact] + public void WriteCell_HorizontalRange_SetsValueOnAllCells() + { + // Arrange + using var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, sheet); + var value = "Horizontal"; + int row = 3, fromCol = 1, toCol = 5; + + // Act + writer.WriteCell(row, fromCol, row, toCol, value); + + // Assert + for (int col = fromCol; col <= toCol; col++) + { + Assert.Equal(value, sheet.GetValue(row, col)); + } + } + + /// + /// Tests that WriteCell with a vertical range sets the value on all cells in the column. + /// Input: A vertical range spanning multiple rows in a single column. + /// Expected: All cells in the vertical range contain the specified value. + /// + [Fact] + public void WriteCell_VerticalRange_SetsValueOnAllCells() + { + // Arrange + using var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, sheet); + var value = "Vertical"; + int col = 2, fromRow = 1, toRow = 4; + + // Act + writer.WriteCell(fromRow, col, toRow, col, value); + + // Assert + for (int row = fromRow; row <= toRow; row++) + { + Assert.Equal(value, sheet.GetValue(row, col)); + } + } + + /// + /// Tests that WriteCell with an empty string sets empty string on all cells. + /// Input: A range with an empty string value. + /// Expected: All cells in the range contain an empty string. + /// + [Fact] + public void WriteCell_EmptyString_SetsEmptyStringOnAllCells() + { + // Arrange + using var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, sheet); + var value = string.Empty; + int fromRow = 1, fromCol = 1, toRow = 2, toCol = 2; + + // Act + writer.WriteCell(fromRow, fromCol, toRow, toCol, value); + + // Assert + Assert.Equal(value, sheet.GetValue(1, 1)); + Assert.Equal(value, sheet.GetValue(1, 2)); + Assert.Equal(value, sheet.GetValue(2, 1)); + Assert.Equal(value, sheet.GetValue(2, 2)); + } + + /// + /// Tests that WriteCell with special numeric values sets them correctly. + /// Input: Special numeric values including zero, negative values, and decimal values. + /// Expected: Each special value is correctly set on the cells. + /// + [Theory] + [InlineData(0.0)] + [InlineData(-1.5)] + [InlineData(double.MaxValue)] + [InlineData(double.MinValue)] + public void WriteCell_SpecialNumericValues_SetsCorrectly(double value) + { + // Arrange + using var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("TestSheet"); + var writer = new EPPlusExcelWriter(package, sheet); + int fromRow = 1, fromCol = 1, toRow = 1, toCol = 1; + + // Act + writer.WriteCell(fromRow, fromCol, toRow, toCol, value); + + // Assert + Assert.Equal(value, sheet.GetValue(1, 1)); + } + + /// + /// Tests that CreateNamedStyle throws ArgumentNullException when styleName is null. + /// Input: null styleName, valid format. + /// Expected: ArgumentNullException with parameter name "styleName". + /// + [Fact] + public void CreateNamedStyle_NullStyleName_ThrowsArgumentNullException() + { + // Arrange + string? styleName = null; + CellFormat format = new CellFormat(); + + // Act & Assert + ArgumentNullException exception = Assert.Throws(() => _writer.CreateNamedStyle(styleName!, format)); + Assert.Equal("styleName", exception.ParamName); + } + + /// + /// Tests that CreateNamedStyle throws ArgumentNullException when styleName is empty. + /// Input: empty string styleName, valid format. + /// Expected: ArgumentNullException with parameter name "styleName". + /// + [Fact] + public void CreateNamedStyle_EmptyStyleName_ThrowsArgumentNullException() + { + // Arrange + string styleName = string.Empty; + CellFormat format = new CellFormat(); + + // Act & Assert + ArgumentNullException exception = Assert.Throws(() => _writer.CreateNamedStyle(styleName, format)); + Assert.Equal("styleName", exception.ParamName); + } + + /// + /// Tests that CreateNamedStyle throws ArgumentNullException when format is null. + /// Input: valid styleName, null format. + /// Expected: ArgumentNullException with parameter name "format". + /// + [Fact] + public void CreateNamedStyle_NullFormat_ThrowsArgumentNullException() + { + // Arrange + string styleName = "TestStyle"; + CellFormat? format = null; + + // Act & Assert + ArgumentNullException exception = Assert.Throws(() => _writer.CreateNamedStyle(styleName, format!)); + Assert.Equal("format", exception.ParamName); + } + + /// + /// Tests that CreateNamedStyle returns early without creating a style when the style already exists. + /// Input: valid styleName that already exists, valid format. + /// Expected: Method returns without error, no new style is created. + /// + [Fact] + public void CreateNamedStyle_StyleAlreadyExists_ReturnsEarly() + { + // Arrange + string styleName = "ExistingStyle"; + CellFormat format = new CellFormat { Bold = true }; + + // Create the style first + _writer.CreateNamedStyle(styleName, format); + int initialStyleCount = _package.Workbook.Styles.NamedStyles.Count(); + + // Act - try to create the same style again + _writer.CreateNamedStyle(styleName, format); + + // Assert - style count should remain the same + int finalStyleCount = _package.Workbook.Styles.NamedStyles.Count(); + Assert.Equal(initialStyleCount, finalStyleCount); + } + + /// + /// Tests that CreateNamedStyle successfully creates a named style with valid inputs. + /// Input: valid styleName, valid format. + /// Expected: Named style is created and exists in the workbook. + /// + [Fact] + public void CreateNamedStyle_ValidInputs_CreatesNamedStyle() + { + // Arrange + string styleName = "ValidStyle"; + CellFormat format = new CellFormat { Bold = true }; + + // Act + _writer.CreateNamedStyle(styleName, format); + + // Assert + Assert.True(_writer.NamedStyleExists(styleName)); + ExcelNamedStyleXml? createdStyle = _package.Workbook.Styles.NamedStyles.FirstOrDefault(s => s.Name == styleName); + Assert.NotNull(createdStyle); + } + + /// + /// Tests that CreateNamedStyle handles empty CellFormat (no formatting properties set). + /// Input: valid styleName, empty CellFormat. + /// Expected: Named style is created without errors. + /// + [Fact] + public void CreateNamedStyle_EmptyFormat_CreatesStyleWithoutErrors() + { + // Arrange + string styleName = "EmptyFormatStyle"; + CellFormat format = new CellFormat(); + + // Act + _writer.CreateNamedStyle(styleName, format); + + // Assert + Assert.True(_writer.NamedStyleExists(styleName)); + } + + /// + /// Tests that ApplyNamedStyle correctly sets the StyleName property on a cell range. + /// This test validates the normal operation with a valid style name. + /// Expected: The StyleName property is set to the provided value + /// + [Fact] + public void ApplyNamedStyle_ValidStyleName_SetsStyleNameOnRange() + { + // Arrange + using var package = new ExcelPackage(); + package.Workbook.Worksheets.Add("Test"); + var sheet = package.Workbook.Worksheets["Test"]; + var writer = new EPPlusExcelWriter(package, sheet); + string expectedStyleName = "MyStyle"; + writer.CreateNamedStyle(expectedStyleName, new CellFormat()); + + // Act + writer.ApplyNamedStyle(1, 1, 2, 2, expectedStyleName); + + // Assert + Assert.Equal(expectedStyleName, sheet.Cells[1, 1, 2, 2].StyleName); + } + + + /// + /// Tests that ApplyNamedStyle handles style names with special characters correctly. + /// This test validates that various valid string values are correctly assigned. + /// Expected: The StyleName is set to the exact value provided, including special characters + /// + [Theory] + [InlineData("Style1")] + [InlineData("My-Style")] + [InlineData("Style_123")] + [InlineData("StyleWithNumbers123")] + [InlineData("Style.Name")] + public void ApplyNamedStyle_SpecialCharactersInStyleName_SetsStyleNameCorrectly(string styleName) + { + // Arrange + using var package = new ExcelPackage(); + package.Workbook.Worksheets.Add("Test"); + var sheet = package.Workbook.Worksheets["Test"]; + var writer = new EPPlusExcelWriter(package, sheet); + writer.CreateNamedStyle(styleName, new CellFormat()); + + // Act + writer.ApplyNamedStyle(1, 1, 2, 2, styleName); + + // Assert + Assert.Equal(styleName, sheet.Cells[1, 1, 2, 2].StyleName); + } + } +} \ No newline at end of file diff --git a/src/Kevull.EPPLus.MultiHeader.Test/FormatTest.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/FormatTest.cs similarity index 88% rename from src/Kevull.EPPLus.MultiHeader.Test/FormatTest.cs rename to tests/Kevull.MultiHeader.EPPLus.Tests/FormatTest.cs index 7acf53e..a2d3501 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/FormatTest.cs +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/FormatTest.cs @@ -1,15 +1,17 @@ -using OfficeOpenXml; +using Kevull.MultiHeader.Core; +using OfficeOpenXml; using System; using System.Collections.Generic; +using Kevull.MultiHeader.TestCommon; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using Xunit; -namespace Kevull.EPPLus.MultiHeader.Test +namespace Kevull.MultiHeader.EPPLus.Test { - public class FormatTest : BaseTest + public class FormatTest : EPPLusBaseTest { [Fact] public void PropertiesWithoutChildren_HasVerticalMerge() @@ -35,15 +37,16 @@ public void Configuration_WithHeaderStyle_HasOverridenDefaultStyle() .AddEnumeration(x => x.ComplexProperty.RightColumn, complexObject.First().ComplexProperty.RightColumn.Keys) .AddHeaderStyle(x => { - x.HorizontalAlignment = OfficeOpenXml.Style.ExcelHorizontalAlignment.Center; - x.VerticalAlignment = OfficeOpenXml.Style.ExcelVerticalAlignment.Center; + x.HorizontalAlignment = HorizontalAlignment.Center; + x.VerticalAlignment = VerticalAlignment.Center; + x.BackgroundColor = ExcelColor.Black; }) ); report.GenerateReport(complexObject); var sheet = xls.Workbook.Worksheets["Object"]; Assert.Equal(OfficeOpenXml.Style.ExcelHorizontalAlignment.Center, sheet.Cells["A1"].Style.HorizontalAlignment); - Assert.NotEqual(Color.LightGray.ToArgb().ToString("X"), sheet.Cells["A1"].Style.Fill.BackgroundColor.Rgb); + Assert.Equal(ExcelColor.Black.Argb, sheet.Cells["A1"].Style.Fill.BackgroundColor.Rgb); } [Fact] @@ -83,8 +86,8 @@ public void DateColumns_WithAppliedStyle_HasSpecifiedFormat() report.Configure(options => options .AddNamedStyle("BirthDay", s => { - s.Font.Italic = true; - s.Numberformat.Format = "dd/mm"; + s.Italic = true; + s.NumberFormat = "dd/mm"; }) .AddColumn(x => x.BirthDate, styleName: "BirthDay") ); diff --git a/src/Kevull.EPPLus.MultiHeader.Test/GeneralConfigurationOptionsTest.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/GeneralConfigurationOptionsTest.cs similarity index 95% rename from src/Kevull.EPPLus.MultiHeader.Test/GeneralConfigurationOptionsTest.cs rename to tests/Kevull.MultiHeader.EPPLus.Tests/GeneralConfigurationOptionsTest.cs index fcdf5f6..903b1f3 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/GeneralConfigurationOptionsTest.cs +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/GeneralConfigurationOptionsTest.cs @@ -3,10 +3,11 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Kevull.MultiHeader.TestCommon; -namespace Kevull.EPPLus.MultiHeader.Test +namespace Kevull.MultiHeader.EPPLus.Test { - public class GeneralConfigurationOptionsTest : BaseTest + public class GeneralConfigurationOptionsTest : EPPLusBaseTest { [Fact] public void ReportStatsAt_TopLeftStartingPoint() diff --git a/tests/Kevull.MultiHeader.EPPLus.Tests/Kevull.MultiHeader.EPPLus.Tests.csproj b/tests/Kevull.MultiHeader.EPPLus.Tests/Kevull.MultiHeader.EPPLus.Tests.csproj new file mode 100644 index 0000000..0f8ed23 --- /dev/null +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/Kevull.MultiHeader.EPPLus.Tests.csproj @@ -0,0 +1,31 @@ + + + + net10.0 + enable + enable + true + all + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/src/Kevull.EPPLus.MultiHeader.Test/OneHeaderRenderTest.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/OneHeaderRenderTest.cs similarity index 97% rename from src/Kevull.EPPLus.MultiHeader.Test/OneHeaderRenderTest.cs rename to tests/Kevull.MultiHeader.EPPLus.Tests/OneHeaderRenderTest.cs index 77f37c4..ec0cf86 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/OneHeaderRenderTest.cs +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/OneHeaderRenderTest.cs @@ -1,10 +1,11 @@ using System.Xml.Linq; using System; using OfficeOpenXml; +using Kevull.MultiHeader.TestCommon; -namespace Kevull.EPPLus.MultiHeader.Test +namespace Kevull.MultiHeader.EPPLus.Test { - public class OneHeaderRenderTest : BaseTest + public class OneHeaderRenderTest : EPPLusBaseTest { private int maxColumns; public OneHeaderRenderTest() : base() diff --git a/src/Kevull.EPPLus.MultiHeader.Test/TwoHeaderRenderTest.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/TwoHeaderRenderTest.cs similarity index 96% rename from src/Kevull.EPPLus.MultiHeader.Test/TwoHeaderRenderTest.cs rename to tests/Kevull.MultiHeader.EPPLus.Tests/TwoHeaderRenderTest.cs index 36f7c3f..7dc822c 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/TwoHeaderRenderTest.cs +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/TwoHeaderRenderTest.cs @@ -6,10 +6,11 @@ using System.Text; using System.Threading.Tasks; using Xunit; +using Kevull.MultiHeader.TestCommon; -namespace Kevull.EPPLus.MultiHeader.Test +namespace Kevull.MultiHeader.EPPLus.Test { - public class TwoHeaderRenderTest : BaseTest + public class TwoHeaderRenderTest : EPPLusBaseTest { [Fact] public void ComposedObjects_AreRendered_InSecondRow() diff --git a/tests/Kevull.MultiHeader.EPPLus.Tests/Usings.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/Usings.cs new file mode 100644 index 0000000..7884853 --- /dev/null +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/Usings.cs @@ -0,0 +1,3 @@ +global using Xunit; +global using OfficeOpenXml; +global using Kevull.MultiHeader.TestCommon; \ No newline at end of file diff --git a/src/Kevull.EPPLus.MultiHeader.Test/BaseTest.cs b/tests/Kevull.MultiHeader.TestCommon/BaseTest.cs similarity index 60% rename from src/Kevull.EPPLus.MultiHeader.Test/BaseTest.cs rename to tests/Kevull.MultiHeader.TestCommon/BaseTest.cs index 28a8f5c..77e3740 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/BaseTest.cs +++ b/tests/Kevull.MultiHeader.TestCommon/BaseTest.cs @@ -1,16 +1,12 @@ -using OfficeOpenXml; +using Kevull.MultiHeader.Core; +using System.IO; using System.Reflection; -namespace Kevull.EPPLus.MultiHeader.Test +namespace Kevull.MultiHeader.TestCommon { public class BaseTest { - public BaseTest() - { - ExcelPackage.License.SetNonCommercialPersonal("Kevull"); - } - - protected void Save(MultiHeaderReport report, [System.Runtime.CompilerServices.CallerMemberName] string methodName = "") + protected void Save(IMultiHeaderReport report, [System.Runtime.CompilerServices.CallerMemberName] string methodName = "") { report.Save(string.Concat(methodName, ".xlsx")); } diff --git a/src/Kevull.EPPLus.MultiHeader.Test/ComplexObject.cs b/tests/Kevull.MultiHeader.TestCommon/ComplexObject.cs similarity index 98% rename from src/Kevull.EPPLus.MultiHeader.Test/ComplexObject.cs rename to tests/Kevull.MultiHeader.TestCommon/ComplexObject.cs index 5697c83..7d9f566 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/ComplexObject.cs +++ b/tests/Kevull.MultiHeader.TestCommon/ComplexObject.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Test +namespace Kevull.MultiHeader.TestCommon { internal class RootLevel { diff --git a/tests/Kevull.MultiHeader.TestCommon/Kevull.MultiHeader.TestCommon.csproj b/tests/Kevull.MultiHeader.TestCommon/Kevull.MultiHeader.TestCommon.csproj new file mode 100644 index 0000000..d819f6b --- /dev/null +++ b/tests/Kevull.MultiHeader.TestCommon/Kevull.MultiHeader.TestCommon.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + enable + string + + + + <_Parameter1>Kevull.MultiHeader.Core.Tests + + + <_Parameter1>Kevull.MultiHeader.EPPLus.Tests + + + <_Parameter1>Kevull.MultiHeader.ClosedXml.Tests + + + + + + diff --git a/src/Kevull.EPPLus.MultiHeader.Test/Person.cs b/tests/Kevull.MultiHeader.TestCommon/Person.cs similarity index 99% rename from src/Kevull.EPPLus.MultiHeader.Test/Person.cs rename to tests/Kevull.MultiHeader.TestCommon/Person.cs index bbf6e1a..fa1ea78 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/Person.cs +++ b/tests/Kevull.MultiHeader.TestCommon/Person.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Test +namespace Kevull.MultiHeader.TestCommon { internal enum Gender {