diff --git a/README.md b/README.md index f97a63a..f819859 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,8 @@ var exportDefinition = new SpreadsheetConfiguration DocumentSubTitle = "Showing the full options", ExportData = GetSampleExportData(100), WorksheetName = "Sample", - FreezePanes = true + FreezePanes = true, + AutoFilterDataRows = true }; var fileContent = exportGenerator.CreateSingleSheetSpreadsheet(exportDefinition); System.IO.File.WriteAllBytes("Sample.xlsx", fileContent); @@ -76,4 +77,6 @@ This package is primarily geared towards the exporting of lists of objects into * Data type formatting for Date & Currency fields * Auto-fit of all columns for display * The ability to freeze the header columns into a freeze pane for single sheet, or multi-sheet exports -* Support for Curreny, Date, F0, F1, and F2 fixed date formats \ No newline at end of file +* The ability to add "Auto Filter" behavior to the data table portion of a sheet, while still supporting all other items +* The ability to automatically add "simple formula" totals to columsn. (SUM, AVG, etc) +* Support for Curreny, Date, F0, F1, F2, and F3 fixed data formats \ No newline at end of file diff --git a/samples/NetCore.Utilities.SpreadsheetExample/Models/SecondExportData.cs b/samples/NetCore.Utilities.SpreadsheetExample/Models/SecondExportData.cs index 2884cf5..525454c 100644 --- a/samples/NetCore.Utilities.SpreadsheetExample/Models/SecondExportData.cs +++ b/samples/NetCore.Utilities.SpreadsheetExample/Models/SecondExportData.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Text; +namespace NetCore.Utilities.SpreadsheetExample.Models; -namespace NetCore.Utilities.SpreadsheetExample.Models +internal class SecondExportData { - class SecondExportData - { - } -} + +} \ No newline at end of file diff --git a/samples/NetCore.Utilities.SpreadsheetExample/Models/SimpleExportData.cs b/samples/NetCore.Utilities.SpreadsheetExample/Models/SimpleExportData.cs index 831e9c4..be23138 100644 --- a/samples/NetCore.Utilities.SpreadsheetExample/Models/SimpleExportData.cs +++ b/samples/NetCore.Utilities.SpreadsheetExample/Models/SimpleExportData.cs @@ -1,21 +1,20 @@ using System; using ICG.NetCore.Utilities.Spreadsheet; -namespace NetCore.Utilities.SpreadsheetExample.Models +namespace NetCore.Utilities.SpreadsheetExample.Models; + +public class SimpleExportData { - public class SimpleExportData - { - public string Title { get; set; } + public string Title { get; set; } + + [SpreadsheetColumn("Due Date", format: "D")] + public DateTime DueDate { get; set; } - [SpreadsheetColumn("Due Date", format:"D")] - public DateTime DueDate { get; set; } - - [SpreadsheetColumn("Total Cost", format:"C", formula: "SUM")] - public decimal TotalCost { get; set; } + [SpreadsheetColumn("Total Cost", format: "C", formula: "SUM")] + public decimal TotalCost { get; set; } - [SpreadsheetColumn("Testing Numbers", format:"F3")] - public decimal TestingNumbers { get; set; } + [SpreadsheetColumn("Testing Numbers", format: "F3")] + public decimal TestingNumbers { get; set; } - public string Notes { get; set; } - } + public string Notes { get; set; } } \ No newline at end of file diff --git a/samples/NetCore.Utilities.SpreadsheetExample/Program.cs b/samples/NetCore.Utilities.SpreadsheetExample/Program.cs index b647a18..000bace 100644 --- a/samples/NetCore.Utilities.SpreadsheetExample/Program.cs +++ b/samples/NetCore.Utilities.SpreadsheetExample/Program.cs @@ -1,61 +1,64 @@ using System; using System.Collections.Generic; +using System.IO; using ICG.NetCore.Utilities.Spreadsheet; using Microsoft.Extensions.DependencyInjection; using NetCore.Utilities.SpreadsheetExample.Models; -namespace NetCore.Utilities.SpreadsheetExample +namespace NetCore.Utilities.SpreadsheetExample; + +internal class Program { - class Program + private static void Main(string[] args) { - static void Main(string[] args) + //Setup our DI Container + var services = new ServiceCollection(); + services.UseIcgNetCoreUtilitiesSpreadsheet(); + var provider = services.BuildServiceProvider(); + + //Get our generator and export + var exportGenerator = provider.GetRequiredService(); + var exportDefinition = new SpreadsheetConfiguration { - //Setup our DI Container - var services = new ServiceCollection(); - services.UseIcgNetCoreUtilitiesSpreadsheet(); - var provider = services.BuildServiceProvider(); + RenderTitle = true, + DocumentTitle = "Sample Export of 100 Records", + RenderSubTitle = true, + DocumentSubTitle = "Showing the full options", + ExportData = GetSampleExportData(100), + WorksheetName = "Sample", + FreezeHeaders = true, + AutoFilterDataRows = true + }; + var fileContent = exportGenerator.CreateSingleSheetSpreadsheet(exportDefinition); + File.WriteAllBytes("Sample.xlsx", fileContent); - //Get our generator and export - var exportGenerator = provider.GetRequiredService(); - var exportDefinition = new SpreadsheetConfiguration + //Sample 2 sheet export + var multiSheetDefinition = new MultisheetConfiguration() + .WithSheet("Sheet 1", GetSampleExportData(100)) + .WithSheet("Additional Sheet", GetSampleExportData(500), config => { - RenderTitle = true, - DocumentTitle = "Sample Export of 100 Records", - RenderSubTitle = true, - DocumentSubTitle = "Showing the full options", - ExportData = GetSampleExportData(100), - WorksheetName = "Sample", - FreezeHeaders = true - }; - var fileContent = exportGenerator.CreateSingleSheetSpreadsheet(exportDefinition); - System.IO.File.WriteAllBytes("Sample.xlsx", fileContent); - - //Sample 2 sheet export - var multiSheetDefinition = new MultisheetConfiguration() - .WithSheet("Sheet 1", GetSampleExportData(100)) - .WithSheet("Additional Sheet", GetSampleExportData(500), config => - { - config.DocumentTitle = "Lots of data"; - config.RenderTitle = true; - config.FreezeHeaders = true; - }); + config.DocumentTitle = "Lots of data"; + config.RenderTitle = true; + config.FreezeHeaders = true; + config.AutoFilterDataRows = true; + }); - var multiFileContent = exportGenerator.CreateMultiSheetSpreadsheet(multiSheetDefinition); - System.IO.File.WriteAllBytes("Sample-Multi.xlsx", multiFileContent); - Console.WriteLine("Files Created"); - Console.ReadLine(); - } + var multiFileContent = exportGenerator.CreateMultiSheetSpreadsheet(multiSheetDefinition); + File.WriteAllBytes("Sample-Multi.xlsx", multiFileContent); + Console.WriteLine("Files Created"); + Console.ReadLine(); + } - private static List GetSampleExportData(int numberOfRecords) - { - var listData = new List(); - for (var i = 0; i < numberOfRecords; i++) + private static List GetSampleExportData(int numberOfRecords) + { + var listData = new List(); + for (var i = 0; i < numberOfRecords; i++) + listData.Add(new SimpleExportData { - listData.Add(new SimpleExportData - {DueDate = DateTime.Now.AddDays(i), Notes = $"Record {i} notes", TotalCost = 15m, TestingNumbers = 1234.4567289m, Title = $"Sample Data Row #{i}"}); - } + DueDate = DateTime.Now.AddDays(i), Notes = $"Record {i} notes", TotalCost = 15m, + TestingNumbers = 1234.4567289m, Title = $"Sample Data Row #{i}" + }); - return listData; - } + return listData; } -} +} \ No newline at end of file diff --git a/src/NetCore.Utilities.Spreadsheet/OpenXmlSpreadsheetGenerator.cs b/src/NetCore.Utilities.Spreadsheet/OpenXmlSpreadsheetGenerator.cs index f06b8fb..cde5300 100644 --- a/src/NetCore.Utilities.Spreadsheet/OpenXmlSpreadsheetGenerator.cs +++ b/src/NetCore.Utilities.Spreadsheet/OpenXmlSpreadsheetGenerator.cs @@ -58,19 +58,24 @@ public bool CreateSingleSheetSpreadsheet(Stream output, SpreadsheetConfigurat stylesPart.Stylesheet = CreateStylesheet(); stylesPart.Stylesheet.Save(); - var data = CreateExportSheet(exportConfiguration, out var columns); + var data = CreateExportSheet(exportConfiguration, out var columns, out var filter); //Add a worksheet to our document var worksheetPart = workbookPart.AddNewPart(); worksheetPart.Worksheet = new Worksheet(); - //If we are freezing panes add the sheet views + //If we are freezing panes add the sheet views, this is done BEFORE the data is loaded if (exportConfiguration.FreezeHeaders) worksheetPart.Worksheet.Append(CreateFreezePane(exportConfiguration)); + //Load the actual data worksheetPart.Worksheet.Append(columns); worksheetPart.Worksheet.Append(data); + //If Filtering, add it after we have added the data + if (exportConfiguration.AutoFilterDataRows) + worksheetPart.Worksheet.Append(filter); + //Add the sheet to the workbook var sheets = spreadsheetDocument.WorkbookPart.Workbook.AppendChild(new Sheets()); var sheet = new Sheet @@ -166,17 +171,24 @@ public bool CreateMultiSheetSpreadsheet(Stream output, IEnumerable(); worksheetPart.Worksheet = new Worksheet(); - //If we are freezing panes add the sheet views + + //If we are freezing panes add the sheet views before loading the data if (item.FreezeHeaders) worksheetPart.Worksheet.Append(CreateFreezePane(item)); + + //Load the data worksheetPart.Worksheet.Append(columns); worksheetPart.Worksheet.Append(data); + //If we are filtering, add the filter ater the data + if (item.AutoFilterDataRows) + worksheetPart.Worksheet.Append(filter); + //Add the sheet to the workbook var sheet = new Sheet { @@ -222,7 +234,7 @@ private static bool IsOfType(Type t) private sealed record OutputPropMap(Column Column, List Cells); - private static SheetData CreateExportSheet(ISpreadsheetConfiguration exportConfiguration, out Columns columns) + private static SheetData CreateExportSheet(ISpreadsheetConfiguration exportConfiguration, out Columns columns, out AutoFilter filter) { //Build out our sheet information var data = new SheetData(); @@ -291,7 +303,8 @@ private static SheetData CreateExportSheet(ISpreadsheetConfiguration exportConfi data.Append(headerRow); currentRow++; - uint? firstDataRow = headerProperties.Any(d => string.IsNullOrWhiteSpace(d.Formula) == false) ? currentRow : null; + var dataRowIndex = currentRow; + var requiresFormula = headerProperties.Any(d => string.IsNullOrWhiteSpace(d.Formula) == false); //Run the data foreach (var item in exportConfiguration.ExportData) @@ -320,7 +333,8 @@ private static SheetData CreateExportSheet(ISpreadsheetConfiguration exportConfi currentRow++; } - if (firstDataRow != null) + //Add the formula(s) as needed + if (requiresFormula) { var dataRow = new Row { RowIndex = currentRow }; foreach (var prop in headerProperties) @@ -331,7 +345,7 @@ private static SheetData CreateExportSheet(ISpreadsheetConfiguration exportConfi { CellReference = GetCellReferenceByRowAndColumn(currentRow, prop.Order), DataType = CellValues.Number, - CellFormula = new CellFormula($"{prop.Formula}({GetCellReferenceByRowAndColumn(firstDataRow.Value, prop.Order)}:{GetCellReferenceByRowAndColumn(currentRow - 1, prop.Order)})") + CellFormula = new CellFormula($"{prop.Formula}({GetCellReferenceByRowAndColumn(dataRowIndex, prop.Order)}:{GetCellReferenceByRowAndColumn(currentRow - 1, prop.Order)})") }; //Match the formatting of the column for this @@ -358,6 +372,16 @@ private static SheetData CreateExportSheet(ISpreadsheetConfiguration exportConfi { CalculateSizes(outputMap.Values.ToList()); } + + filter = new AutoFilter(); + if (exportConfiguration.AutoFilterDataRows) + { + //Start 1 row up from data row start (to include header) + var startPosition = GetCellReferenceByRowAndColumn(dataRowIndex -1, 1); + var endPosition = GetCellReferenceByRowAndColumn(currentRow - 1, headerProperties.Max(p => p.Order)); + filter.Reference = $"{startPosition}:{endPosition}"; + } + return data; } diff --git a/src/NetCore.Utilities.Spreadsheet/SpreadsheetConfiguration.cs b/src/NetCore.Utilities.Spreadsheet/SpreadsheetConfiguration.cs index b9c53b8..ee0cc8d 100644 --- a/src/NetCore.Utilities.Spreadsheet/SpreadsheetConfiguration.cs +++ b/src/NetCore.Utilities.Spreadsheet/SpreadsheetConfiguration.cs @@ -63,6 +63,11 @@ public interface ISpreadsheetConfiguration /// if set to true the header(s) will be frozen /// public bool FreezeHeaders { get; set; } + + /// + /// If set to true the data section of the sheet will have auto-filter turned on + /// + public bool AutoFilterDataRows { get; set; } } /// /// The type to be exported @@ -130,6 +135,9 @@ public class SpreadsheetConfiguration : ISpreadsheetConfiguration where T /// if set to true the header(s) will be frozen /// public bool FreezeHeaders { get; set; } + + /// + public bool AutoFilterDataRows { get; set; } } ///