diff --git a/ClosedXML.Report/ClosedXML.Report.csproj b/ClosedXML.Report/ClosedXML.Report.csproj index 5c40cdc..386b4b4 100644 --- a/ClosedXML.Report/ClosedXML.Report.csproj +++ b/ClosedXML.Report/ClosedXML.Report.csproj @@ -4,20 +4,21 @@ netstandard2.0;netstandard2.1 10 ClosedXML.Report - ClosedXML.Report + CustomEa.ClosedXML.Report Debug;Release https://github.com/ClosedXML/ClosedXML.Report - https://github.com/ClosedXML/ClosedXML.Report - Alexey Rozhkov, Alexey Pankratev + https://github.com/eagleoriginal/ClosedXML.Report + IOrlov, Alexey Rozhkov, Alexey Pankratev MIT - ClosedXML.Report - See https://github.com/ClosedXML/ClosedXML.Report/releases/tag/$(productVersion) - ClosedXML.Report is a tool for report generation and data analysis in .NET applications through the use of Microsoft Excel. ClosedXML.Report is a .NET-library for report generation Microsoft Excel without requiring Excel to be installed on the machine that's running the code. + CustomEa.ClosedXML.Report + See https://github.com/eagleoriginal/ClosedXML.Report/releases/tag/$(productVersion) + Not Intended for Common Use!!! +Pacakge with Custom Changes in original ClosedXML.Report. +ClosedXML.Report is a tool for report generation and data analysis in .NET applications through the use of Microsoft Excel. ClosedXML.Report is a .NET-library for report generation Microsoft Excel without requiring Excel to be installed on the machine that's running the code. ClosedXML Reporting Excel - ClosedXML - https://github.com/ClosedXML/ClosedXML.Report/raw/develop/Resources/favicon-01.png + CustomEa.ClosedXML MIT - false + True true @@ -30,6 +31,8 @@ snupkg true ClosedXML.Report.snk + 0.2.13-Beta9 + favicon-01.png @@ -38,6 +41,13 @@ + + True + \ + + + + diff --git a/ClosedXML.Report/Excel/Subtotal.cs b/ClosedXML.Report/Excel/Subtotal.cs index 76d0af5..7cf7ed4 100644 --- a/ClosedXML.Report/Excel/Subtotal.cs +++ b/ClosedXML.Report/Excel/Subtotal.cs @@ -351,8 +351,16 @@ private MoveData[] ScanRange(int groupBy) var val = row.Cell(groupBy).GetString(); var isSummaryRow = row.IsSummary(); - - if (string.IsNullOrEmpty(val) && !isSummaryRow) + var isLeftGroupEndLine = false; + if (false == isSummaryRow) + { + isLeftGroupEndLine = _groups.SingleOrDefault(gr => gr.Column == groupBy - 1 && + gr.SummaryRow == null && + gr.Range.RangeAddress.LastAddress.RowNumber == row.RowNumber()) != null; + } + + if (this._summaryAbove && // TODO: Without _summaryAbove clarification all empty values fall into RangeType.HeaderRow category and as result no groups created + string.IsNullOrEmpty(val) && !isSummaryRow) { if (groupStart > 0) { @@ -364,6 +372,18 @@ private MoveData[] ScanRange(int groupBy) continue; } + if (isLeftGroupEndLine) + { + var localGroupStart = groupStart == 0 + ? row.RangeAddress.Relative(_range.RangeAddress).FirstAddress.RowNumber + : groupStart; + + groups.Add(CreateMoveTask(groupBy, prevVal, _range.Cell(localGroupStart, 1), row.LastCell(), RangeType.DataRange)); + prevVal = null; + groupStart = 0; + continue; + } + if (val != prevVal) { if (groupStart > 0) diff --git a/ClosedXML.Report/Excel/SubtotalSummaryFunc.cs b/ClosedXML.Report/Excel/SubtotalSummaryFunc.cs index 8d1bd51..1487754 100644 --- a/ClosedXML.Report/Excel/SubtotalSummaryFunc.cs +++ b/ClosedXML.Report/Excel/SubtotalSummaryFunc.cs @@ -56,6 +56,8 @@ public virtual int FuncNum } } + public object DefaultValueForEmptySource { get; set; } + public Func GetCalculateDelegate; internal object Calculate(IDataSource dataSource) diff --git a/ClosedXML.Report/Excel/XlExtensions.cs b/ClosedXML.Report/Excel/XlExtensions.cs index 368a9dd..cab6498 100644 --- a/ClosedXML.Report/Excel/XlExtensions.cs +++ b/ClosedXML.Report/Excel/XlExtensions.cs @@ -350,7 +350,13 @@ public static void ReplaceCFFormulaeToA1(this IXLWorksheet worksheet) { foreach (var format in worksheet.ConditionalFormats) { - var target = format.Ranges.OrderBy(x=>x.RangeAddress.FirstAddress.RowNumber) + format.Ranges.RemoveAll(range => false == range.RangeAddress.IsValid); + var validRanges = format.Ranges.Where(range => range.RangeAddress.IsValid).ToList(); + if (false == validRanges.Any()) + continue; + + var target = + validRanges.OrderBy(x=>x.RangeAddress.FirstAddress.RowNumber) .ThenBy(x=> x.RangeAddress.FirstAddress.ColumnNumber) .First().FirstCell(); foreach (var v in format.Values.Where(v => v.Value.Value.StartsWith("&=")).ToList()) diff --git a/ClosedXML.Report/Options/GroupTag.cs b/ClosedXML.Report/Options/GroupTag.cs index 5eb8bb6..8b61f89 100644 --- a/ClosedXML.Report/Options/GroupTag.cs +++ b/ClosedXML.Report/Options/GroupTag.cs @@ -12,6 +12,7 @@ OPTION PARAMS OBJECTS RNG Priority "\PageBreaks" "\TotalLabel" "\GrandLabel" + "\DisableSubtotalLine" "SummaryAbove" Range rD Normal @@ -30,6 +31,8 @@ OPTION PARAMS OBJECTS RNG Priority using ClosedXML.Excel; using ClosedXML.Report.Excel; using ClosedXML.Report.Utils; +using DocumentFormat.OpenXml.Drawing; +using DocumentFormat.OpenXml.Spreadsheet; using MoreLinq; namespace ClosedXML.Report.Options @@ -40,6 +43,7 @@ public class GroupTag : SortTag public bool PageBreaks => Parameters.ContainsKey("pagebreaks"); public bool DisableSubtotals => Parameters.ContainsKey("disablesubtotals"); + public bool DisableSubtotalLine => Parameters.ContainsKey("disablesubtotalline"); public bool Collapse => Parameters.ContainsKey("collapse"); public bool DisableOutLine => Parameters.ContainsKey("disableoutline"); public bool OutLine => !Parameters.ContainsKey("disableoutline"); @@ -110,11 +114,48 @@ private void Process(ProcessingContext context, GroupTag[] groups, bool summaryA var level = 0; var rows = root.RowCount() - 1; var columns = root.ColumnCount(); - if (rows <= 0 || columns <= 0) + if (columns <= 0) + { return; + } - var r = root.Offset(0, 0, rows, columns); + // Empty Total grand for report + if (rows <= 0) + { + if (disableGrandTotal) + return; + + var r2= root.Offset(0, 0, 1, columns); + using (var subtotal = new Subtotal(r2, summaryAbove, groups, context.Evaluator)) + { + if (TotalLabel != null) subtotal.TotalLabel = TotalLabel; + if (GrandLabel != null) subtotal.GrandLabel = GrandLabel; + if (!disableGrandTotal) + { + var total = subtotal.AddGrandTotal(summaries); + total.SummaryRow.Cell(2).Value = total.SummaryRow.Cell(1).Value; + total.SummaryRow.Cell(1).Value = Blank.Value; + level++; + } + + foreach (var subGroup in subtotal.Groups.OrderBy(x => x.Column).Reverse()) + { + FormatHeaderFooter(subGroup, groupRow); + + GroupRender(subGroup, new GroupTag { Column = 1, Level = 1 }); + } + + r2.Rows().ForEach(r => r.WorksheetRow().OutlineLevel = 0); + } + + // Rem DoDeleteSpecialRow + root.LastRow().Delete(XLShiftDeletedCells.ShiftCellsUp); + return; + } + + var r = root.Offset(0, 0, rows, columns); + using (var subtotal = new Subtotal(r, summaryAbove, groups, context.Evaluator)) { if (TotalLabel != null) subtotal.TotalLabel = TotalLabel; @@ -129,9 +170,18 @@ private void Process(ProcessingContext context, GroupTag[] groups, bool summaryA foreach (var g in groups.OrderBy(x => x.Column)) { + // Todo: New Feature Group Without Subtotal. Only Merge. + if (g.DisableSubtotalLine) + { + subtotal.ScanForGroups(g.Column); + g.Level = ++level; + + continue; + } + Func labFormat = null; if (!string.IsNullOrEmpty(g.LabelFormat)) - labFormat = title => string.Format(LabelFormat, title); + labFormat = title => string.Format(g.LabelFormat, title); if (g.MergeLabels == MergeMode.Merge2 && summaries.Length == 0) subtotal.ScanForGroups(g.Column); @@ -218,7 +268,12 @@ protected virtual void GroupRender(SubtotalGroup subGroup, GroupTag grData) var rng = subGroup.Range.Column(subGroup.Column); if (subGroup.Range.RowCount() > 1) { - int cellIdx = _maxLevel - subGroup.Level + 1; + // TODO: Wrong Style apply for merged cells if on right has grouped total + // But in first cell i expect already cell with value and style + // Plus with DisableSubtotalLine feature this became totally wrong + int cellIdx = 1; + //int cellIdx = _maxLevel - subGroup.Level + 1; // TODO: Comment for future investigation + var style = rng.Cell(cellIdx).Style; rng.Merge(); rng.Style = style; diff --git a/ClosedXML.Report/Options/SummaryFuncTag.cs b/ClosedXML.Report/Options/SummaryFuncTag.cs index 73e64ef..a852112 100644 --- a/ClosedXML.Report/Options/SummaryFuncTag.cs +++ b/ClosedXML.Report/Options/SummaryFuncTag.cs @@ -1,8 +1,11 @@ using System; +using System.Linq; using System.Linq.Expressions; using ClosedXML.Excel; using ClosedXML.Report.Excel; using ClosedXML.Report.Utils; +using DocumentFormat.OpenXml.Math; +using DocumentFormat.OpenXml.Spreadsheet; namespace ClosedXML.Report.Options { @@ -33,7 +36,24 @@ public override void Execute(ProcessingContext context) calculatedRange = context.Range.Offset(0, summ.Column - 1, context.Range.RowCount() - 1, 1); } - if (summ.FuncNum == 0) + object[] items; + if (summ.DataSource != null) + { + items = summ.DataSource.GetAll(); + } + else + { + items = (context.Value as IDataSource).GetAll(); + } + + if (items == null || items.Length == 0) + { + if (summ.DefaultValueForEmptySource != null) + { + summRow.Cell(summ.Column).Value = XLCellValueConverter.FromObject(summ.DefaultValueForEmptySource); + } + } + else if (summ.FuncNum == 0) { var value = summ.Calculate((IDataSource)context.Value); summRow.Cell(summ.Column).Value = XLCellValueConverter.FromObject(value); @@ -62,6 +82,14 @@ private SubtotalSummaryFunc GetFunc(ProcessingContext context) //return XLDynamicExpressionParser.ParseLambda(new[] {par}, null, GetParameter("Over")); }; func.DataSource = DataSource; + + if (HasParameter("Default")) + { + var dlg = context.Evaluator.ParseExpression(GetParameter("Default"), new ParameterExpression[] {}); + + func.DefaultValueForEmptySource = dlg.DynamicInvoke(); + } + return func; } } diff --git a/ClosedXML.Report/RangeInterpreter.cs b/ClosedXML.Report/RangeInterpreter.cs index 4c60b60..310757b 100644 --- a/ClosedXML.Report/RangeInterpreter.cs +++ b/ClosedXML.Report/RangeInterpreter.cs @@ -178,6 +178,13 @@ string EvalString(string str) { var grownRange = rng.GrowToMergedRanges(); var items = nr.RangeData as object[] ?? nr.RangeData.Cast().ToArray(); + // Comment this section + // Revision: a38eda3a3a7092812c8358a949d4e489635501bf + // Author: Aleksei + // Date: 27.07.2023 23:54:54 + // Message: + // Forcibly remove a range representing a table if its data source is empty (#251) (#323) + /*if (!items.Any()) if (!items.Any() && grownRange.IsOptionsRowEmpty()) { @@ -187,7 +194,7 @@ string EvalString(string str) // row would thus delete only first cell, not full (empty) options row. grownRange.Delete(XLShiftDeletedCells.ShiftCellsUp); continue; - } + }*/ // Range template generates output into a new temporary sheet, as not to affect other things // and then copies it to the range in the original sheet. diff --git a/tests/ClosedXML.Report.Tests/GroupTagTests.cs b/tests/ClosedXML.Report.Tests/GroupTagTests.cs index 130a07c..0fadcf0 100644 --- a/tests/ClosedXML.Report.Tests/GroupTagTests.cs +++ b/tests/ClosedXML.Report.Tests/GroupTagTests.cs @@ -1,4 +1,8 @@ -using System.Linq; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; using ClosedXML.Report.Tests.TestModels; using LinqToDB; using Xunit; @@ -36,7 +40,7 @@ public void Simple(string templateFile) { using (var db = new DbDemos()) { - var cust = db.customers.LoadWith(x=>x.Orders.First().Items).OrderBy(c => c.CustNo).First(x=>x.CustNo == 1356); + var cust = db.customers.LoadWith(x => x.Orders.First().Items).OrderBy(c => c.CustNo).First(x => x.CustNo == 1356); cust.Logo = Resource.toms_diving_center; tpl.AddVariable("MoreOrders", cust.Orders.Take(5)); tpl.AddVariable(cust); @@ -50,6 +54,29 @@ public void Simple(string templateFile) }); } + [Xunit.Fact] + public void Simple_EmptyResult() + { + string templateFile = "GroupTagTests_Simple_Empty.xlsx"; + XlTemplateTest(templateFile, + tpl => + { + using (var db = new DbDemos()) + { + var cust = db.customers.LoadWith(x => x.Orders.First().Items).OrderBy(c => c.CustNo).First(x => x.CustNo == 1356); + cust.Orders.Clear(); + cust.Logo = Resource.toms_diving_center; + tpl.AddVariable("MoreOrders", cust.Orders.Take(0)); + tpl.AddVariable(cust); + } + tpl.AddVariable("Tax", 13); + }, + wb => + { + CompareWithGauge(wb, templateFile); + }); + } + [Theory, InlineData("GroupTagTests_SummaryAbove.xlsx"), InlineData("GroupTagTests_MergeLabels.xlsx"), @@ -77,6 +104,7 @@ public void EmptyDataSource(string templateFile) InlineData("GroupTagTests_MultiRanges.xlsx"), InlineData("GroupTagTests_FormulasWithTagsInGroupRow.xlsx", Skip = "Formulas with tags got broken after upgrading to ClosedXML 0.100"), InlineData("GroupTagTests_TotalLabel.xlsx"), + InlineData("GroupTagTests_DisableSubTotals_MergeLabels.xlsx") ] public void Customers(string templateFile) { @@ -86,6 +114,7 @@ public void Customers(string templateFile) using (var db = new DbDemos()) { var orders = db.orders.LoadWith(x => x.Customer).ToList(); + //orders.ForEach(order => order.Customer.Company = order.Customer.Company == "Adventure Undersea"? "": order.Customer.Company); tpl.AddVariable("Orders", orders); } }, @@ -102,7 +131,12 @@ public void WithHeader() tpl => { using (var db = new DbDemos()) - tpl.AddVariable("Orders", db.orders.LoadWith(x => x.Customer).OrderBy(c => c.OrderNo).ToArray()); + { + db.orders.LoadWith(x => x.Customer); + var orders = db.orders.LoadWith(x => x.Customer).ToList(); + //orders.ForEach(order => order.Customer.Company = order.Customer.Company == "Adventure Undersea" ? "" : ""); + tpl.AddVariable("Orders", orders.OrderBy(c => c.OrderNo).ToArray()); + } }, wb => { diff --git a/tests/Gauges/GroupTagTests_DisableSubTotals_MergeLabels.xlsx b/tests/Gauges/GroupTagTests_DisableSubTotals_MergeLabels.xlsx new file mode 100644 index 0000000..d495687 Binary files /dev/null and b/tests/Gauges/GroupTagTests_DisableSubTotals_MergeLabels.xlsx differ diff --git a/tests/Gauges/GroupTagTests_Simple_Empty.xlsx b/tests/Gauges/GroupTagTests_Simple_Empty.xlsx new file mode 100644 index 0000000..875b8da Binary files /dev/null and b/tests/Gauges/GroupTagTests_Simple_Empty.xlsx differ diff --git a/tests/Templates/GroupTagTests_DisableSubTotals_MergeLabels.xlsx b/tests/Templates/GroupTagTests_DisableSubTotals_MergeLabels.xlsx new file mode 100644 index 0000000..6caeab1 Binary files /dev/null and b/tests/Templates/GroupTagTests_DisableSubTotals_MergeLabels.xlsx differ diff --git a/tests/Templates/GroupTagTests_Simple_Empty.xlsx b/tests/Templates/GroupTagTests_Simple_Empty.xlsx new file mode 100644 index 0000000..058765c Binary files /dev/null and b/tests/Templates/GroupTagTests_Simple_Empty.xlsx differ