From e69c0fe420250057e89bd26bec507491895a8486 Mon Sep 17 00:00:00 2001 From: mnieto Date: Sun, 12 Apr 2026 20:37:52 +0200 Subject: [PATCH 01/16] update .net version, update libraries version, migrate to slnx format --- .../Kevull.EPPLus.MultiHeader.Test.csproj | 4 +-- src/Kevull.EPPLus.MultiHeader.sln | 31 ------------------- .../Kevull.EPPLus.MultiHeader.csproj | 4 +-- src/Kevull.MultiHeader.slnx | 4 +++ 4 files changed, 8 insertions(+), 35 deletions(-) delete mode 100644 src/Kevull.EPPLus.MultiHeader.sln create mode 100644 src/Kevull.MultiHeader.slnx diff --git a/src/Kevull.EPPLus.MultiHeader.Test/Kevull.EPPLus.MultiHeader.Test.csproj b/src/Kevull.EPPLus.MultiHeader.Test/Kevull.EPPLus.MultiHeader.Test.csproj index 1948bda..30ca18d 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/Kevull.EPPLus.MultiHeader.Test.csproj +++ b/src/Kevull.EPPLus.MultiHeader.Test/Kevull.EPPLus.MultiHeader.Test.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable true @@ -12,7 +12,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all 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.EPPLus.MultiHeader/Kevull.EPPLus.MultiHeader.csproj b/src/Kevull.EPPLus.MultiHeader/Kevull.EPPLus.MultiHeader.csproj index 2f14887..aa41aea 100644 --- a/src/Kevull.EPPLus.MultiHeader/Kevull.EPPLus.MultiHeader.csproj +++ b/src/Kevull.EPPLus.MultiHeader/Kevull.EPPLus.MultiHeader.csproj @@ -1,8 +1,8 @@  - net8.0;netstandard2.0 - 12.0 + net10.0;netstandard2.0 + 14.0 enable enable True diff --git a/src/Kevull.MultiHeader.slnx b/src/Kevull.MultiHeader.slnx new file mode 100644 index 0000000..b977478 --- /dev/null +++ b/src/Kevull.MultiHeader.slnx @@ -0,0 +1,4 @@ + + + + From 679bba0c9cb91e6983f10a1299a858c16c86abcd Mon Sep 17 00:00:00 2001 From: mnieto Date: Sun, 12 Apr 2026 20:49:21 +0200 Subject: [PATCH 02/16] rename Kevull.EPPLus.MultiHeader to Kevull.MultiHeader.EPPLus --- .../BaseTest.cs | 2 +- .../ColumnInfoTest.cs | 4 ++-- .../ComplexObject.cs | 2 +- .../FormatTest.cs | 2 +- .../GeneralConfigurationOptionsTest.cs | 2 +- .../HeaderManagerTest.cs | 2 +- .../Kevull.MultiHeader.EPPLus.Test.csproj} | 2 +- .../OneHeaderRenderTest.cs | 2 +- .../Person.cs | 2 +- .../PropertyNameBuilderTest.cs | 4 ++-- .../TwoHeaderRenderTest.cs | 2 +- .../Usings.cs | 0 .../ColumnWidth.cs | 2 +- .../Columns/ColumnDef.cs | 2 +- .../Columns/ColumnEnumeration.cs | 2 +- .../Columns/ColumnExpression.cs | 2 +- .../Columns/ColumnFormula.cs | 2 +- .../Columns/ColumnHyperLink.cs | 2 +- .../Columns/ColumnInfo.cs | 2 +- .../Columns/PropertyNameBuilder.cs | 2 +- .../Columns/PropertyNames.cs | 2 +- .../ConfigurationBuilder.cs | 4 ++-- .../HeaderManager.cs | 4 ++-- .../Kevull.MultiHeader.EPPLus.csproj} | 6 +++--- .../MultiHeaderReport.cs | 4 ++-- src/Kevull.MultiHeader.slnx | 4 ++-- 26 files changed, 33 insertions(+), 33 deletions(-) rename src/{Kevull.EPPLus.MultiHeader.Test => Kevull.MultiHeader.EPPLus.Test}/BaseTest.cs (94%) rename src/{Kevull.EPPLus.MultiHeader.Test => Kevull.MultiHeader.EPPLus.Test}/ColumnInfoTest.cs (98%) rename src/{Kevull.EPPLus.MultiHeader.Test => Kevull.MultiHeader.EPPLus.Test}/ComplexObject.cs (98%) rename src/{Kevull.EPPLus.MultiHeader.Test => Kevull.MultiHeader.EPPLus.Test}/FormatTest.cs (99%) rename src/{Kevull.EPPLus.MultiHeader.Test => Kevull.MultiHeader.EPPLus.Test}/GeneralConfigurationOptionsTest.cs (98%) rename src/{Kevull.EPPLus.MultiHeader.Test => Kevull.MultiHeader.EPPLus.Test}/HeaderManagerTest.cs (96%) rename src/{Kevull.EPPLus.MultiHeader.Test/Kevull.EPPLus.MultiHeader.Test.csproj => Kevull.MultiHeader.EPPLus.Test/Kevull.MultiHeader.EPPLus.Test.csproj} (91%) rename src/{Kevull.EPPLus.MultiHeader.Test => Kevull.MultiHeader.EPPLus.Test}/OneHeaderRenderTest.cs (99%) rename src/{Kevull.EPPLus.MultiHeader.Test => Kevull.MultiHeader.EPPLus.Test}/Person.cs (99%) rename src/{Kevull.EPPLus.MultiHeader.Test => Kevull.MultiHeader.EPPLus.Test}/PropertyNameBuilderTest.cs (95%) rename src/{Kevull.EPPLus.MultiHeader.Test => Kevull.MultiHeader.EPPLus.Test}/TwoHeaderRenderTest.cs (98%) rename src/{Kevull.EPPLus.MultiHeader.Test => Kevull.MultiHeader.EPPLus.Test}/Usings.cs (100%) rename src/{Kevull.EPPLus.MultiHeader => Kevull.MultiHeader.EPPLus}/ColumnWidth.cs (98%) rename src/{Kevull.EPPLus.MultiHeader => Kevull.MultiHeader.EPPLus}/Columns/ColumnDef.cs (97%) rename src/{Kevull.EPPLus.MultiHeader => Kevull.MultiHeader.EPPLus}/Columns/ColumnEnumeration.cs (99%) rename src/{Kevull.EPPLus.MultiHeader => Kevull.MultiHeader.EPPLus}/Columns/ColumnExpression.cs (98%) rename src/{Kevull.EPPLus.MultiHeader => Kevull.MultiHeader.EPPLus}/Columns/ColumnFormula.cs (98%) rename src/{Kevull.EPPLus.MultiHeader => Kevull.MultiHeader.EPPLus}/Columns/ColumnHyperLink.cs (98%) rename src/{Kevull.EPPLus.MultiHeader => Kevull.MultiHeader.EPPLus}/Columns/ColumnInfo.cs (99%) rename src/{Kevull.EPPLus.MultiHeader => Kevull.MultiHeader.EPPLus}/Columns/PropertyNameBuilder.cs (97%) rename src/{Kevull.EPPLus.MultiHeader => Kevull.MultiHeader.EPPLus}/Columns/PropertyNames.cs (89%) rename src/{Kevull.EPPLus.MultiHeader => Kevull.MultiHeader.EPPLus}/ConfigurationBuilder.cs (99%) rename src/{Kevull.EPPLus.MultiHeader => Kevull.MultiHeader.EPPLus}/HeaderManager.cs (99%) rename src/{Kevull.EPPLus.MultiHeader/Kevull.EPPLus.MultiHeader.csproj => Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj} (91%) rename src/{Kevull.EPPLus.MultiHeader => Kevull.MultiHeader.EPPLus}/MultiHeaderReport.cs (99%) diff --git a/src/Kevull.EPPLus.MultiHeader.Test/BaseTest.cs b/src/Kevull.MultiHeader.EPPLus.Test/BaseTest.cs similarity index 94% rename from src/Kevull.EPPLus.MultiHeader.Test/BaseTest.cs rename to src/Kevull.MultiHeader.EPPLus.Test/BaseTest.cs index 28a8f5c..e0120f6 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/BaseTest.cs +++ b/src/Kevull.MultiHeader.EPPLus.Test/BaseTest.cs @@ -1,7 +1,7 @@ using OfficeOpenXml; using System.Reflection; -namespace Kevull.EPPLus.MultiHeader.Test +namespace Kevull.MultiHeader.EPPLus.Test { public class BaseTest { diff --git a/src/Kevull.EPPLus.MultiHeader.Test/ColumnInfoTest.cs b/src/Kevull.MultiHeader.EPPLus.Test/ColumnInfoTest.cs similarity index 98% rename from src/Kevull.EPPLus.MultiHeader.Test/ColumnInfoTest.cs rename to src/Kevull.MultiHeader.EPPLus.Test/ColumnInfoTest.cs index 3472025..b60da9f 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/ColumnInfoTest.cs +++ b/src/Kevull.MultiHeader.EPPLus.Test/ColumnInfoTest.cs @@ -1,4 +1,4 @@ -using Kevull.EPPLus.MultiHeader.Columns; +using Kevull.MultiHeader.EPPLus.Columns; using OfficeOpenXml; using System; using System.Collections.Generic; @@ -7,7 +7,7 @@ using System.Threading.Tasks; using System.Xml.Linq; -namespace Kevull.EPPLus.MultiHeader.Test +namespace Kevull.MultiHeader.EPPLus.Test { public class ColumnInfoTest : BaseTest { diff --git a/src/Kevull.EPPLus.MultiHeader.Test/ComplexObject.cs b/src/Kevull.MultiHeader.EPPLus.Test/ComplexObject.cs similarity index 98% rename from src/Kevull.EPPLus.MultiHeader.Test/ComplexObject.cs rename to src/Kevull.MultiHeader.EPPLus.Test/ComplexObject.cs index 5697c83..6dc6395 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/ComplexObject.cs +++ b/src/Kevull.MultiHeader.EPPLus.Test/ComplexObject.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Test +namespace Kevull.MultiHeader.EPPLus.Test { internal class RootLevel { diff --git a/src/Kevull.EPPLus.MultiHeader.Test/FormatTest.cs b/src/Kevull.MultiHeader.EPPLus.Test/FormatTest.cs similarity index 99% rename from src/Kevull.EPPLus.MultiHeader.Test/FormatTest.cs rename to src/Kevull.MultiHeader.EPPLus.Test/FormatTest.cs index 7acf53e..947022f 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/FormatTest.cs +++ b/src/Kevull.MultiHeader.EPPLus.Test/FormatTest.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Xunit; -namespace Kevull.EPPLus.MultiHeader.Test +namespace Kevull.MultiHeader.EPPLus.Test { public class FormatTest : BaseTest { diff --git a/src/Kevull.EPPLus.MultiHeader.Test/GeneralConfigurationOptionsTest.cs b/src/Kevull.MultiHeader.EPPLus.Test/GeneralConfigurationOptionsTest.cs similarity index 98% rename from src/Kevull.EPPLus.MultiHeader.Test/GeneralConfigurationOptionsTest.cs rename to src/Kevull.MultiHeader.EPPLus.Test/GeneralConfigurationOptionsTest.cs index fcdf5f6..77666c0 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/GeneralConfigurationOptionsTest.cs +++ b/src/Kevull.MultiHeader.EPPLus.Test/GeneralConfigurationOptionsTest.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Test +namespace Kevull.MultiHeader.EPPLus.Test { public class GeneralConfigurationOptionsTest : BaseTest { diff --git a/src/Kevull.EPPLus.MultiHeader.Test/HeaderManagerTest.cs b/src/Kevull.MultiHeader.EPPLus.Test/HeaderManagerTest.cs similarity index 96% rename from src/Kevull.EPPLus.MultiHeader.Test/HeaderManagerTest.cs rename to src/Kevull.MultiHeader.EPPLus.Test/HeaderManagerTest.cs index 39abf58..d47acdd 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/HeaderManagerTest.cs +++ b/src/Kevull.MultiHeader.EPPLus.Test/HeaderManagerTest.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Test +namespace Kevull.MultiHeader.EPPLus.Test { diff --git a/src/Kevull.EPPLus.MultiHeader.Test/Kevull.EPPLus.MultiHeader.Test.csproj b/src/Kevull.MultiHeader.EPPLus.Test/Kevull.MultiHeader.EPPLus.Test.csproj similarity index 91% rename from src/Kevull.EPPLus.MultiHeader.Test/Kevull.EPPLus.MultiHeader.Test.csproj rename to src/Kevull.MultiHeader.EPPLus.Test/Kevull.MultiHeader.EPPLus.Test.csproj index 30ca18d..c8dbc5c 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/Kevull.EPPLus.MultiHeader.Test.csproj +++ b/src/Kevull.MultiHeader.EPPLus.Test/Kevull.MultiHeader.EPPLus.Test.csproj @@ -23,7 +23,7 @@ - + diff --git a/src/Kevull.EPPLus.MultiHeader.Test/OneHeaderRenderTest.cs b/src/Kevull.MultiHeader.EPPLus.Test/OneHeaderRenderTest.cs similarity index 99% rename from src/Kevull.EPPLus.MultiHeader.Test/OneHeaderRenderTest.cs rename to src/Kevull.MultiHeader.EPPLus.Test/OneHeaderRenderTest.cs index 77f37c4..91e3daf 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/OneHeaderRenderTest.cs +++ b/src/Kevull.MultiHeader.EPPLus.Test/OneHeaderRenderTest.cs @@ -2,7 +2,7 @@ using System; using OfficeOpenXml; -namespace Kevull.EPPLus.MultiHeader.Test +namespace Kevull.MultiHeader.EPPLus.Test { public class OneHeaderRenderTest : BaseTest { diff --git a/src/Kevull.EPPLus.MultiHeader.Test/Person.cs b/src/Kevull.MultiHeader.EPPLus.Test/Person.cs similarity index 99% rename from src/Kevull.EPPLus.MultiHeader.Test/Person.cs rename to src/Kevull.MultiHeader.EPPLus.Test/Person.cs index bbf6e1a..6607e8b 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/Person.cs +++ b/src/Kevull.MultiHeader.EPPLus.Test/Person.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Test +namespace Kevull.MultiHeader.EPPLus.Test { internal enum Gender { diff --git a/src/Kevull.EPPLus.MultiHeader.Test/PropertyNameBuilderTest.cs b/src/Kevull.MultiHeader.EPPLus.Test/PropertyNameBuilderTest.cs similarity index 95% rename from src/Kevull.EPPLus.MultiHeader.Test/PropertyNameBuilderTest.cs rename to src/Kevull.MultiHeader.EPPLus.Test/PropertyNameBuilderTest.cs index 5aacd16..3e843dc 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/PropertyNameBuilderTest.cs +++ b/src/Kevull.MultiHeader.EPPLus.Test/PropertyNameBuilderTest.cs @@ -1,11 +1,11 @@ -using Kevull.EPPLus.MultiHeader.Columns; +using Kevull.MultiHeader.EPPLus.Columns; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Test +namespace Kevull.MultiHeader.EPPLus.Test { public class PropertyNameBuilderTest { diff --git a/src/Kevull.EPPLus.MultiHeader.Test/TwoHeaderRenderTest.cs b/src/Kevull.MultiHeader.EPPLus.Test/TwoHeaderRenderTest.cs similarity index 98% rename from src/Kevull.EPPLus.MultiHeader.Test/TwoHeaderRenderTest.cs rename to src/Kevull.MultiHeader.EPPLus.Test/TwoHeaderRenderTest.cs index 36f7c3f..ce9f82d 100644 --- a/src/Kevull.EPPLus.MultiHeader.Test/TwoHeaderRenderTest.cs +++ b/src/Kevull.MultiHeader.EPPLus.Test/TwoHeaderRenderTest.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Xunit; -namespace Kevull.EPPLus.MultiHeader.Test +namespace Kevull.MultiHeader.EPPLus.Test { public class TwoHeaderRenderTest : BaseTest { diff --git a/src/Kevull.EPPLus.MultiHeader.Test/Usings.cs b/src/Kevull.MultiHeader.EPPLus.Test/Usings.cs similarity index 100% rename from src/Kevull.EPPLus.MultiHeader.Test/Usings.cs rename to src/Kevull.MultiHeader.EPPLus.Test/Usings.cs diff --git a/src/Kevull.EPPLus.MultiHeader/ColumnWidth.cs b/src/Kevull.MultiHeader.EPPLus/ColumnWidth.cs similarity index 98% rename from src/Kevull.EPPLus.MultiHeader/ColumnWidth.cs rename to src/Kevull.MultiHeader.EPPLus/ColumnWidth.cs index 2ce438e..16b6fd9 100644 --- a/src/Kevull.EPPLus.MultiHeader/ColumnWidth.cs +++ b/src/Kevull.MultiHeader.EPPLus/ColumnWidth.cs @@ -1,4 +1,4 @@ -namespace Kevull.EPPLus.MultiHeader +namespace Kevull.MultiHeader.EPPLus { /// /// Allows to configure the colum with diff --git a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnDef.cs b/src/Kevull.MultiHeader.EPPLus/Columns/ColumnDef.cs similarity index 97% rename from src/Kevull.EPPLus.MultiHeader/Columns/ColumnDef.cs rename to src/Kevull.MultiHeader.EPPLus/Columns/ColumnDef.cs index a5269f9..2669939 100644 --- a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnDef.cs +++ b/src/Kevull.MultiHeader.EPPLus/Columns/ColumnDef.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Columns +namespace Kevull.MultiHeader.EPPLus.Columns { /// /// Column common properties diff --git a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnEnumeration.cs b/src/Kevull.MultiHeader.EPPLus/Columns/ColumnEnumeration.cs similarity index 99% rename from src/Kevull.EPPLus.MultiHeader/Columns/ColumnEnumeration.cs rename to src/Kevull.MultiHeader.EPPLus/Columns/ColumnEnumeration.cs index 48b3c28..223f11d 100644 --- a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnEnumeration.cs +++ b/src/Kevull.MultiHeader.EPPLus/Columns/ColumnEnumeration.cs @@ -8,7 +8,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Columns +namespace Kevull.MultiHeader.EPPLus.Columns { /// /// Specialized that renders data from a or . diff --git a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnExpression.cs b/src/Kevull.MultiHeader.EPPLus/Columns/ColumnExpression.cs similarity index 98% rename from src/Kevull.EPPLus.MultiHeader/Columns/ColumnExpression.cs rename to src/Kevull.MultiHeader.EPPLus/Columns/ColumnExpression.cs index 1b2cd69..c42c0d2 100644 --- a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnExpression.cs +++ b/src/Kevull.MultiHeader.EPPLus/Columns/ColumnExpression.cs @@ -6,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Columns +namespace Kevull.MultiHeader.EPPLus.Columns { /// /// Add an expression column. That is, each time the report will render a value for this column, it will invoke a lambda expression. diff --git a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnFormula.cs b/src/Kevull.MultiHeader.EPPLus/Columns/ColumnFormula.cs similarity index 98% rename from src/Kevull.EPPLus.MultiHeader/Columns/ColumnFormula.cs rename to src/Kevull.MultiHeader.EPPLus/Columns/ColumnFormula.cs index 02fc540..29e6738 100644 --- a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnFormula.cs +++ b/src/Kevull.MultiHeader.EPPLus/Columns/ColumnFormula.cs @@ -6,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Columns +namespace Kevull.MultiHeader.EPPLus.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 diff --git a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnHyperLink.cs b/src/Kevull.MultiHeader.EPPLus/Columns/ColumnHyperLink.cs similarity index 98% rename from src/Kevull.EPPLus.MultiHeader/Columns/ColumnHyperLink.cs rename to src/Kevull.MultiHeader.EPPLus/Columns/ColumnHyperLink.cs index 8950a72..6acc3cd 100644 --- a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnHyperLink.cs +++ b/src/Kevull.MultiHeader.EPPLus/Columns/ColumnHyperLink.cs @@ -7,7 +7,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Columns +namespace Kevull.MultiHeader.EPPLus.Columns { /// /// Add a column with hyperlink. That is, the Excel column is associated to 2 fields: the url and the display content diff --git a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnInfo.cs b/src/Kevull.MultiHeader.EPPLus/Columns/ColumnInfo.cs similarity index 99% rename from src/Kevull.EPPLus.MultiHeader/Columns/ColumnInfo.cs rename to src/Kevull.MultiHeader.EPPLus/Columns/ColumnInfo.cs index 46ce167..c0d4817 100644 --- a/src/Kevull.EPPLus.MultiHeader/Columns/ColumnInfo.cs +++ b/src/Kevull.MultiHeader.EPPLus/Columns/ColumnInfo.cs @@ -9,7 +9,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Columns +namespace Kevull.MultiHeader.EPPLus.Columns { /// /// Base class for columns diff --git a/src/Kevull.EPPLus.MultiHeader/Columns/PropertyNameBuilder.cs b/src/Kevull.MultiHeader.EPPLus/Columns/PropertyNameBuilder.cs similarity index 97% rename from src/Kevull.EPPLus.MultiHeader/Columns/PropertyNameBuilder.cs rename to src/Kevull.MultiHeader.EPPLus/Columns/PropertyNameBuilder.cs index 14b05a5..974a2aa 100644 --- a/src/Kevull.EPPLus.MultiHeader/Columns/PropertyNameBuilder.cs +++ b/src/Kevull.MultiHeader.EPPLus/Columns/PropertyNameBuilder.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Columns +namespace Kevull.MultiHeader.EPPLus.Columns { internal class PropertyNameBuilder { diff --git a/src/Kevull.EPPLus.MultiHeader/Columns/PropertyNames.cs b/src/Kevull.MultiHeader.EPPLus/Columns/PropertyNames.cs similarity index 89% rename from src/Kevull.EPPLus.MultiHeader/Columns/PropertyNames.cs rename to src/Kevull.MultiHeader.EPPLus/Columns/PropertyNames.cs index 71fd022..9f3e013 100644 --- a/src/Kevull.EPPLus.MultiHeader/Columns/PropertyNames.cs +++ b/src/Kevull.MultiHeader.EPPLus/Columns/PropertyNames.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.EPPLus.MultiHeader.Columns +namespace Kevull.MultiHeader.EPPLus.Columns { internal class PropertyNames { diff --git a/src/Kevull.EPPLus.MultiHeader/ConfigurationBuilder.cs b/src/Kevull.MultiHeader.EPPLus/ConfigurationBuilder.cs similarity index 99% rename from src/Kevull.EPPLus.MultiHeader/ConfigurationBuilder.cs rename to src/Kevull.MultiHeader.EPPLus/ConfigurationBuilder.cs index 0c27e2c..f4a4415 100644 --- a/src/Kevull.EPPLus.MultiHeader/ConfigurationBuilder.cs +++ b/src/Kevull.MultiHeader.EPPLus/ConfigurationBuilder.cs @@ -1,11 +1,11 @@ -using Kevull.EPPLus.MultiHeader.Columns; +using Kevull.MultiHeader.EPPLus.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 diff --git a/src/Kevull.EPPLus.MultiHeader/HeaderManager.cs b/src/Kevull.MultiHeader.EPPLus/HeaderManager.cs similarity index 99% rename from src/Kevull.EPPLus.MultiHeader/HeaderManager.cs rename to src/Kevull.MultiHeader.EPPLus/HeaderManager.cs index 90001d5..548f914 100644 --- a/src/Kevull.EPPLus.MultiHeader/HeaderManager.cs +++ b/src/Kevull.MultiHeader.EPPLus/HeaderManager.cs @@ -1,4 +1,4 @@ -using Kevull.EPPLus.MultiHeader.Columns; +using Kevull.MultiHeader.EPPLus.Columns; using System; using System.Collections; using System.Collections.Generic; @@ -6,7 +6,7 @@ using System.Reflection; using System.Security.Cryptography.Pkcs; -namespace Kevull.EPPLus.MultiHeader +namespace Kevull.MultiHeader.EPPLus { /// /// Stores information about the columns to be shown and build the needed header structure diff --git a/src/Kevull.EPPLus.MultiHeader/Kevull.EPPLus.MultiHeader.csproj b/src/Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj similarity index 91% rename from src/Kevull.EPPLus.MultiHeader/Kevull.EPPLus.MultiHeader.csproj rename to src/Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj index aa41aea..31193b7 100644 --- a/src/Kevull.EPPLus.MultiHeader/Kevull.EPPLus.MultiHeader.csproj +++ b/src/Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj @@ -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/MultiHeader.EPPlus + https://github.com/mnieto/MultiHeader.EPPlus git EPPlus;Excel;Multi-header;Report LGPL-3.0-or-later @@ -36,7 +36,7 @@ - <_Parameter1>Kevull.EPPLus.MultiHeader.Test + <_Parameter1>Kevull.MultiHeader.EPPLus.Test diff --git a/src/Kevull.EPPLus.MultiHeader/MultiHeaderReport.cs b/src/Kevull.MultiHeader.EPPLus/MultiHeaderReport.cs similarity index 99% rename from src/Kevull.EPPLus.MultiHeader/MultiHeaderReport.cs rename to src/Kevull.MultiHeader.EPPLus/MultiHeaderReport.cs index 0f4e78b..5c667b6 100644 --- a/src/Kevull.EPPLus.MultiHeader/MultiHeaderReport.cs +++ b/src/Kevull.MultiHeader.EPPLus/MultiHeaderReport.cs @@ -1,11 +1,11 @@ -using Kevull.EPPLus.MultiHeader.Columns; +using Kevull.MultiHeader.EPPLus.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 diff --git a/src/Kevull.MultiHeader.slnx b/src/Kevull.MultiHeader.slnx index b977478..1151e37 100644 --- a/src/Kevull.MultiHeader.slnx +++ b/src/Kevull.MultiHeader.slnx @@ -1,4 +1,4 @@  - - + + From d9fe7e7b91ae2ca13f1aafda09a4727f14461aa1 Mon Sep 17 00:00:00 2001 From: mnieto Date: Sat, 25 Apr 2026 19:17:09 +0200 Subject: [PATCH 03/16] Add Kevull.MultiHeader.Core abstraction layer --- src/Kevull.MultiHeader.Core/CellFormat.cs | 83 +++++ src/Kevull.MultiHeader.Core/ExcelFormats.cs | 298 ++++++++++++++++ src/Kevull.MultiHeader.Core/IExcelWritter.cs | 178 ++++++++++ .../Kevull.MultiHeader.Core.csproj | 57 +++ .../EPPlusExcelWriter.cs | 331 ++++++++++++++++++ .../Kevull.MultiHeader.EPPLus.csproj | 15 +- src/Kevull.MultiHeader.slnx | 1 + 7 files changed, 958 insertions(+), 5 deletions(-) create mode 100644 src/Kevull.MultiHeader.Core/CellFormat.cs create mode 100644 src/Kevull.MultiHeader.Core/ExcelFormats.cs create mode 100644 src/Kevull.MultiHeader.Core/IExcelWritter.cs create mode 100644 src/Kevull.MultiHeader.Core/Kevull.MultiHeader.Core.csproj create mode 100644 src/Kevull.MultiHeader.EPPLus/EPPlusExcelWriter.cs diff --git a/src/Kevull.MultiHeader.Core/CellFormat.cs b/src/Kevull.MultiHeader.Core/CellFormat.cs new file mode 100644 index 0000000..82e969d --- /dev/null +++ b/src/Kevull.MultiHeader.Core/CellFormat.cs @@ -0,0 +1,83 @@ +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 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; } + } +} diff --git a/src/Kevull.MultiHeader.Core/ExcelFormats.cs b/src/Kevull.MultiHeader.Core/ExcelFormats.cs new file mode 100644 index 0000000..bbcad97 --- /dev/null +++ b/src/Kevull.MultiHeader.Core/ExcelFormats.cs @@ -0,0 +1,298 @@ +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" + /// ExcelColor created from the hex string + /// Thrown when hex string is not in a valid format + public static ExcelColor FromHex(string hex) + { + hex = hex.TrimStart('#'); + if (hex.Length == 6) + return new ExcelColor( + Convert.ToByte(hex.Substring(0, 2), 16), + Convert.ToByte(hex.Substring(2, 2), 16), + Convert.ToByte(hex.Substring(4, 2), 16)); + else if (hex.Length == 8) + return new ExcelColor( + Convert.ToByte(hex.Substring(0, 2), 16), + Convert.ToByte(hex.Substring(2, 2), 16), + Convert.ToByte(hex.Substring(4, 2), 16), + Convert.ToByte(hex.Substring(6, 2), 16)); + throw new ArgumentException("Invalid hex color format"); + } + + /// + /// Converts the color to a hex string (e.g., "#RRGGBB") + /// + /// Hex string representation of the color + public string ToHex() => $"#{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.MultiHeader.Core/IExcelWritter.cs b/src/Kevull.MultiHeader.Core/IExcelWritter.cs new file mode 100644 index 0000000..c4e4442 --- /dev/null +++ b/src/Kevull.MultiHeader.Core/IExcelWritter.cs @@ -0,0 +1,178 @@ +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 + 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 + void WriteCell(int fromRow, int fromCol, int toRow, int toCol, object value); + + /// + /// 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/Kevull.MultiHeader.Core.csproj b/src/Kevull.MultiHeader.Core/Kevull.MultiHeader.Core.csproj new file mode 100644 index 0000000..71a73c4 --- /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/MultiHeader.Core + https://github.com/mnieto/MultiHeader.Core + 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.Test + + + <_Parameter1>Kevull.MultiHeader.Core.Test + + + <_Parameter1>Kevull.MultiHeader.ClosedXML.Test + + + + + True + \ + + + True + \ + + + diff --git a/src/Kevull.MultiHeader.EPPLus/EPPlusExcelWriter.cs b/src/Kevull.MultiHeader.EPPLus/EPPlusExcelWriter.cs new file mode 100644 index 0000000..5b58391 --- /dev/null +++ b/src/Kevull.MultiHeader.EPPLus/EPPlusExcelWriter.cs @@ -0,0 +1,331 @@ +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 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 (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 + } +} diff --git a/src/Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj b/src/Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj index 31193b7..9a4a8b7 100644 --- a/src/Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj +++ b/src/Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj @@ -28,12 +28,17 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + <_Parameter1>Kevull.MultiHeader.EPPLus.Test diff --git a/src/Kevull.MultiHeader.slnx b/src/Kevull.MultiHeader.slnx index 1151e37..42fdcb3 100644 --- a/src/Kevull.MultiHeader.slnx +++ b/src/Kevull.MultiHeader.slnx @@ -1,4 +1,5 @@  + From 664ff4996060173a70c95b5a6250505c578154a5 Mon Sep 17 00:00:00 2001 From: mnieto Date: Mon, 27 Apr 2026 21:08:32 +0200 Subject: [PATCH 04/16] Implement IExcelWritter in Kevull.MultiHeader.EPPLus. Adeapt tests. Fix missing functionality in IExcelWritter --- src/Kevull.MultiHeader.Core/IExcelWritter.cs | 18 ++-- .../ColumnInfoTest.cs | 19 ++-- .../Columns/ColumnEnumeration.cs | 41 +++++---- .../Columns/ColumnExpression.cs | 7 +- .../Columns/ColumnFormula.cs | 14 ++- .../Columns/ColumnHyperLink.cs | 21 +++-- .../Columns/ColumnInfo.cs | 30 +++++-- .../EPPlusExcelWriter.cs | 15 +++- .../MultiHeaderReport.cs | 88 +++++++++++-------- 9 files changed, 166 insertions(+), 87 deletions(-) diff --git a/src/Kevull.MultiHeader.Core/IExcelWritter.cs b/src/Kevull.MultiHeader.Core/IExcelWritter.cs index c4e4442..f98d78f 100644 --- a/src/Kevull.MultiHeader.Core/IExcelWritter.cs +++ b/src/Kevull.MultiHeader.Core/IExcelWritter.cs @@ -16,8 +16,8 @@ public interface IExcelWriter /// /// Row number (1-based) /// Column number (1-based) - /// Value to write to the cell - void WriteCell(int row, int col, object value); + /// 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 @@ -26,14 +26,22 @@ public interface IExcelWriter /// Starting column number (1-based) /// Ending row number (1-based) /// Ending column number (1-based) - /// Value to write to all cells in the range - void WriteCell(int fromRow, int fromCol, int toRow, int toCol, object value); + /// 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 formula to a specific cell + /// 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); diff --git a/src/Kevull.MultiHeader.EPPLus.Test/ColumnInfoTest.cs b/src/Kevull.MultiHeader.EPPLus.Test/ColumnInfoTest.cs index b60da9f..ec09d22 100644 --- a/src/Kevull.MultiHeader.EPPLus.Test/ColumnInfoTest.cs +++ b/src/Kevull.MultiHeader.EPPLus.Test/ColumnInfoTest.cs @@ -1,4 +1,5 @@ -using Kevull.MultiHeader.EPPLus.Columns; +using Kevull.MultiHeader.Core; +using Kevull.MultiHeader.EPPLus.Columns; using OfficeOpenXml; using System; using System.Collections.Generic; @@ -48,6 +49,8 @@ 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 +65,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 +79,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 +95,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 +104,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 +115,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 +129,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 +140,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/src/Kevull.MultiHeader.EPPLus/Columns/ColumnEnumeration.cs b/src/Kevull.MultiHeader.EPPLus/Columns/ColumnEnumeration.cs index 223f11d..c6944a7 100644 --- a/src/Kevull.MultiHeader.EPPLus/Columns/ColumnEnumeration.cs +++ b/src/Kevull.MultiHeader.EPPLus/Columns/ColumnEnumeration.cs @@ -1,4 +1,5 @@ -using OfficeOpenXml; +using Kevull.MultiHeader.Core; +using OfficeOpenXml; using System; using System.Collections; using System.Collections.Generic; @@ -26,7 +27,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 +75,20 @@ internal ColumnEnumeration(string name, IEnumerable keyValues, int? orde _keyValues = AddKeyValues(keyValues); } - internal override void FormatHeader(ExcelRange cell, int height) + internal 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) + internal override void WriteCell(IExcelWriter writer, int row, int col, Dictionary properties, object? obj) { if (obj == null) return; @@ -98,7 +101,7 @@ internal override void WriteCell(ExcelRange cell, Dictionary expression, Action properties, object? obj) + internal 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.MultiHeader.EPPLus/Columns/ColumnFormula.cs b/src/Kevull.MultiHeader.EPPLus/Columns/ColumnFormula.cs index 29e6738..dbaac22 100644 --- a/src/Kevull.MultiHeader.EPPLus/Columns/ColumnFormula.cs +++ b/src/Kevull.MultiHeader.EPPLus/Columns/ColumnFormula.cs @@ -1,4 +1,5 @@ -using OfficeOpenXml; +using Kevull.MultiHeader.Core; +using OfficeOpenXml; using System; using System.Collections.Generic; using System.Linq; @@ -57,9 +58,16 @@ public ColumnFormula(string name, string formula, Action cfg) _formula = formula; } - internal override void WriteCell(ExcelRange cell, Dictionary properties, object? obj) + internal override void WriteCell(IExcelWriter writer, int row, int col, Dictionary properties, object? obj) { - cell.Formula = _formula; + writer.WriteFormula(row, col, _formula); + } + + internal 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.MultiHeader.EPPLus/Columns/ColumnHyperLink.cs b/src/Kevull.MultiHeader.EPPLus/Columns/ColumnHyperLink.cs index 6acc3cd..3af6ee2 100644 --- a/src/Kevull.MultiHeader.EPPLus/Columns/ColumnHyperLink.cs +++ b/src/Kevull.MultiHeader.EPPLus/Columns/ColumnHyperLink.cs @@ -1,4 +1,5 @@ -using OfficeOpenXml; +using Kevull.MultiHeader.Core; +using OfficeOpenXml; using System; using System.Collections.Generic; using System.Linq; @@ -51,24 +52,32 @@ public ColumnHyperLink(Expression> columnSelector, Expression properties, object? obj) + internal 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.MultiHeader.EPPLus/Columns/ColumnInfo.cs b/src/Kevull.MultiHeader.EPPLus/Columns/ColumnInfo.cs index c0d4817..41c233b 100644 --- a/src/Kevull.MultiHeader.EPPLus/Columns/ColumnInfo.cs +++ b/src/Kevull.MultiHeader.EPPLus/Columns/ColumnInfo.cs @@ -1,4 +1,5 @@ -using OfficeOpenXml; +using Kevull.MultiHeader.Core; +using OfficeOpenXml; using OfficeOpenXml.FormulaParsing; using System; using System.Collections.Generic; @@ -205,20 +206,35 @@ internal ColumnInfo(PropertyNames names, int? order = null, string? displayName _displayName = displayName; } - internal virtual void FormatHeader(ExcelRange cell, int height) + internal 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) + internal 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) + internal virtual void WriteCell(IExcelWriter writer, int fromRow, int fromCol, int toRow, int toCol, Dictionary properties, object? obj) { - cell.Value = DisplayName; + // 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)); + } + + internal virtual void WriteHeader(IExcelWriter writer, int row, int col) + { + writer.WriteCell(row, col, DisplayName); } private string GetName(string fullName) diff --git a/src/Kevull.MultiHeader.EPPLus/EPPlusExcelWriter.cs b/src/Kevull.MultiHeader.EPPLus/EPPlusExcelWriter.cs index 5b58391..e1298ad 100644 --- a/src/Kevull.MultiHeader.EPPLus/EPPlusExcelWriter.cs +++ b/src/Kevull.MultiHeader.EPPLus/EPPlusExcelWriter.cs @@ -30,17 +30,28 @@ public EPPlusExcelWriter(ExcelPackage package, ExcelWorksheet sheet) #region Basic Writing /// - public void WriteCell(int row, int col, object value) + 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) + 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) { diff --git a/src/Kevull.MultiHeader.EPPLus/MultiHeaderReport.cs b/src/Kevull.MultiHeader.EPPLus/MultiHeaderReport.cs index 5c667b6..fba52a6 100644 --- a/src/Kevull.MultiHeader.EPPLus/MultiHeaderReport.cs +++ b/src/Kevull.MultiHeader.EPPLus/MultiHeaderReport.cs @@ -1,7 +1,6 @@ -using Kevull.MultiHeader.EPPLus.Columns; +using Kevull.MultiHeader.Core; +using Kevull.MultiHeader.EPPLus.Columns; using OfficeOpenXml; -using OfficeOpenXml.Style; -using System.Drawing; using System.Linq; using System.Reflection; @@ -15,12 +14,13 @@ public class MultiHeaderReport { 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 /// @@ -42,6 +42,7 @@ public MultiHeaderReport(ExcelPackage xls, ExcelWorksheet sheet) { _xls = xls; _sheet = sheet; + _writer = new EPPlusExcelWriter(xls, sheet); } /// @@ -117,7 +118,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++; @@ -137,7 +138,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 +150,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,29 +162,29 @@ 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 )) { - _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)) { 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 @@ -194,14 +194,12 @@ private void DoFormatting() if (!_header!.AppendToExistingReport) { - var rangeHeader = _sheet.Cells[_header.FirstRow, _header!.Columns.Min(x => x.Index), lastHeaderRow, lastHeaderColumn]; - rangeHeader.StyleName = StyleNames.HeaderStyleName; + _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,45 +208,59 @@ 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 + }; + _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); } } From 6439a96e6a7377fe1077df0ed48ea277e48ef591 Mon Sep 17 00:00:00 2001 From: mnieto Date: Fri, 1 May 2026 14:17:18 +0200 Subject: [PATCH 05/16] Add ExcelWritter tests --- .../EPPlusExcelWriterTests.cs | 991 ++++++++++++++++++ 1 file changed, 991 insertions(+) create mode 100644 src/Kevull.MultiHeader.EPPLus.Test/EPPlusExcelWriterTests.cs diff --git a/src/Kevull.MultiHeader.EPPLus.Test/EPPlusExcelWriterTests.cs b/src/Kevull.MultiHeader.EPPLus.Test/EPPlusExcelWriterTests.cs new file mode 100644 index 0000000..8f58de8 --- /dev/null +++ b/src/Kevull.MultiHeader.EPPLus.Test/EPPlusExcelWriterTests.cs @@ -0,0 +1,991 @@ +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 + { + 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 + }; + + // 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); + + package.Dispose(); + } + + /// + /// 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(); + } + + /// + /// 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 From b7c5f54fa58d36bd4888ae0ad59ad369ff4f6a8b Mon Sep 17 00:00:00 2001 From: mnieto Date: Fri, 1 May 2026 21:35:14 +0200 Subject: [PATCH 06/16] Reorganize folders structure grouping test projects --- .github/workflows/build.yml | 4 ++-- Kevull.MultiHeader.slnx | 5 +++++ src/Kevull.MultiHeader.slnx | 5 ----- {src => tests}/Kevull.MultiHeader.EPPLus.Test/BaseTest.cs | 0 .../Kevull.MultiHeader.EPPLus.Test/ColumnInfoTest.cs | 0 .../Kevull.MultiHeader.EPPLus.Test/ComplexObject.cs | 0 .../Kevull.MultiHeader.EPPLus.Test/EPPlusExcelWriterTests.cs | 0 {src => tests}/Kevull.MultiHeader.EPPLus.Test/FormatTest.cs | 0 .../GeneralConfigurationOptionsTest.cs | 0 .../Kevull.MultiHeader.EPPLus.Test/HeaderManagerTest.cs | 0 .../Kevull.MultiHeader.EPPLus.Test.csproj | 2 +- .../Kevull.MultiHeader.EPPLus.Test/OneHeaderRenderTest.cs | 0 {src => tests}/Kevull.MultiHeader.EPPLus.Test/Person.cs | 0 .../PropertyNameBuilderTest.cs | 0 .../Kevull.MultiHeader.EPPLus.Test/TwoHeaderRenderTest.cs | 0 {src => tests}/Kevull.MultiHeader.EPPLus.Test/Usings.cs | 0 16 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 Kevull.MultiHeader.slnx delete mode 100644 src/Kevull.MultiHeader.slnx rename {src => tests}/Kevull.MultiHeader.EPPLus.Test/BaseTest.cs (100%) rename {src => tests}/Kevull.MultiHeader.EPPLus.Test/ColumnInfoTest.cs (100%) rename {src => tests}/Kevull.MultiHeader.EPPLus.Test/ComplexObject.cs (100%) rename {src => tests}/Kevull.MultiHeader.EPPLus.Test/EPPlusExcelWriterTests.cs (100%) rename {src => tests}/Kevull.MultiHeader.EPPLus.Test/FormatTest.cs (100%) rename {src => tests}/Kevull.MultiHeader.EPPLus.Test/GeneralConfigurationOptionsTest.cs (100%) rename {src => tests}/Kevull.MultiHeader.EPPLus.Test/HeaderManagerTest.cs (100%) rename {src => tests}/Kevull.MultiHeader.EPPLus.Test/Kevull.MultiHeader.EPPLus.Test.csproj (90%) rename {src => tests}/Kevull.MultiHeader.EPPLus.Test/OneHeaderRenderTest.cs (100%) rename {src => tests}/Kevull.MultiHeader.EPPLus.Test/Person.cs (100%) rename {src => tests}/Kevull.MultiHeader.EPPLus.Test/PropertyNameBuilderTest.cs (100%) rename {src => tests}/Kevull.MultiHeader.EPPLus.Test/TwoHeaderRenderTest.cs (100%) rename {src => tests}/Kevull.MultiHeader.EPPLus.Test/Usings.cs (100%) 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..b4216e5 --- /dev/null +++ b/Kevull.MultiHeader.slnx @@ -0,0 +1,5 @@ + + + + + diff --git a/src/Kevull.MultiHeader.slnx b/src/Kevull.MultiHeader.slnx deleted file mode 100644 index 42fdcb3..0000000 --- a/src/Kevull.MultiHeader.slnx +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/Kevull.MultiHeader.EPPLus.Test/BaseTest.cs b/tests/Kevull.MultiHeader.EPPLus.Test/BaseTest.cs similarity index 100% rename from src/Kevull.MultiHeader.EPPLus.Test/BaseTest.cs rename to tests/Kevull.MultiHeader.EPPLus.Test/BaseTest.cs diff --git a/src/Kevull.MultiHeader.EPPLus.Test/ColumnInfoTest.cs b/tests/Kevull.MultiHeader.EPPLus.Test/ColumnInfoTest.cs similarity index 100% rename from src/Kevull.MultiHeader.EPPLus.Test/ColumnInfoTest.cs rename to tests/Kevull.MultiHeader.EPPLus.Test/ColumnInfoTest.cs diff --git a/src/Kevull.MultiHeader.EPPLus.Test/ComplexObject.cs b/tests/Kevull.MultiHeader.EPPLus.Test/ComplexObject.cs similarity index 100% rename from src/Kevull.MultiHeader.EPPLus.Test/ComplexObject.cs rename to tests/Kevull.MultiHeader.EPPLus.Test/ComplexObject.cs diff --git a/src/Kevull.MultiHeader.EPPLus.Test/EPPlusExcelWriterTests.cs b/tests/Kevull.MultiHeader.EPPLus.Test/EPPlusExcelWriterTests.cs similarity index 100% rename from src/Kevull.MultiHeader.EPPLus.Test/EPPlusExcelWriterTests.cs rename to tests/Kevull.MultiHeader.EPPLus.Test/EPPlusExcelWriterTests.cs diff --git a/src/Kevull.MultiHeader.EPPLus.Test/FormatTest.cs b/tests/Kevull.MultiHeader.EPPLus.Test/FormatTest.cs similarity index 100% rename from src/Kevull.MultiHeader.EPPLus.Test/FormatTest.cs rename to tests/Kevull.MultiHeader.EPPLus.Test/FormatTest.cs diff --git a/src/Kevull.MultiHeader.EPPLus.Test/GeneralConfigurationOptionsTest.cs b/tests/Kevull.MultiHeader.EPPLus.Test/GeneralConfigurationOptionsTest.cs similarity index 100% rename from src/Kevull.MultiHeader.EPPLus.Test/GeneralConfigurationOptionsTest.cs rename to tests/Kevull.MultiHeader.EPPLus.Test/GeneralConfigurationOptionsTest.cs diff --git a/src/Kevull.MultiHeader.EPPLus.Test/HeaderManagerTest.cs b/tests/Kevull.MultiHeader.EPPLus.Test/HeaderManagerTest.cs similarity index 100% rename from src/Kevull.MultiHeader.EPPLus.Test/HeaderManagerTest.cs rename to tests/Kevull.MultiHeader.EPPLus.Test/HeaderManagerTest.cs diff --git a/src/Kevull.MultiHeader.EPPLus.Test/Kevull.MultiHeader.EPPLus.Test.csproj b/tests/Kevull.MultiHeader.EPPLus.Test/Kevull.MultiHeader.EPPLus.Test.csproj similarity index 90% rename from src/Kevull.MultiHeader.EPPLus.Test/Kevull.MultiHeader.EPPLus.Test.csproj rename to tests/Kevull.MultiHeader.EPPLus.Test/Kevull.MultiHeader.EPPLus.Test.csproj index c8dbc5c..1109faf 100644 --- a/src/Kevull.MultiHeader.EPPLus.Test/Kevull.MultiHeader.EPPLus.Test.csproj +++ b/tests/Kevull.MultiHeader.EPPLus.Test/Kevull.MultiHeader.EPPLus.Test.csproj @@ -23,7 +23,7 @@ - + diff --git a/src/Kevull.MultiHeader.EPPLus.Test/OneHeaderRenderTest.cs b/tests/Kevull.MultiHeader.EPPLus.Test/OneHeaderRenderTest.cs similarity index 100% rename from src/Kevull.MultiHeader.EPPLus.Test/OneHeaderRenderTest.cs rename to tests/Kevull.MultiHeader.EPPLus.Test/OneHeaderRenderTest.cs diff --git a/src/Kevull.MultiHeader.EPPLus.Test/Person.cs b/tests/Kevull.MultiHeader.EPPLus.Test/Person.cs similarity index 100% rename from src/Kevull.MultiHeader.EPPLus.Test/Person.cs rename to tests/Kevull.MultiHeader.EPPLus.Test/Person.cs diff --git a/src/Kevull.MultiHeader.EPPLus.Test/PropertyNameBuilderTest.cs b/tests/Kevull.MultiHeader.EPPLus.Test/PropertyNameBuilderTest.cs similarity index 100% rename from src/Kevull.MultiHeader.EPPLus.Test/PropertyNameBuilderTest.cs rename to tests/Kevull.MultiHeader.EPPLus.Test/PropertyNameBuilderTest.cs diff --git a/src/Kevull.MultiHeader.EPPLus.Test/TwoHeaderRenderTest.cs b/tests/Kevull.MultiHeader.EPPLus.Test/TwoHeaderRenderTest.cs similarity index 100% rename from src/Kevull.MultiHeader.EPPLus.Test/TwoHeaderRenderTest.cs rename to tests/Kevull.MultiHeader.EPPLus.Test/TwoHeaderRenderTest.cs diff --git a/src/Kevull.MultiHeader.EPPLus.Test/Usings.cs b/tests/Kevull.MultiHeader.EPPLus.Test/Usings.cs similarity index 100% rename from src/Kevull.MultiHeader.EPPLus.Test/Usings.cs rename to tests/Kevull.MultiHeader.EPPLus.Test/Usings.cs From 399ee8346a366af2079f57773f186d89d7311ec3 Mon Sep 17 00:00:00 2001 From: mnieto Date: Fri, 1 May 2026 22:33:57 +0200 Subject: [PATCH 07/16] Add FontName format and improve Font and ExcelColor tests --- src/Kevull.MultiHeader.Core/CellFormat.cs | 5 ++ src/Kevull.MultiHeader.Core/ExcelFormats.cs | 52 ++++++++++++------ .../EPPlusExcelWriter.cs | 2 + .../EPPlusExcelWriterTests.cs | 55 ++++++++++++++++++- 4 files changed, 97 insertions(+), 17 deletions(-) diff --git a/src/Kevull.MultiHeader.Core/CellFormat.cs b/src/Kevull.MultiHeader.Core/CellFormat.cs index 82e969d..5bc04dd 100644 --- a/src/Kevull.MultiHeader.Core/CellFormat.cs +++ b/src/Kevull.MultiHeader.Core/CellFormat.cs @@ -55,6 +55,11 @@ public class CellFormat /// public bool? Italic { get; set; } + /// + /// Font family name + /// + public string? FontName { get; set; } + /// /// Font size in points /// diff --git a/src/Kevull.MultiHeader.Core/ExcelFormats.cs b/src/Kevull.MultiHeader.Core/ExcelFormats.cs index bbcad97..c52669b 100644 --- a/src/Kevull.MultiHeader.Core/ExcelFormats.cs +++ b/src/Kevull.MultiHeader.Core/ExcelFormats.cs @@ -56,30 +56,50 @@ public ExcelColor(byte a, byte r, byte g, byte b) /// Creates a color from a hex string (e.g., "#FF0000" or "FF0000") /// /// Hex color string in format "#RRGGBB", "RRGGBB", "#AARRGGBB", or "AARRGGBB" - /// ExcelColor created from the hex string /// Thrown when hex string is not in a valid format - public static ExcelColor FromHex(string hex) + /// Thrown when hex string is null or empty + public ExcelColor(string hex) { + if (string.IsNullOrEmpty(hex)) + throw new ArgumentNullException(nameof(hex)); + hex = hex.TrimStart('#'); - if (hex.Length == 6) - return new ExcelColor( - Convert.ToByte(hex.Substring(0, 2), 16), - Convert.ToByte(hex.Substring(2, 2), 16), - Convert.ToByte(hex.Substring(4, 2), 16)); - else if (hex.Length == 8) - return new ExcelColor( - Convert.ToByte(hex.Substring(0, 2), 16), - Convert.ToByte(hex.Substring(2, 2), 16), - Convert.ToByte(hex.Substring(4, 2), 16), - Convert.ToByte(hex.Substring(6, 2), 16)); - throw new ArgumentException("Invalid hex color format"); + 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") /// - /// Hex string representation of the color - public string ToHex() => $"#{R:X2}{G:X2}{B:X2}"; + 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) diff --git a/src/Kevull.MultiHeader.EPPLus/EPPlusExcelWriter.cs b/src/Kevull.MultiHeader.EPPLus/EPPlusExcelWriter.cs index e1298ad..f32298d 100644 --- a/src/Kevull.MultiHeader.EPPLus/EPPlusExcelWriter.cs +++ b/src/Kevull.MultiHeader.EPPLus/EPPlusExcelWriter.cs @@ -238,6 +238,8 @@ private void ApplyFormatToRange(ExcelStyle style, CellFormat format) 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) diff --git a/tests/Kevull.MultiHeader.EPPLus.Test/EPPlusExcelWriterTests.cs b/tests/Kevull.MultiHeader.EPPLus.Test/EPPlusExcelWriterTests.cs index 8f58de8..c9459ef 100644 --- a/tests/Kevull.MultiHeader.EPPLus.Test/EPPlusExcelWriterTests.cs +++ b/tests/Kevull.MultiHeader.EPPLus.Test/EPPlusExcelWriterTests.cs @@ -369,7 +369,8 @@ public void ApplyFormat_ComplexFormat_AppliesAllProperties() Italic = true, FontSize = 14, WrapText = true, - TextRotation = 45 + TextRotation = 45, + FontColor = Core.ExcelColor.Red }; // Act @@ -382,10 +383,42 @@ public void ApplyFormat_ComplexFormat_AppliesAllProperties() 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. @@ -415,6 +448,26 @@ public void ApplyFormat_TypicalExcelBoundaries_AppliesFormat(int row, int col) 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) From 500a73307b8932b4fb258f0a67f152ad9dc350f7 Mon Sep 17 00:00:00 2001 From: mnieto Date: Sat, 2 May 2026 21:38:46 +0200 Subject: [PATCH 08/16] Reorganize tests and move agnostic tests to Kevull.MultiHeader.Core.Tests --- Kevull.MultiHeader.slnx | 7 ++-- .../ColumnWidth.cs | 2 +- .../Columns/ColumnDef.cs | 5 +-- .../Columns/ColumnEnumeration.cs | 9 ++--- .../Columns/ColumnExpression.cs | 7 ++-- .../Columns/ColumnFormula.cs | 7 ++-- .../Columns/ColumnHyperLink.cs | 5 +-- .../Columns/ColumnInfo.cs | 22 +++++------ .../Columns/PropertyNameBuilder.cs | 2 +- .../Columns/PropertyNames.cs | 2 +- .../HeaderManager.cs | 11 +++--- .../Kevull.MultiHeader.Core.csproj | 7 +--- src/Kevull.MultiHeader.Core/StyleNames.cs | 12 ++++++ .../ConfigurationBuilder.cs | 3 +- .../Kevull.MultiHeader.EPPLus.csproj | 2 +- .../MultiHeaderReport.cs | 16 ++------ .../ColumnInfoTest.cs | 39 +++++++++++++++++++ .../HeaderManagerTest.cs | 6 ++- .../Kevull.MultiHeader.Core.Tests.csproj | 29 ++++++++++++++ .../PropertyNameBuilderTest.cs | 5 ++- tests/Kevull.MultiHeader.Core.Tests/Usings.cs | 1 + .../ColumnInfoTest.cs | 37 +----------------- .../EPPlusExcelWriterTests.cs | 0 .../FormatTest.cs | 5 ++- .../GeneralConfigurationOptionsTest.cs | 1 + .../Kevull.MultiHeader.EPPLus.Tests.csproj} | 2 + .../OneHeaderRenderTest.cs | 1 + .../TwoHeaderRenderTest.cs | 1 + .../Usings.cs | 0 .../BaseTest.cs | 0 .../ComplexObject.cs | 2 +- .../Kevull.MultiHeader.TestCommon.csproj | 14 +++++++ .../Person.cs | 2 +- 33 files changed, 159 insertions(+), 105 deletions(-) rename src/{Kevull.MultiHeader.EPPLus => Kevull.MultiHeader.Core}/ColumnWidth.cs (98%) rename src/{Kevull.MultiHeader.EPPLus => Kevull.MultiHeader.Core}/Columns/ColumnDef.cs (95%) rename src/{Kevull.MultiHeader.EPPLus => Kevull.MultiHeader.Core}/Columns/ColumnEnumeration.cs (94%) rename src/{Kevull.MultiHeader.EPPLus => Kevull.MultiHeader.Core}/Columns/ColumnExpression.cs (90%) rename src/{Kevull.MultiHeader.EPPLus => Kevull.MultiHeader.Core}/Columns/ColumnFormula.cs (89%) rename src/{Kevull.MultiHeader.EPPLus => Kevull.MultiHeader.Core}/Columns/ColumnHyperLink.cs (94%) rename src/{Kevull.MultiHeader.EPPLus => Kevull.MultiHeader.Core}/Columns/ColumnInfo.cs (93%) rename src/{Kevull.MultiHeader.EPPLus => Kevull.MultiHeader.Core}/Columns/PropertyNameBuilder.cs (97%) rename src/{Kevull.MultiHeader.EPPLus => Kevull.MultiHeader.Core}/Columns/PropertyNames.cs (89%) rename src/{Kevull.MultiHeader.EPPLus => Kevull.MultiHeader.Core}/HeaderManager.cs (97%) create mode 100644 src/Kevull.MultiHeader.Core/StyleNames.cs create mode 100644 tests/Kevull.MultiHeader.Core.Tests/ColumnInfoTest.cs rename tests/{Kevull.MultiHeader.EPPLus.Test => Kevull.MultiHeader.Core.Tests}/HeaderManagerTest.cs (90%) create mode 100644 tests/Kevull.MultiHeader.Core.Tests/Kevull.MultiHeader.Core.Tests.csproj rename tests/{Kevull.MultiHeader.EPPLus.Test => Kevull.MultiHeader.Core.Tests}/PropertyNameBuilderTest.cs (94%) create mode 100644 tests/Kevull.MultiHeader.Core.Tests/Usings.cs rename tests/{Kevull.MultiHeader.EPPLus.Test => Kevull.MultiHeader.EPPLus.Tests}/ColumnInfoTest.cs (77%) rename tests/{Kevull.MultiHeader.EPPLus.Test => Kevull.MultiHeader.EPPLus.Tests}/EPPlusExcelWriterTests.cs (100%) rename tests/{Kevull.MultiHeader.EPPLus.Test => Kevull.MultiHeader.EPPLus.Tests}/FormatTest.cs (97%) rename tests/{Kevull.MultiHeader.EPPLus.Test => Kevull.MultiHeader.EPPLus.Tests}/GeneralConfigurationOptionsTest.cs (98%) rename tests/{Kevull.MultiHeader.EPPLus.Test/Kevull.MultiHeader.EPPLus.Test.csproj => Kevull.MultiHeader.EPPLus.Tests/Kevull.MultiHeader.EPPLus.Tests.csproj} (84%) rename tests/{Kevull.MultiHeader.EPPLus.Test => Kevull.MultiHeader.EPPLus.Tests}/OneHeaderRenderTest.cs (99%) rename tests/{Kevull.MultiHeader.EPPLus.Test => Kevull.MultiHeader.EPPLus.Tests}/TwoHeaderRenderTest.cs (99%) rename tests/{Kevull.MultiHeader.EPPLus.Test => Kevull.MultiHeader.EPPLus.Tests}/Usings.cs (100%) rename tests/{Kevull.MultiHeader.EPPLus.Test => Kevull.MultiHeader.TestCommon}/BaseTest.cs (100%) rename tests/{Kevull.MultiHeader.EPPLus.Test => Kevull.MultiHeader.TestCommon}/ComplexObject.cs (98%) create mode 100644 tests/Kevull.MultiHeader.TestCommon/Kevull.MultiHeader.TestCommon.csproj rename tests/{Kevull.MultiHeader.EPPLus.Test => Kevull.MultiHeader.TestCommon}/Person.cs (99%) diff --git a/Kevull.MultiHeader.slnx b/Kevull.MultiHeader.slnx index b4216e5..902fa30 100644 --- a/Kevull.MultiHeader.slnx +++ b/Kevull.MultiHeader.slnx @@ -1,5 +1,6 @@ - - - + + + + diff --git a/src/Kevull.MultiHeader.EPPLus/ColumnWidth.cs b/src/Kevull.MultiHeader.Core/ColumnWidth.cs similarity index 98% rename from src/Kevull.MultiHeader.EPPLus/ColumnWidth.cs rename to src/Kevull.MultiHeader.Core/ColumnWidth.cs index 16b6fd9..b00ae57 100644 --- a/src/Kevull.MultiHeader.EPPLus/ColumnWidth.cs +++ b/src/Kevull.MultiHeader.Core/ColumnWidth.cs @@ -1,4 +1,4 @@ -namespace Kevull.MultiHeader.EPPLus +namespace Kevull.MultiHeader.Core { /// /// Allows to configure the colum with diff --git a/src/Kevull.MultiHeader.EPPLus/Columns/ColumnDef.cs b/src/Kevull.MultiHeader.Core/Columns/ColumnDef.cs similarity index 95% rename from src/Kevull.MultiHeader.EPPLus/Columns/ColumnDef.cs rename to src/Kevull.MultiHeader.Core/Columns/ColumnDef.cs index 2669939..380ad3e 100644 --- a/src/Kevull.MultiHeader.EPPLus/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.MultiHeader.EPPLus.Columns +namespace Kevull.MultiHeader.Core.Columns { /// /// Column common properties diff --git a/src/Kevull.MultiHeader.EPPLus/Columns/ColumnEnumeration.cs b/src/Kevull.MultiHeader.Core/Columns/ColumnEnumeration.cs similarity index 94% rename from src/Kevull.MultiHeader.EPPLus/Columns/ColumnEnumeration.cs rename to src/Kevull.MultiHeader.Core/Columns/ColumnEnumeration.cs index c6944a7..ab15e5d 100644 --- a/src/Kevull.MultiHeader.EPPLus/Columns/ColumnEnumeration.cs +++ b/src/Kevull.MultiHeader.Core/Columns/ColumnEnumeration.cs @@ -1,5 +1,4 @@ using Kevull.MultiHeader.Core; -using OfficeOpenXml; using System; using System.Collections; using System.Collections.Generic; @@ -9,7 +8,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.MultiHeader.EPPLus.Columns +namespace Kevull.MultiHeader.Core.Columns { /// /// Specialized that renders data from a or . @@ -75,7 +74,7 @@ internal ColumnEnumeration(string name, IEnumerable keyValues, int? orde _keyValues = AddKeyValues(keyValues); } - internal override void FormatHeader(IExcelWriter writer, int row, int col, int height) + public override void FormatHeader(IExcelWriter writer, int row, int col, int height) { // Merge the parent header across all columns writer.Merge(row, col, row, col + Width - 1); @@ -88,7 +87,7 @@ internal override void FormatHeader(IExcelWriter writer, int row, int col, int h } } - internal override void WriteCell(IExcelWriter writer, int row, int col, Dictionary properties, object? obj) + public override void WriteCell(IExcelWriter writer, int row, int col, Dictionary properties, object? obj) { if (obj == null) return; @@ -119,7 +118,7 @@ internal override void WriteCell(IExcelWriter writer, int row, int col, Dictiona } } - internal override void WriteHeader(IExcelWriter writer, int row, int col) + public override void WriteHeader(IExcelWriter writer, int row, int col) { // Write parent header writer.WriteCell(row, col, DisplayName); diff --git a/src/Kevull.MultiHeader.EPPLus/Columns/ColumnExpression.cs b/src/Kevull.MultiHeader.Core/Columns/ColumnExpression.cs similarity index 90% rename from src/Kevull.MultiHeader.EPPLus/Columns/ColumnExpression.cs rename to src/Kevull.MultiHeader.Core/Columns/ColumnExpression.cs index 260d390..33daa7f 100644 --- a/src/Kevull.MultiHeader.EPPLus/Columns/ColumnExpression.cs +++ b/src/Kevull.MultiHeader.Core/Columns/ColumnExpression.cs @@ -1,5 +1,4 @@ using Kevull.MultiHeader.Core; -using OfficeOpenXml; using System; using System.Collections.Generic; using System.Linq; @@ -7,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.MultiHeader.EPPLus.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. @@ -52,12 +51,12 @@ 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(IExcelWriter writer, int row, int col, Dictionary properties, object? obj) + public override void WriteCell(IExcelWriter writer, int row, int col, Dictionary properties, object? obj) { if (obj is null) return; diff --git a/src/Kevull.MultiHeader.EPPLus/Columns/ColumnFormula.cs b/src/Kevull.MultiHeader.Core/Columns/ColumnFormula.cs similarity index 89% rename from src/Kevull.MultiHeader.EPPLus/Columns/ColumnFormula.cs rename to src/Kevull.MultiHeader.Core/Columns/ColumnFormula.cs index dbaac22..3c5bd5b 100644 --- a/src/Kevull.MultiHeader.EPPLus/Columns/ColumnFormula.cs +++ b/src/Kevull.MultiHeader.Core/Columns/ColumnFormula.cs @@ -1,5 +1,4 @@ using Kevull.MultiHeader.Core; -using OfficeOpenXml; using System; using System.Collections.Generic; using System.Linq; @@ -7,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.MultiHeader.EPPLus.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 @@ -58,12 +57,12 @@ public ColumnFormula(string name, string formula, Action cfg) _formula = formula; } - internal override void WriteCell(IExcelWriter writer, int row, int col, Dictionary properties, object? obj) + public override void WriteCell(IExcelWriter writer, int row, int col, Dictionary properties, object? obj) { writer.WriteFormula(row, col, _formula); } - internal override void WriteCell(IExcelWriter writer, int fromRow, int fromCol, int toRow, int toCol, Dictionary properties, object? obj) + 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 diff --git a/src/Kevull.MultiHeader.EPPLus/Columns/ColumnHyperLink.cs b/src/Kevull.MultiHeader.Core/Columns/ColumnHyperLink.cs similarity index 94% rename from src/Kevull.MultiHeader.EPPLus/Columns/ColumnHyperLink.cs rename to src/Kevull.MultiHeader.Core/Columns/ColumnHyperLink.cs index 3af6ee2..7ba73a6 100644 --- a/src/Kevull.MultiHeader.EPPLus/Columns/ColumnHyperLink.cs +++ b/src/Kevull.MultiHeader.Core/Columns/ColumnHyperLink.cs @@ -1,5 +1,4 @@ using Kevull.MultiHeader.Core; -using OfficeOpenXml; using System; using System.Collections.Generic; using System.Linq; @@ -8,7 +7,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.MultiHeader.EPPLus.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 @@ -52,7 +51,7 @@ 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; diff --git a/src/Kevull.MultiHeader.EPPLus/Columns/ColumnInfo.cs b/src/Kevull.MultiHeader.Core/Columns/ColumnInfo.cs similarity index 93% rename from src/Kevull.MultiHeader.EPPLus/Columns/ColumnInfo.cs rename to src/Kevull.MultiHeader.Core/Columns/ColumnInfo.cs index 41c233b..f64a3c8 100644 --- a/src/Kevull.MultiHeader.EPPLus/Columns/ColumnInfo.cs +++ b/src/Kevull.MultiHeader.Core/Columns/ColumnInfo.cs @@ -1,6 +1,4 @@ using Kevull.MultiHeader.Core; -using OfficeOpenXml; -using OfficeOpenXml.FormulaParsing; using System; using System.Collections.Generic; using System.Diagnostics; @@ -10,7 +8,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.MultiHeader.EPPLus.Columns +namespace Kevull.MultiHeader.Core.Columns { /// /// Base class for columns @@ -66,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 @@ -96,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 . /// @@ -105,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) @@ -206,18 +204,18 @@ internal ColumnInfo(PropertyNames names, int? order = null, string? displayName _displayName = displayName; } - internal virtual void FormatHeader(IExcelWriter writer, int row, int col, int height) + public virtual void FormatHeader(IExcelWriter writer, int row, int col, int height) { writer.Merge(row, col, row + height - 1, col + Width - 1); } - internal virtual void WriteCell(IExcelWriter writer, int row, int col, Dictionary properties, object? obj) + public virtual void WriteCell(IExcelWriter writer, int row, int col, Dictionary properties, object? obj) { if (obj != null) writer.WriteCell(row, col, properties[Name].GetValue(obj)); } - internal virtual void WriteCell(IExcelWriter writer, int fromRow, int fromCol, int toRow, int toCol, Dictionary properties, object? obj) + 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 @@ -232,7 +230,7 @@ internal virtual void WriteCell(IExcelWriter writer, int fromRow, int fromCol, i writer.WriteCell(fromRow, fromCol, properties[Name].GetValue(obj)); } - internal virtual void WriteHeader(IExcelWriter writer, int row, int col) + public virtual void WriteHeader(IExcelWriter writer, int row, int col) { writer.WriteCell(row, col, DisplayName); } @@ -286,7 +284,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.MultiHeader.EPPLus/Columns/PropertyNameBuilder.cs b/src/Kevull.MultiHeader.Core/Columns/PropertyNameBuilder.cs similarity index 97% rename from src/Kevull.MultiHeader.EPPLus/Columns/PropertyNameBuilder.cs rename to src/Kevull.MultiHeader.Core/Columns/PropertyNameBuilder.cs index 974a2aa..c2c5b92 100644 --- a/src/Kevull.MultiHeader.EPPLus/Columns/PropertyNameBuilder.cs +++ b/src/Kevull.MultiHeader.Core/Columns/PropertyNameBuilder.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.MultiHeader.EPPLus.Columns +namespace Kevull.MultiHeader.Core.Columns { internal class PropertyNameBuilder { diff --git a/src/Kevull.MultiHeader.EPPLus/Columns/PropertyNames.cs b/src/Kevull.MultiHeader.Core/Columns/PropertyNames.cs similarity index 89% rename from src/Kevull.MultiHeader.EPPLus/Columns/PropertyNames.cs rename to src/Kevull.MultiHeader.Core/Columns/PropertyNames.cs index 9f3e013..48fa779 100644 --- a/src/Kevull.MultiHeader.EPPLus/Columns/PropertyNames.cs +++ b/src/Kevull.MultiHeader.Core/Columns/PropertyNames.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.MultiHeader.EPPLus.Columns +namespace Kevull.MultiHeader.Core.Columns { internal class PropertyNames { diff --git a/src/Kevull.MultiHeader.EPPLus/HeaderManager.cs b/src/Kevull.MultiHeader.Core/HeaderManager.cs similarity index 97% rename from src/Kevull.MultiHeader.EPPLus/HeaderManager.cs rename to src/Kevull.MultiHeader.Core/HeaderManager.cs index 548f914..297f41c 100644 --- a/src/Kevull.MultiHeader.EPPLus/HeaderManager.cs +++ b/src/Kevull.MultiHeader.Core/HeaderManager.cs @@ -1,12 +1,13 @@ -using Kevull.MultiHeader.EPPLus.Columns; +using Kevull.MultiHeader.Code; +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.MultiHeader.EPPLus + +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/Kevull.MultiHeader.Core.csproj b/src/Kevull.MultiHeader.Core/Kevull.MultiHeader.Core.csproj index 71a73c4..bcba50b 100644 --- a/src/Kevull.MultiHeader.Core/Kevull.MultiHeader.Core.csproj +++ b/src/Kevull.MultiHeader.Core/Kevull.MultiHeader.Core.csproj @@ -35,13 +35,10 @@ - <_Parameter1>Kevull.MultiHeader.EPPLus.Test + <_Parameter1>Kevull.MultiHeader.EPPLus.Tests - <_Parameter1>Kevull.MultiHeader.Core.Test - - - <_Parameter1>Kevull.MultiHeader.ClosedXML.Test + <_Parameter1>Kevull.MultiHeader.Core.Tests diff --git a/src/Kevull.MultiHeader.Core/StyleNames.cs b/src/Kevull.MultiHeader.Core/StyleNames.cs new file mode 100644 index 0000000..1f43b16 --- /dev/null +++ b/src/Kevull.MultiHeader.Core/StyleNames.cs @@ -0,0 +1,12 @@ +namespace Kevull.MultiHeader.Code +{ + public class StyleNames + { + public const string HeaderStyleName = "__Headers__"; + public const string DateStyleName = "__date__"; + public const string TimeStyleName = "__time__"; + + public const string TimeFormat = "[$-x-systime]h:mm:ss AM/PM"; //This format depends on local system settings + public const string DateFormat = "mm-dd-yy"; //This format depends on local system settings + } +} \ No newline at end of file diff --git a/src/Kevull.MultiHeader.EPPLus/ConfigurationBuilder.cs b/src/Kevull.MultiHeader.EPPLus/ConfigurationBuilder.cs index f4a4415..bf7d14b 100644 --- a/src/Kevull.MultiHeader.EPPLus/ConfigurationBuilder.cs +++ b/src/Kevull.MultiHeader.EPPLus/ConfigurationBuilder.cs @@ -1,4 +1,5 @@ -using Kevull.MultiHeader.EPPLus.Columns; +using Kevull.MultiHeader.Core; +using Kevull.MultiHeader.Core.Columns; using OfficeOpenXml; using OfficeOpenXml.Style; using System.Linq.Expressions; diff --git a/src/Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj b/src/Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj index 9a4a8b7..2a93d2b 100644 --- a/src/Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj +++ b/src/Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj @@ -41,7 +41,7 @@ - <_Parameter1>Kevull.MultiHeader.EPPLus.Test + <_Parameter1>Kevull.MultiHeader.EPPLus.Tests diff --git a/src/Kevull.MultiHeader.EPPLus/MultiHeaderReport.cs b/src/Kevull.MultiHeader.EPPLus/MultiHeaderReport.cs index fba52a6..287776f 100644 --- a/src/Kevull.MultiHeader.EPPLus/MultiHeaderReport.cs +++ b/src/Kevull.MultiHeader.EPPLus/MultiHeaderReport.cs @@ -1,5 +1,6 @@ -using Kevull.MultiHeader.Core; -using Kevull.MultiHeader.EPPLus.Columns; +using Kevull.MultiHeader.Code; +using Kevull.MultiHeader.Core; +using Kevull.MultiHeader.Core.Columns; using OfficeOpenXml; using System.Linq; using System.Reflection; @@ -263,16 +264,5 @@ private void BuildTimeStyle() _writer.CreateNamedStyle(StyleNames.TimeStyleName, format); } } - - } - - internal class StyleNames - { - public const string HeaderStyleName = "__Headers__"; - public const string DateStyleName = "__date__"; - public const string TimeStyleName = "__time__"; - - 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.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/tests/Kevull.MultiHeader.EPPLus.Test/HeaderManagerTest.cs b/tests/Kevull.MultiHeader.Core.Tests/HeaderManagerTest.cs similarity index 90% rename from tests/Kevull.MultiHeader.EPPLus.Test/HeaderManagerTest.cs rename to tests/Kevull.MultiHeader.Core.Tests/HeaderManagerTest.cs index d47acdd..c73f1c4 100644 --- a/tests/Kevull.MultiHeader.EPPLus.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.MultiHeader.EPPLus.Test +namespace Kevull.MultiHeader.Core.Tests { diff --git a/tests/Kevull.MultiHeader.Core.Tests/Kevull.MultiHeader.Core.Tests.csproj b/tests/Kevull.MultiHeader.Core.Tests/Kevull.MultiHeader.Core.Tests.csproj new file mode 100644 index 0000000..49c156e --- /dev/null +++ b/tests/Kevull.MultiHeader.Core.Tests/Kevull.MultiHeader.Core.Tests.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + enable + enable + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/tests/Kevull.MultiHeader.EPPLus.Test/PropertyNameBuilderTest.cs b/tests/Kevull.MultiHeader.Core.Tests/PropertyNameBuilderTest.cs similarity index 94% rename from tests/Kevull.MultiHeader.EPPLus.Test/PropertyNameBuilderTest.cs rename to tests/Kevull.MultiHeader.Core.Tests/PropertyNameBuilderTest.cs index 3e843dc..c0dd70a 100644 --- a/tests/Kevull.MultiHeader.EPPLus.Test/PropertyNameBuilderTest.cs +++ b/tests/Kevull.MultiHeader.Core.Tests/PropertyNameBuilderTest.cs @@ -1,11 +1,12 @@ -using Kevull.MultiHeader.EPPLus.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.MultiHeader.EPPLus.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/tests/Kevull.MultiHeader.EPPLus.Test/ColumnInfoTest.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/ColumnInfoTest.cs similarity index 77% rename from tests/Kevull.MultiHeader.EPPLus.Test/ColumnInfoTest.cs rename to tests/Kevull.MultiHeader.EPPLus.Tests/ColumnInfoTest.cs index ec09d22..a13ce3d 100644 --- a/tests/Kevull.MultiHeader.EPPLus.Test/ColumnInfoTest.cs +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/ColumnInfoTest.cs @@ -1,48 +1,13 @@ -using Kevull.MultiHeader.Core; -using Kevull.MultiHeader.EPPLus.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.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() { diff --git a/tests/Kevull.MultiHeader.EPPLus.Test/EPPlusExcelWriterTests.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/EPPlusExcelWriterTests.cs similarity index 100% rename from tests/Kevull.MultiHeader.EPPLus.Test/EPPlusExcelWriterTests.cs rename to tests/Kevull.MultiHeader.EPPLus.Tests/EPPlusExcelWriterTests.cs diff --git a/tests/Kevull.MultiHeader.EPPLus.Test/FormatTest.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/FormatTest.cs similarity index 97% rename from tests/Kevull.MultiHeader.EPPLus.Test/FormatTest.cs rename to tests/Kevull.MultiHeader.EPPLus.Tests/FormatTest.cs index 947022f..5b509e2 100644 --- a/tests/Kevull.MultiHeader.EPPLus.Test/FormatTest.cs +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/FormatTest.cs @@ -1,6 +1,9 @@ -using OfficeOpenXml; +using Kevull.MultiHeader.Code; +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; diff --git a/tests/Kevull.MultiHeader.EPPLus.Test/GeneralConfigurationOptionsTest.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/GeneralConfigurationOptionsTest.cs similarity index 98% rename from tests/Kevull.MultiHeader.EPPLus.Test/GeneralConfigurationOptionsTest.cs rename to tests/Kevull.MultiHeader.EPPLus.Tests/GeneralConfigurationOptionsTest.cs index 77666c0..5a78a6a 100644 --- a/tests/Kevull.MultiHeader.EPPLus.Test/GeneralConfigurationOptionsTest.cs +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/GeneralConfigurationOptionsTest.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Kevull.MultiHeader.TestCommon; namespace Kevull.MultiHeader.EPPLus.Test { diff --git a/tests/Kevull.MultiHeader.EPPLus.Test/Kevull.MultiHeader.EPPLus.Test.csproj b/tests/Kevull.MultiHeader.EPPLus.Tests/Kevull.MultiHeader.EPPLus.Tests.csproj similarity index 84% rename from tests/Kevull.MultiHeader.EPPLus.Test/Kevull.MultiHeader.EPPLus.Test.csproj rename to tests/Kevull.MultiHeader.EPPLus.Tests/Kevull.MultiHeader.EPPLus.Tests.csproj index 1109faf..0f8ed23 100644 --- a/tests/Kevull.MultiHeader.EPPLus.Test/Kevull.MultiHeader.EPPLus.Test.csproj +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/Kevull.MultiHeader.EPPLus.Tests.csproj @@ -23,7 +23,9 @@ + + diff --git a/tests/Kevull.MultiHeader.EPPLus.Test/OneHeaderRenderTest.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/OneHeaderRenderTest.cs similarity index 99% rename from tests/Kevull.MultiHeader.EPPLus.Test/OneHeaderRenderTest.cs rename to tests/Kevull.MultiHeader.EPPLus.Tests/OneHeaderRenderTest.cs index 91e3daf..5dd8fa5 100644 --- a/tests/Kevull.MultiHeader.EPPLus.Test/OneHeaderRenderTest.cs +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/OneHeaderRenderTest.cs @@ -1,6 +1,7 @@ using System.Xml.Linq; using System; using OfficeOpenXml; +using Kevull.MultiHeader.TestCommon; namespace Kevull.MultiHeader.EPPLus.Test { diff --git a/tests/Kevull.MultiHeader.EPPLus.Test/TwoHeaderRenderTest.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/TwoHeaderRenderTest.cs similarity index 99% rename from tests/Kevull.MultiHeader.EPPLus.Test/TwoHeaderRenderTest.cs rename to tests/Kevull.MultiHeader.EPPLus.Tests/TwoHeaderRenderTest.cs index ce9f82d..c2c79dc 100644 --- a/tests/Kevull.MultiHeader.EPPLus.Test/TwoHeaderRenderTest.cs +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/TwoHeaderRenderTest.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using Xunit; +using Kevull.MultiHeader.TestCommon; namespace Kevull.MultiHeader.EPPLus.Test { diff --git a/tests/Kevull.MultiHeader.EPPLus.Test/Usings.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/Usings.cs similarity index 100% rename from tests/Kevull.MultiHeader.EPPLus.Test/Usings.cs rename to tests/Kevull.MultiHeader.EPPLus.Tests/Usings.cs diff --git a/tests/Kevull.MultiHeader.EPPLus.Test/BaseTest.cs b/tests/Kevull.MultiHeader.TestCommon/BaseTest.cs similarity index 100% rename from tests/Kevull.MultiHeader.EPPLus.Test/BaseTest.cs rename to tests/Kevull.MultiHeader.TestCommon/BaseTest.cs diff --git a/tests/Kevull.MultiHeader.EPPLus.Test/ComplexObject.cs b/tests/Kevull.MultiHeader.TestCommon/ComplexObject.cs similarity index 98% rename from tests/Kevull.MultiHeader.EPPLus.Test/ComplexObject.cs rename to tests/Kevull.MultiHeader.TestCommon/ComplexObject.cs index 6dc6395..7d9f566 100644 --- a/tests/Kevull.MultiHeader.EPPLus.Test/ComplexObject.cs +++ b/tests/Kevull.MultiHeader.TestCommon/ComplexObject.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.MultiHeader.EPPLus.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..afc5d12 --- /dev/null +++ b/tests/Kevull.MultiHeader.TestCommon/Kevull.MultiHeader.TestCommon.csproj @@ -0,0 +1,14 @@ + + + + net10.0 + + + + <_Parameter1>Kevull.MultiHeader.Core.Tests + + + <_Parameter1>Kevull.MultiHeader.EPPLus.Tests + + + diff --git a/tests/Kevull.MultiHeader.EPPLus.Test/Person.cs b/tests/Kevull.MultiHeader.TestCommon/Person.cs similarity index 99% rename from tests/Kevull.MultiHeader.EPPLus.Test/Person.cs rename to tests/Kevull.MultiHeader.TestCommon/Person.cs index 6607e8b..fa1ea78 100644 --- a/tests/Kevull.MultiHeader.EPPLus.Test/Person.cs +++ b/tests/Kevull.MultiHeader.TestCommon/Person.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; -namespace Kevull.MultiHeader.EPPLus.Test +namespace Kevull.MultiHeader.TestCommon { internal enum Gender { From 93e4371fd6d64eb4dbdd4faddc8c80ba8653af56 Mon Sep 17 00:00:00 2001 From: mnieto Date: Sun, 3 May 2026 22:01:03 +0200 Subject: [PATCH 09/16] Introduce core interfaces for report config and generation - Added IConfigurationBuilder and IMultiHeaderReport to Kevull.MultiHeader.Core, enabling a library-agnostic, extensible API for report configuration and generation. - Refactored EPPLus implementation to use these interfaces. - Enhanced CellFormat with a Merge method and centralized style mapping logic. - Added a TestCommon project with sample data --- .github/copilot-instructions.md | 10 ++ Kevull.MultiHeader.slnx | 1 + src/Kevull.MultiHeader.Core/CellFormat.cs | 31 ++++ src/Kevull.MultiHeader.Core/ExcelFormats.cs | 1 - .../IConfigurationBuilder.cs | 33 ++++ .../IMultiHeaderReport.cs | 28 ++++ .../ConfigurationBuilder.cs | 74 ++++++--- .../EPPlusExcelWriter.cs | 148 ++++++++++++++++++ .../MultiHeaderReport.cs | 45 ++++-- .../EPPLusBaseTest.cs | 26 +++ .../FormatTest.cs | 11 +- .../Kevull.MultiHeader.TestCommon/BaseTest.cs | 10 +- .../Kevull.MultiHeader.TestCommon.csproj | 3 + 13 files changed, 380 insertions(+), 41 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 src/Kevull.MultiHeader.Core/IConfigurationBuilder.cs create mode 100644 src/Kevull.MultiHeader.Core/IMultiHeaderReport.cs create mode 100644 tests/Kevull.MultiHeader.EPPLus.Tests/EPPLusBaseTest.cs 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/Kevull.MultiHeader.slnx b/Kevull.MultiHeader.slnx index 902fa30..465f3d6 100644 --- a/Kevull.MultiHeader.slnx +++ b/Kevull.MultiHeader.slnx @@ -3,4 +3,5 @@ + diff --git a/src/Kevull.MultiHeader.Core/CellFormat.cs b/src/Kevull.MultiHeader.Core/CellFormat.cs index 5bc04dd..a56f2f5 100644 --- a/src/Kevull.MultiHeader.Core/CellFormat.cs +++ b/src/Kevull.MultiHeader.Core/CellFormat.cs @@ -84,5 +84,36 @@ public class CellFormat /// 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.MultiHeader.Core/ExcelFormats.cs b/src/Kevull.MultiHeader.Core/ExcelFormats.cs index c52669b..4fe7a35 100644 --- a/src/Kevull.MultiHeader.Core/ExcelFormats.cs +++ b/src/Kevull.MultiHeader.Core/ExcelFormats.cs @@ -90,7 +90,6 @@ public ExcelColor(string hex) throw new ArgumentException($"Invalid hex color format: {hex}", nameof(hex)); } - /// /// Converts the color to a hex string (e.g., "#RRGGBB") /// diff --git a/src/Kevull.MultiHeader.Core/IConfigurationBuilder.cs b/src/Kevull.MultiHeader.Core/IConfigurationBuilder.cs new file mode 100644 index 0000000..f67d64c --- /dev/null +++ b/src/Kevull.MultiHeader.Core/IConfigurationBuilder.cs @@ -0,0 +1,33 @@ +using Kevull.MultiHeader.Core; +using Kevull.MultiHeader.Core.Columns; +using System.Linq.Expressions; + +namespace Kevull.MultiHeader.Core +{ + public interface IConfigurationBuilder + { + bool AppendToExistingReport { get; set; } + bool AutoFilter { get; set; } + bool AutoFreezePanes { get; set; } + int LeftColumn { get; } + int TopRow { get; } + + IConfigurationBuilder AddColumn(Expression> columnSelector); + IConfigurationBuilder AddColumn(Expression> columnSelector, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null); + IConfigurationBuilder AddColumn(Expression> columnSelector, Action cfg); + IConfigurationBuilder AddEnumeration(Expression> columnSelector, IEnumerable keyValues, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null); + IConfigurationBuilder AddEnumeration(Expression> columnSelector, IEnumerable keyValues, Action cfg); + IConfigurationBuilder AddExpression(string name, Func expression, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null); + IConfigurationBuilder AddExpression(string name, Func expression, Action cfg); + IConfigurationBuilder AddFormula(string name, string formula, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null); + IConfigurationBuilder AddFormula(string name, string formula, Action cfg); + IConfigurationBuilder AddHeaderStyle(Action style); + IConfigurationBuilder AddHyperLinkColumn(Expression> columnSelector, Expression> urlColumnSelector, int? order = null, string? displayName = null, bool hidden = false, string? styleName = null); + IConfigurationBuilder AddHyperLinkColumn(Expression> columnSelector, Expression> urlColumnSelector, Action cfg); + IConfigurationBuilder AddNamedStyle(string name, Action style); + HeaderManager Build(); + IConfigurationBuilder IgnoreColumn(Expression> columnSelector); + IConfigurationBuilder SetStartingAddres(int row, int column); + IConfigurationBuilder SetStartingAddress(string address); + } +} \ No newline at end of file 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.EPPLus/ConfigurationBuilder.cs b/src/Kevull.MultiHeader.EPPLus/ConfigurationBuilder.cs index bf7d14b..405aef9 100644 --- a/src/Kevull.MultiHeader.EPPLus/ConfigurationBuilder.cs +++ b/src/Kevull.MultiHeader.EPPLus/ConfigurationBuilder.cs @@ -12,11 +12,12 @@ 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(); + public Dictionary NamedStyles = new Dictionary(); /// /// Shows or not autofilter on last header row @@ -74,7 +75,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; @@ -88,7 +89,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; @@ -99,7 +100,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; @@ -114,7 +115,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; @@ -126,7 +127,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; @@ -141,7 +142,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; @@ -153,7 +154,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; @@ -168,7 +169,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; @@ -180,7 +181,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; @@ -195,7 +196,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; @@ -207,7 +208,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; @@ -217,12 +218,35 @@ 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; } + ///// + ///// Adds a custom header style. If not specified, a default one will be applyed + ///// + ///// Lambda expresion to define the style + ///// The default style is defined as below + ///// + ///// + ///// var namedStyle = _xls.Workbook.Styles.CreateNamedStyle("Headers"); + ///// 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; + ///// + ///// + //public ConfigurationBuilder AddHeaderStyle(Action style) + //{ + // return AddNamedStyle(MultiHeaderReport.HeaderStyleName, style); + //} + /// /// Adds a custom header style. If not specified, a default one will be applyed /// @@ -241,20 +265,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); } + ///// + ///// 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; } @@ -262,7 +298,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; @@ -273,7 +309,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 index f32298d..4b06478 100644 --- a/src/Kevull.MultiHeader.EPPLus/EPPlusExcelWriter.cs +++ b/src/Kevull.MultiHeader.EPPLus/EPPlusExcelWriter.cs @@ -341,4 +341,152 @@ private Color ConvertColor(CoreExcelColor excelColor) #endregion } + + 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.MultiHeader.EPPLus/MultiHeaderReport.cs b/src/Kevull.MultiHeader.EPPLus/MultiHeaderReport.cs index 287776f..428a772 100644 --- a/src/Kevull.MultiHeader.EPPLus/MultiHeaderReport.cs +++ b/src/Kevull.MultiHeader.EPPLus/MultiHeaderReport.cs @@ -11,7 +11,7 @@ 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; @@ -27,6 +27,12 @@ public class MultiHeaderReport /// protected HeaderManager? _header; + /// + /// 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 = "__Headers__"; /// @@ -51,18 +57,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; } @@ -78,7 +85,8 @@ public void GenerateReport(IEnumerable data) if (_header == null) { _header = new HeaderManager(); - } else + } + else { _header.BuildHeaders(); } @@ -95,7 +103,8 @@ public void GenerateReport(IEnumerable data) CalulateFormulas(); } - internal void Save(string fileName) + /// + public void Save(string fileName) { _xls.SaveAs(fileName); } @@ -131,7 +140,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) { @@ -166,7 +175,7 @@ private void DoFormatting() _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)) { _writer.SetColumnHidden(columnInfo.Index, true); } @@ -177,7 +186,7 @@ private void DoFormatting() _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; @@ -189,12 +198,13 @@ private void DoFormatting() } //Styles - BuildDefaultHeaderStyle(); BuildDateStyle(); BuildTimeStyle(); + BuildColumnStyles(); if (!_header!.AppendToExistingReport) { + BuildDefaultHeaderStyle(); _writer.ApplyNamedStyle(_header.FirstRow, _header!.Columns.Min(x => x.Index), lastHeaderRow, lastHeaderColumn, StyleNames.HeaderStyleName); } @@ -237,6 +247,10 @@ private void BuildDefaultHeaderStyle() FillStyle = FillStyle.Solid, Bold = true }; + + //If the user has defined a custom header style, merge it with the default one + if (_namedStyles.ContainsKey(HeaderStyleName)) + format.Merge(_namedStyles[HeaderStyleName]); _writer.CreateNamedStyle(StyleNames.HeaderStyleName, format); } } @@ -264,5 +278,18 @@ private void BuildTimeStyle() _writer.CreateNamedStyle(StyleNames.TimeStyleName, format); } } + + 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 != HeaderStyleName && !_writer.NamedStyleExists(style)) + { + _writer.CreateNamedStyle(style, _namedStyles[style]); + } + } + } + } } \ No newline at end of file diff --git a/tests/Kevull.MultiHeader.EPPLus.Tests/EPPLusBaseTest.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/EPPLusBaseTest.cs new file mode 100644 index 0000000..e0120f6 --- /dev/null +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/EPPLusBaseTest.cs @@ -0,0 +1,26 @@ +using OfficeOpenXml; +using System.Reflection; + +namespace Kevull.MultiHeader.EPPLus.Test +{ + public class BaseTest + { + public BaseTest() + { + ExcelPackage.License.SetNonCommercialPersonal("Kevull"); + } + + protected void Save(MultiHeaderReport report, [System.Runtime.CompilerServices.CallerMemberName] string methodName = "") + { + report.Save(string.Concat(methodName, ".xlsx")); + } + + protected string GetTestAssemblyFolder() + { + string fullPath = Assembly.GetExecutingAssembly().Location; + string name = Assembly.GetExecutingAssembly().GetName().Name!; + int index = fullPath.IndexOf(name); + return Path.Combine(fullPath.Substring(0, index), name); + } + } +} \ No newline at end of file diff --git a/tests/Kevull.MultiHeader.EPPLus.Tests/FormatTest.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/FormatTest.cs index 5b509e2..6f809e0 100644 --- a/tests/Kevull.MultiHeader.EPPLus.Tests/FormatTest.cs +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/FormatTest.cs @@ -38,15 +38,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] @@ -86,8 +87,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/tests/Kevull.MultiHeader.TestCommon/BaseTest.cs b/tests/Kevull.MultiHeader.TestCommon/BaseTest.cs index e0120f6..49028fc 100644 --- a/tests/Kevull.MultiHeader.TestCommon/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.MultiHeader.EPPLus.Test { 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/tests/Kevull.MultiHeader.TestCommon/Kevull.MultiHeader.TestCommon.csproj b/tests/Kevull.MultiHeader.TestCommon/Kevull.MultiHeader.TestCommon.csproj index afc5d12..ebd18b9 100644 --- a/tests/Kevull.MultiHeader.TestCommon/Kevull.MultiHeader.TestCommon.csproj +++ b/tests/Kevull.MultiHeader.TestCommon/Kevull.MultiHeader.TestCommon.csproj @@ -11,4 +11,7 @@ <_Parameter1>Kevull.MultiHeader.EPPLus.Tests + + + From 18971b678c5a5392ca9db4861c372fd891327fd8 Mon Sep 17 00:00:00 2001 From: mnieto Date: Fri, 8 May 2026 23:02:13 +0200 Subject: [PATCH 10/16] add missing xml documentation --- .../Columns/ColumnEnumeration.cs | 3 + .../Columns/ColumnExpression.cs | 1 + .../Columns/ColumnFormula.cs | 2 + .../Columns/ColumnHyperLink.cs | 1 + .../Columns/ColumnInfo.cs | 36 +++- .../IConfigurationBuilder.cs | 160 +++++++++++++++++- src/Kevull.MultiHeader.Core/StyleNames.cs | 31 +++- .../ConfigurationBuilder.cs | 4 + .../EPPlusExcelWriter.cs | 3 + 9 files changed, 235 insertions(+), 6 deletions(-) diff --git a/src/Kevull.MultiHeader.Core/Columns/ColumnEnumeration.cs b/src/Kevull.MultiHeader.Core/Columns/ColumnEnumeration.cs index ab15e5d..7002bd4 100644 --- a/src/Kevull.MultiHeader.Core/Columns/ColumnEnumeration.cs +++ b/src/Kevull.MultiHeader.Core/Columns/ColumnEnumeration.cs @@ -74,6 +74,7 @@ internal ColumnEnumeration(string name, IEnumerable keyValues, int? orde _keyValues = AddKeyValues(keyValues); } + /// public override void FormatHeader(IExcelWriter writer, int row, int col, int height) { // Merge the parent header across all columns @@ -87,6 +88,7 @@ public override void FormatHeader(IExcelWriter writer, int row, int col, int hei } } + /// public override void WriteCell(IExcelWriter writer, int row, int col, Dictionary properties, object? obj) { if (obj == null) @@ -118,6 +120,7 @@ public override void WriteCell(IExcelWriter writer, int row, int col, Dictionary } } + /// public override void WriteHeader(IExcelWriter writer, int row, int col) { // Write parent header diff --git a/src/Kevull.MultiHeader.Core/Columns/ColumnExpression.cs b/src/Kevull.MultiHeader.Core/Columns/ColumnExpression.cs index 33daa7f..e95517d 100644 --- a/src/Kevull.MultiHeader.Core/Columns/ColumnExpression.cs +++ b/src/Kevull.MultiHeader.Core/Columns/ColumnExpression.cs @@ -56,6 +56,7 @@ public ColumnExpression(string name, Func expression, Action public override void WriteCell(IExcelWriter writer, int row, int col, Dictionary properties, object? obj) { if (obj is null) diff --git a/src/Kevull.MultiHeader.Core/Columns/ColumnFormula.cs b/src/Kevull.MultiHeader.Core/Columns/ColumnFormula.cs index 3c5bd5b..158188a 100644 --- a/src/Kevull.MultiHeader.Core/Columns/ColumnFormula.cs +++ b/src/Kevull.MultiHeader.Core/Columns/ColumnFormula.cs @@ -57,11 +57,13 @@ public ColumnFormula(string name, string formula, Action cfg) _formula = formula; } + /// public override void WriteCell(IExcelWriter writer, int row, int col, Dictionary properties, object? obj) { 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 diff --git a/src/Kevull.MultiHeader.Core/Columns/ColumnHyperLink.cs b/src/Kevull.MultiHeader.Core/Columns/ColumnHyperLink.cs index 7ba73a6..01acf87 100644 --- a/src/Kevull.MultiHeader.Core/Columns/ColumnHyperLink.cs +++ b/src/Kevull.MultiHeader.Core/Columns/ColumnHyperLink.cs @@ -51,6 +51,7 @@ public ColumnHyperLink(Expression> columnSelector, Expression public override void WriteCell(IExcelWriter writer, int row, int col, Dictionary properties, object? obj) { if (obj == null) diff --git a/src/Kevull.MultiHeader.Core/Columns/ColumnInfo.cs b/src/Kevull.MultiHeader.Core/Columns/ColumnInfo.cs index f64a3c8..6ca631c 100644 --- a/src/Kevull.MultiHeader.Core/Columns/ColumnInfo.cs +++ b/src/Kevull.MultiHeader.Core/Columns/ColumnInfo.cs @@ -119,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; } @@ -204,17 +203,42 @@ internal ColumnInfo(PropertyNames names, int? order = null, string? displayName _displayName = displayName; } + /// + /// 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) { writer.Merge(row, col, row + height - 1, col + Width - 1); } + /// + /// 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) writer.WriteCell(row, col, properties[Name].GetValue(obj)); } + /// + /// 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 @@ -230,6 +254,12 @@ public virtual void WriteCell(IExcelWriter writer, int fromRow, int fromCol, int 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) { writer.WriteCell(row, col, DisplayName); diff --git a/src/Kevull.MultiHeader.Core/IConfigurationBuilder.cs b/src/Kevull.MultiHeader.Core/IConfigurationBuilder.cs index f67d64c..689ac52 100644 --- a/src/Kevull.MultiHeader.Core/IConfigurationBuilder.cs +++ b/src/Kevull.MultiHeader.Core/IConfigurationBuilder.cs @@ -4,30 +4,188 @@ 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); - HeaderManager Build(); + + /// + /// 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/StyleNames.cs b/src/Kevull.MultiHeader.Core/StyleNames.cs index 1f43b16..5061e77 100644 --- a/src/Kevull.MultiHeader.Core/StyleNames.cs +++ b/src/Kevull.MultiHeader.Core/StyleNames.cs @@ -1,12 +1,39 @@ namespace Kevull.MultiHeader.Code { + /// + /// 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__"; - public const string TimeFormat = "[$-x-systime]h:mm:ss AM/PM"; //This format depends on local system settings - public const string DateFormat = "mm-dd-yy"; //This format depends on local system settings + /// + /// 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.MultiHeader.EPPLus/ConfigurationBuilder.cs b/src/Kevull.MultiHeader.EPPLus/ConfigurationBuilder.cs index 405aef9..bf486e0 100644 --- a/src/Kevull.MultiHeader.EPPLus/ConfigurationBuilder.cs +++ b/src/Kevull.MultiHeader.EPPLus/ConfigurationBuilder.cs @@ -17,6 +17,10 @@ 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(); /// diff --git a/src/Kevull.MultiHeader.EPPLus/EPPlusExcelWriter.cs b/src/Kevull.MultiHeader.EPPLus/EPPlusExcelWriter.cs index 4b06478..4f60414 100644 --- a/src/Kevull.MultiHeader.EPPLus/EPPlusExcelWriter.cs +++ b/src/Kevull.MultiHeader.EPPLus/EPPlusExcelWriter.cs @@ -342,6 +342,9 @@ private Color ConvertColor(CoreExcelColor excelColor) #endregion } + /// + /// Extension methods to apply library-agnostic cell format definitions to EPPlus styles. + /// public static class ExcelStyleExtensions { /// From 9e2eaaf6a9d00e70c0c8cfaabda1759e7256a3de Mon Sep 17 00:00:00 2001 From: mnieto Date: Fri, 8 May 2026 23:25:04 +0200 Subject: [PATCH 11/16] fix warnings, including vulnerable transitive dependency --- .../Kevull.MultiHeader.EPPLus.csproj | 2 +- .../EPPLusBaseTest.cs | 17 ++--------------- .../EPPlusExcelWriterTests.cs | 2 +- .../OneHeaderRenderTest.cs | 2 +- .../Kevull.MultiHeader.TestCommon.csproj | 1 + 5 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj b/src/Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj index 2a93d2b..8997f27 100644 --- a/src/Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj +++ b/src/Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj @@ -32,7 +32,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/tests/Kevull.MultiHeader.EPPLus.Tests/EPPLusBaseTest.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/EPPLusBaseTest.cs index e0120f6..bf0656f 100644 --- a/tests/Kevull.MultiHeader.EPPLus.Tests/EPPLusBaseTest.cs +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/EPPLusBaseTest.cs @@ -3,24 +3,11 @@ namespace Kevull.MultiHeader.EPPLus.Test { - public class BaseTest + public class EPPLusBaseTest : BaseTest { - public BaseTest() + public EPPLusBaseTest() { ExcelPackage.License.SetNonCommercialPersonal("Kevull"); } - - protected void Save(MultiHeaderReport report, [System.Runtime.CompilerServices.CallerMemberName] string methodName = "") - { - report.Save(string.Concat(methodName, ".xlsx")); - } - - protected string GetTestAssemblyFolder() - { - string fullPath = Assembly.GetExecutingAssembly().Location; - string name = Assembly.GetExecutingAssembly().GetName().Name!; - int index = fullPath.IndexOf(name); - return Path.Combine(fullPath.Substring(0, index), name); - } } } \ No newline at end of file diff --git a/tests/Kevull.MultiHeader.EPPLus.Tests/EPPlusExcelWriterTests.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/EPPlusExcelWriterTests.cs index c9459ef..c740947 100644 --- a/tests/Kevull.MultiHeader.EPPLus.Tests/EPPlusExcelWriterTests.cs +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/EPPlusExcelWriterTests.cs @@ -12,7 +12,7 @@ namespace Kevull.MultiHeader.EPPLus.Test /// /// Unit tests for EPPlusExcelWriter class /// - public partial class EPPlusExcelWriterTests : BaseTest + public partial class EPPlusExcelWriterTests : BaseTest, IDisposable { private readonly ExcelPackage _package; private readonly ExcelWorksheet _worksheet; diff --git a/tests/Kevull.MultiHeader.EPPLus.Tests/OneHeaderRenderTest.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/OneHeaderRenderTest.cs index 5dd8fa5..ec0cf86 100644 --- a/tests/Kevull.MultiHeader.EPPLus.Tests/OneHeaderRenderTest.cs +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/OneHeaderRenderTest.cs @@ -5,7 +5,7 @@ namespace Kevull.MultiHeader.EPPLus.Test { - public class OneHeaderRenderTest : BaseTest + public class OneHeaderRenderTest : EPPLusBaseTest { private int maxColumns; public OneHeaderRenderTest() : base() diff --git a/tests/Kevull.MultiHeader.TestCommon/Kevull.MultiHeader.TestCommon.csproj b/tests/Kevull.MultiHeader.TestCommon/Kevull.MultiHeader.TestCommon.csproj index ebd18b9..a7ff626 100644 --- a/tests/Kevull.MultiHeader.TestCommon/Kevull.MultiHeader.TestCommon.csproj +++ b/tests/Kevull.MultiHeader.TestCommon/Kevull.MultiHeader.TestCommon.csproj @@ -2,6 +2,7 @@ net10.0 + enable From 89159d693158d557063e183d5c69dcda1086ae3f Mon Sep 17 00:00:00 2001 From: mnieto Date: Sat, 9 May 2026 11:26:08 +0200 Subject: [PATCH 12/16] Add hollow new projects for ClosedXml implementation --- Kevull.MultiHeader.slnx | 10 +++- .../Kevull.MultiHeader.ClosedXml.csproj | 58 +++++++++++++++++++ .../Kevull.MultiHeader.EPPLus.csproj | 4 +- .../Kevull.MultiHeader.ClosedXml.Tests.csproj | 31 ++++++++++ 4 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 src/Kevull.MultiHeader.ClosedXml/Kevull.MultiHeader.ClosedXml.csproj create mode 100644 tests/Kevull.MultiHeader.ClosedXml.Tests/Kevull.MultiHeader.ClosedXml.Tests.csproj diff --git a/Kevull.MultiHeader.slnx b/Kevull.MultiHeader.slnx index 465f3d6..dfb7208 100644 --- a/Kevull.MultiHeader.slnx +++ b/Kevull.MultiHeader.slnx @@ -1,7 +1,11 @@ + + + + + + + - - - 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.EPPLus/Kevull.MultiHeader.EPPLus.csproj b/src/Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj index 8997f27..b2f63bb 100644 --- a/src/Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj +++ b/src/Kevull.MultiHeader.EPPLus/Kevull.MultiHeader.EPPLus.csproj @@ -16,8 +16,8 @@ MNieto EPlus multi-header report Extension for the EPPlus library to create reports from complex objects - https://github.com/mnieto/MultiHeader.EPPlus - https://github.com/mnieto/MultiHeader.EPPlus + https://github.com/mnieto/Kevull.MultiHeader + https://github.com/mnieto/Kevull.MultiHeader git EPPlus;Excel;Multi-header;Report LGPL-3.0-or-later 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 + + + + + + + + + + From ca84bcc9ba9bdcd3d881215e125cbc5c2a4c1038 Mon Sep 17 00:00:00 2001 From: mnieto Date: Sat, 9 May 2026 14:11:36 +0200 Subject: [PATCH 13/16] Add ClosedXML backend and tests for MultiHeader Introduces a new ClosedXML implementation for the MultiHeader library, including ClosedXmlExcelWriter, ConfigurationBuilder, and MultiHeaderReport classes. Adds a comprehensive test suite in Kevull.MultiHeader.ClosedXml.Tests, mirroring the EPPlus tests to ensure behavior parity. Updates project files for internal visibility, refactors namespaces for consistency, and documents the implementation approach. This enables MultiHeader to support both EPPlus and ClosedXML backends with consistent APIs and test coverage. --- README.md | 17 +- .../ClosedXmlExcelWriter.cs | 325 ++++++++++++++++++ .../ConfigurationBuilder.cs | 227 ++++++++++++ .../MultiHeaderReport.cs | 282 +++++++++++++++ src/Kevull.MultiHeader.Core/HeaderManager.cs | 2 +- .../Kevull.MultiHeader.Core.csproj | 3 + src/Kevull.MultiHeader.Core/StyleNames.cs | 2 +- .../ConfigurationBuilder.cs | 25 +- .../MultiHeaderReport.cs | 11 +- .../ClosedXmlExcelWriterTests.cs | 299 ++++++++++++++++ .../ColumnInfoTest.cs | 109 ++++++ .../FormatTest.cs | 126 +++++++ .../GeneralConfigurationOptionsTest.cs | 83 +++++ .../OneHeaderRenderTest.cs | 131 +++++++ .../TwoHeaderRenderTest.cs | 80 +++++ .../Usings.cs | 2 + .../FormatTest.cs | 5 +- .../GeneralConfigurationOptionsTest.cs | 2 +- .../TwoHeaderRenderTest.cs | 2 +- .../Kevull.MultiHeader.EPPLus.Tests/Usings.cs | 3 +- .../Kevull.MultiHeader.TestCommon/BaseTest.cs | 2 +- .../Kevull.MultiHeader.TestCommon.csproj | 3 + 22 files changed, 1699 insertions(+), 42 deletions(-) create mode 100644 src/Kevull.MultiHeader.ClosedXml/ClosedXmlExcelWriter.cs create mode 100644 src/Kevull.MultiHeader.ClosedXml/ConfigurationBuilder.cs create mode 100644 src/Kevull.MultiHeader.ClosedXml/MultiHeaderReport.cs create mode 100644 tests/Kevull.MultiHeader.ClosedXml.Tests/ClosedXmlExcelWriterTests.cs create mode 100644 tests/Kevull.MultiHeader.ClosedXml.Tests/ColumnInfoTest.cs create mode 100644 tests/Kevull.MultiHeader.ClosedXml.Tests/FormatTest.cs create mode 100644 tests/Kevull.MultiHeader.ClosedXml.Tests/GeneralConfigurationOptionsTest.cs create mode 100644 tests/Kevull.MultiHeader.ClosedXml.Tests/OneHeaderRenderTest.cs create mode 100644 tests/Kevull.MultiHeader.ClosedXml.Tests/TwoHeaderRenderTest.cs create mode 100644 tests/Kevull.MultiHeader.ClosedXml.Tests/Usings.cs 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.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/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/HeaderManager.cs b/src/Kevull.MultiHeader.Core/HeaderManager.cs index 297f41c..3417e80 100644 --- a/src/Kevull.MultiHeader.Core/HeaderManager.cs +++ b/src/Kevull.MultiHeader.Core/HeaderManager.cs @@ -1,4 +1,4 @@ -using Kevull.MultiHeader.Code; +using Kevull.MultiHeader.Core; using Kevull.MultiHeader.Core.Columns; using System; using System.Collections; diff --git a/src/Kevull.MultiHeader.Core/Kevull.MultiHeader.Core.csproj b/src/Kevull.MultiHeader.Core/Kevull.MultiHeader.Core.csproj index bcba50b..f8d9a55 100644 --- a/src/Kevull.MultiHeader.Core/Kevull.MultiHeader.Core.csproj +++ b/src/Kevull.MultiHeader.Core/Kevull.MultiHeader.Core.csproj @@ -40,6 +40,9 @@ <_Parameter1>Kevull.MultiHeader.Core.Tests + + <_Parameter1>Kevull.MultiHeader.ClosedXml.Tests + diff --git a/src/Kevull.MultiHeader.Core/StyleNames.cs b/src/Kevull.MultiHeader.Core/StyleNames.cs index 5061e77..f2367b7 100644 --- a/src/Kevull.MultiHeader.Core/StyleNames.cs +++ b/src/Kevull.MultiHeader.Core/StyleNames.cs @@ -1,4 +1,4 @@ -namespace Kevull.MultiHeader.Code +namespace Kevull.MultiHeader.Core { /// /// Contains names and number formats used by default report styles. diff --git a/src/Kevull.MultiHeader.EPPLus/ConfigurationBuilder.cs b/src/Kevull.MultiHeader.EPPLus/ConfigurationBuilder.cs index bf486e0..36db356 100644 --- a/src/Kevull.MultiHeader.EPPLus/ConfigurationBuilder.cs +++ b/src/Kevull.MultiHeader.EPPLus/ConfigurationBuilder.cs @@ -228,29 +228,6 @@ public IConfigurationBuilder IgnoreColumn(Expression> column return this; } - ///// - ///// Adds a custom header style. If not specified, a default one will be applyed - ///// - ///// Lambda expresion to define the style - ///// The default style is defined as below - ///// - ///// - ///// var namedStyle = _xls.Workbook.Styles.CreateNamedStyle("Headers"); - ///// 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; - ///// - ///// - //public ConfigurationBuilder AddHeaderStyle(Action style) - //{ - // return AddNamedStyle(MultiHeaderReport.HeaderStyleName, style); - //} - /// /// Adds a custom header style. If not specified, a default one will be applyed /// @@ -271,7 +248,7 @@ public IConfigurationBuilder IgnoreColumn(Expression> column /// public IConfigurationBuilder AddHeaderStyle(Action style) { - return AddNamedStyle(MultiHeaderReport.HeaderStyleName, style); + return AddNamedStyle(StyleNames.HeaderStyleName, style); } ///// diff --git a/src/Kevull.MultiHeader.EPPLus/MultiHeaderReport.cs b/src/Kevull.MultiHeader.EPPLus/MultiHeaderReport.cs index 428a772..5b1d87b 100644 --- a/src/Kevull.MultiHeader.EPPLus/MultiHeaderReport.cs +++ b/src/Kevull.MultiHeader.EPPLus/MultiHeaderReport.cs @@ -1,5 +1,4 @@ -using Kevull.MultiHeader.Code; -using Kevull.MultiHeader.Core; +using Kevull.MultiHeader.Core; using Kevull.MultiHeader.Core.Columns; using OfficeOpenXml; using System.Linq; @@ -33,7 +32,7 @@ public class MultiHeaderReport : IMultiHeaderReport /// protected Dictionary _namedStyles = []; - internal const string HeaderStyleName = "__Headers__"; + //internal const string HeaderStyleName = StyleNames.HeaderStyleName; /// /// Object properties associated to the columns @@ -249,8 +248,8 @@ private void BuildDefaultHeaderStyle() }; //If the user has defined a custom header style, merge it with the default one - if (_namedStyles.ContainsKey(HeaderStyleName)) - format.Merge(_namedStyles[HeaderStyleName]); + if (_namedStyles.ContainsKey(StyleNames.HeaderStyleName)) + format.Merge(_namedStyles[StyleNames.HeaderStyleName]); _writer.CreateNamedStyle(StyleNames.HeaderStyleName, format); } } @@ -284,7 +283,7 @@ 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 != HeaderStyleName && !_writer.NamedStyleExists(style)) + if (style != StyleNames.HeaderStyleName && !_writer.NamedStyleExists(style)) { _writer.CreateNamedStyle(style, _namedStyles[style]); } 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..e22b508 --- /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 + { + [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..96d7fbf --- /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 + { + [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/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..38cff63 --- /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 + { + [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.EPPLus.Tests/FormatTest.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/FormatTest.cs index 6f809e0..a2d3501 100644 --- a/tests/Kevull.MultiHeader.EPPLus.Tests/FormatTest.cs +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/FormatTest.cs @@ -1,5 +1,4 @@ -using Kevull.MultiHeader.Code; -using Kevull.MultiHeader.Core; +using Kevull.MultiHeader.Core; using OfficeOpenXml; using System; using System.Collections.Generic; @@ -12,7 +11,7 @@ namespace Kevull.MultiHeader.EPPLus.Test { - public class FormatTest : BaseTest + public class FormatTest : EPPLusBaseTest { [Fact] public void PropertiesWithoutChildren_HasVerticalMerge() diff --git a/tests/Kevull.MultiHeader.EPPLus.Tests/GeneralConfigurationOptionsTest.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/GeneralConfigurationOptionsTest.cs index 5a78a6a..903b1f3 100644 --- a/tests/Kevull.MultiHeader.EPPLus.Tests/GeneralConfigurationOptionsTest.cs +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/GeneralConfigurationOptionsTest.cs @@ -7,7 +7,7 @@ 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/TwoHeaderRenderTest.cs b/tests/Kevull.MultiHeader.EPPLus.Tests/TwoHeaderRenderTest.cs index c2c79dc..7dc822c 100644 --- a/tests/Kevull.MultiHeader.EPPLus.Tests/TwoHeaderRenderTest.cs +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/TwoHeaderRenderTest.cs @@ -10,7 +10,7 @@ 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 index 13ce7ac..7884853 100644 --- a/tests/Kevull.MultiHeader.EPPLus.Tests/Usings.cs +++ b/tests/Kevull.MultiHeader.EPPLus.Tests/Usings.cs @@ -1,2 +1,3 @@ global using Xunit; -global using OfficeOpenXml; \ No newline at end of file +global using OfficeOpenXml; +global using Kevull.MultiHeader.TestCommon; \ No newline at end of file diff --git a/tests/Kevull.MultiHeader.TestCommon/BaseTest.cs b/tests/Kevull.MultiHeader.TestCommon/BaseTest.cs index 49028fc..77e3740 100644 --- a/tests/Kevull.MultiHeader.TestCommon/BaseTest.cs +++ b/tests/Kevull.MultiHeader.TestCommon/BaseTest.cs @@ -2,7 +2,7 @@ using System.IO; using System.Reflection; -namespace Kevull.MultiHeader.EPPLus.Test +namespace Kevull.MultiHeader.TestCommon { public class BaseTest { diff --git a/tests/Kevull.MultiHeader.TestCommon/Kevull.MultiHeader.TestCommon.csproj b/tests/Kevull.MultiHeader.TestCommon/Kevull.MultiHeader.TestCommon.csproj index a7ff626..1e36c3e 100644 --- a/tests/Kevull.MultiHeader.TestCommon/Kevull.MultiHeader.TestCommon.csproj +++ b/tests/Kevull.MultiHeader.TestCommon/Kevull.MultiHeader.TestCommon.csproj @@ -11,6 +11,9 @@ <_Parameter1>Kevull.MultiHeader.EPPLus.Tests + + <_Parameter1>Kevull.MultiHeader.ClosedXml.Tests + From f34d03c97f053f71e33d7e58aa920d5663d3b5b8 Mon Sep 17 00:00:00 2001 From: mnieto Date: Sat, 9 May 2026 20:33:04 +0200 Subject: [PATCH 14/16] minor fixes --- src/Kevull.MultiHeader.Core/Columns/ColumnDef.cs | 4 ++-- tests/Kevull.MultiHeader.ClosedXml.Tests/FormatTest.cs | 2 +- .../GeneralConfigurationOptionsTest.cs | 2 +- .../Kevull.MultiHeader.ClosedXml.Tests/TwoHeaderRenderTest.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Kevull.MultiHeader.Core/Columns/ColumnDef.cs b/src/Kevull.MultiHeader.Core/Columns/ColumnDef.cs index 380ad3e..aaf6722 100644 --- a/src/Kevull.MultiHeader.Core/Columns/ColumnDef.cs +++ b/src/Kevull.MultiHeader.Core/Columns/ColumnDef.cs @@ -44,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/tests/Kevull.MultiHeader.ClosedXml.Tests/FormatTest.cs b/tests/Kevull.MultiHeader.ClosedXml.Tests/FormatTest.cs index e22b508..59791a8 100644 --- a/tests/Kevull.MultiHeader.ClosedXml.Tests/FormatTest.cs +++ b/tests/Kevull.MultiHeader.ClosedXml.Tests/FormatTest.cs @@ -3,7 +3,7 @@ namespace Kevull.MultiHeader.ClosedXml.Test { - public class FormatTest + public class FormatTest : BaseTest { [Fact] public void PropertiesWithoutChildren_HasVerticalMerge() diff --git a/tests/Kevull.MultiHeader.ClosedXml.Tests/GeneralConfigurationOptionsTest.cs b/tests/Kevull.MultiHeader.ClosedXml.Tests/GeneralConfigurationOptionsTest.cs index 96d7fbf..d893411 100644 --- a/tests/Kevull.MultiHeader.ClosedXml.Tests/GeneralConfigurationOptionsTest.cs +++ b/tests/Kevull.MultiHeader.ClosedXml.Tests/GeneralConfigurationOptionsTest.cs @@ -2,7 +2,7 @@ namespace Kevull.MultiHeader.ClosedXml.Test { - public class GeneralConfigurationOptionsTest + public class GeneralConfigurationOptionsTest : BaseTest { [Fact] public void ReportStatsAt_TopLeftStartingPoint() diff --git a/tests/Kevull.MultiHeader.ClosedXml.Tests/TwoHeaderRenderTest.cs b/tests/Kevull.MultiHeader.ClosedXml.Tests/TwoHeaderRenderTest.cs index 38cff63..00d46f2 100644 --- a/tests/Kevull.MultiHeader.ClosedXml.Tests/TwoHeaderRenderTest.cs +++ b/tests/Kevull.MultiHeader.ClosedXml.Tests/TwoHeaderRenderTest.cs @@ -2,7 +2,7 @@ namespace Kevull.MultiHeader.ClosedXml.Test { - public class TwoHeaderRenderTest + public class TwoHeaderRenderTest : BaseTest { [Fact] public void ComposedObjects_AreRendered_InSecondRow() From bf64b3df3b4d42e4a34fc265a1c5522e43d13210 Mon Sep 17 00:00:00 2001 From: mnieto Date: Sat, 9 May 2026 21:14:47 +0200 Subject: [PATCH 15/16] Avoid to generate nuget package for Kevull.MultiHeader.TestCommon library --- .../Kevull.MultiHeader.TestCommon.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Kevull.MultiHeader.TestCommon/Kevull.MultiHeader.TestCommon.csproj b/tests/Kevull.MultiHeader.TestCommon/Kevull.MultiHeader.TestCommon.csproj index 1e36c3e..d819f6b 100644 --- a/tests/Kevull.MultiHeader.TestCommon/Kevull.MultiHeader.TestCommon.csproj +++ b/tests/Kevull.MultiHeader.TestCommon/Kevull.MultiHeader.TestCommon.csproj @@ -3,6 +3,7 @@ net10.0 enable + string From 6b1557c3dcca255433a250273830442d4eca3148 Mon Sep 17 00:00:00 2001 From: mnieto Date: Sat, 9 May 2026 21:34:33 +0200 Subject: [PATCH 16/16] Fix nuget package validation --- src/Kevull.MultiHeader.Core/Kevull.MultiHeader.Core.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Kevull.MultiHeader.Core/Kevull.MultiHeader.Core.csproj b/src/Kevull.MultiHeader.Core/Kevull.MultiHeader.Core.csproj index f8d9a55..28cce1c 100644 --- a/src/Kevull.MultiHeader.Core/Kevull.MultiHeader.Core.csproj +++ b/src/Kevull.MultiHeader.Core/Kevull.MultiHeader.Core.csproj @@ -16,8 +16,8 @@ MNieto EPlus multi-header report Extension for Excel libraries to create reports from complex objects - https://github.com/mnieto/MultiHeader.Core - https://github.com/mnieto/MultiHeader.Core + https://github.com/mnieto/Kevull.MultiHeader + https://github.com/mnieto/Kevull.MultiHeader git EPPlus;Excel;Multi-header;Report LGPL-3.0-or-later