diff --git a/SharpExcel.Models/Configuration/ExporterOptions.cs b/SharpExcel.Models/Configuration/ExporterOptions.cs index ca1e5e7..80002e9 100644 --- a/SharpExcel.Models/Configuration/ExporterOptions.cs +++ b/SharpExcel.Models/Configuration/ExporterOptions.cs @@ -1,3 +1,4 @@ +using SharpExcel.Models.Data; using SharpExcel.Models.Styling; using SharpExcel.Models.Styling.Rules; @@ -15,6 +16,11 @@ public class ExporterOptions /// public StylingCollection Styling { get; set; } = new(); + /// + /// Targeting collection + /// + public TargetingCollection Targeting { get; set; } = new(); + /// /// Fluent method to set default header style for this exporter /// @@ -60,4 +66,17 @@ public ExporterOptions WithStylingRule(Action + /// Fluent method to add a styling rule for this exporter + /// + /// constructs the styling rule + /// + public ExporterOptions WithTarget(Action> targetingRuleOptions) + { + var stylingRule = new TargetingRule(); + targetingRuleOptions(stylingRule); + Targeting.Rules.Add(stylingRule); + return this; + } } \ No newline at end of file diff --git a/SharpExcel.Models/Data/TargetingCollection.cs b/SharpExcel.Models/Data/TargetingCollection.cs new file mode 100644 index 0000000..1e2acd5 --- /dev/null +++ b/SharpExcel.Models/Data/TargetingCollection.cs @@ -0,0 +1,6 @@ +namespace SharpExcel.Models.Data; + +public class TargetingCollection +{ + public List> Rules { get; set; } = new(); +} \ No newline at end of file diff --git a/SharpExcel.Models/Data/TargetingRule.cs b/SharpExcel.Models/Data/TargetingRule.cs new file mode 100644 index 0000000..51bcf6f --- /dev/null +++ b/SharpExcel.Models/Data/TargetingRule.cs @@ -0,0 +1,76 @@ +using System.ComponentModel.DataAnnotations; + +namespace SharpExcel.Models.Data; + +public interface ITargetingRule +{ + +} +public record TargetingRule : ITargetingRule +{ + /// + /// REQUIRED: name of the sheet in the excel file + /// + [MinLength(1)] + public string SheetName { get; set; } = null!; + + /// + /// Optional Row to start reading/writing from. + /// This is useful when you want to only affect part of a sheet. + /// Excel rows start as 1, so 1 is the first row + /// + public int? Row { get; set; } + + /// + /// Optional Column to start reading/writing from. + /// This is useful when you want to only affect part of a sheet. + /// Excel columns start as 1, so 1 is the first row + /// + public int? Column { get; set; } + + /// + /// Conditions to check if the rule should be applied. + /// + public Func? RulePredicate { get; set; } + + public TargetingRule WithCondition(Func condition) + { + RulePredicate = condition; + //return this object so we can chain calls + return this; + } + + /// + /// Sets the row to start reading/writing from. + /// + /// + /// + public TargetingRule WithStartRow(int rowId) + { + Row = rowId; + //return this object so we can chain calls + return this; + } + + /// + /// Sets the column to start reading/writing from. + /// + public TargetingRule WithStartColumn(int columnId) + { + Column = columnId; + //return this object so we can chain calls + return this; + } + + /// + /// Sets the sheet name to start reading/writing from. + /// + public TargetingRule WithSheetName(string sheetName) + { + SheetName = sheetName; + //return this object so we can chain calls + return this; + } +} + + diff --git a/SharpExcel.Models/Results/ExcelAddress.cs b/SharpExcel.Models/Results/ExcelAddress.cs index ad24e2f..1507d85 100644 --- a/SharpExcel.Models/Results/ExcelAddress.cs +++ b/SharpExcel.Models/Results/ExcelAddress.cs @@ -24,4 +24,9 @@ public record struct ExcelAddress /// Header name /// public string? HeaderName { get; set; } + + /// + /// Sheet Name + /// + public string SheetName { get; set; } } \ No newline at end of file diff --git a/SharpExcel.Models/Results/ExcelReadResult.cs b/SharpExcel.Models/Results/ExcelReadResult.cs index 7f15502..aa77757 100644 --- a/SharpExcel.Models/Results/ExcelReadResult.cs +++ b/SharpExcel.Models/Results/ExcelReadResult.cs @@ -6,4 +6,25 @@ public class ExcelReadResult public List Records { get; set; } = new(); public Dictionary ValidationResults { get; set; } = new(); + + +} + +public static class ExcelReadResultExtensions +{ + public static void Append(this ExcelReadResult result, ExcelReadResult other) + where TModel : class + { + result.Records.AddRange(other.Records); + foreach (var kvp in other.ValidationResults) + { + if (!result.ValidationResults.ContainsKey(kvp.Key)) + { + result.ValidationResults.Add(kvp.Key, kvp.Value); + continue; + } + result.ValidationResults[kvp.Key] = kvp.Value; + } + + } } \ No newline at end of file diff --git a/SharpExcel.Models/Styling/Constants/ExcelTargetingConstants.cs b/SharpExcel.Models/Styling/Constants/ExcelTargetingConstants.cs new file mode 100644 index 0000000..4ba4c6f --- /dev/null +++ b/SharpExcel.Models/Styling/Constants/ExcelTargetingConstants.cs @@ -0,0 +1,16 @@ +using SharpExcel.Models.Data; + +namespace SharpExcel.Models.Styling.Constants; + +public class ExcelTargetingConstants + where TModel : class +{ + public static TargetingRule DefaultTargetingRule = new TargetingRule + { + SheetName = "Export", + Column = 1, + Row = 1, + RulePredicate = _ => true, + }; + +} \ No newline at end of file diff --git a/SharpExcel.TestApplication/Program.cs b/SharpExcel.TestApplication/Program.cs index 8bacc53..986e439 100644 --- a/SharpExcel.TestApplication/Program.cs +++ b/SharpExcel.TestApplication/Program.cs @@ -1,5 +1,4 @@ using System.Globalization; -using SharpExcel.Models.Arguments; using SharpExcel.Models.Results; using SharpExcel.Models.Styling.Colorization; using SharpExcel.TestApplication.TestData; @@ -48,6 +47,23 @@ //can be omitted to use default style rule.WhenFalse(ExcelCellStyleConstants.DefaultDataStyle.WithTextColor(new(80, 160, 80))); }); + + options.WithTarget(rule => + { + rule.WithCondition(x => x.Status != TestStatus.Fired); + rule.WithSheetName("Employees"); + rule.WithStartColumn(3); + rule.WithStartRow(3); + }); + + options.WithTarget(rule => + { + rule.WithCondition(x => x.Status == TestStatus.Fired); + rule.WithSheetName("Fired"); + rule.WithStartRow(3); + rule.WithStartColumn(3); + + }); }); using IHost host = builder.Build(); @@ -60,20 +76,14 @@ async Task RunApp(IServiceProvider services) var exportPath = $"./OutputFolder/TestExport-{Guid.NewGuid()}.xlsx"; var validationExportPath = $"./OutputFolder/ErrorChecked-{Guid.NewGuid()}.xlsx"; var exportService = services.GetRequiredService>(); - - var excelArguments = new ExcelArguments() - { - SheetName = "Budgets", - CultureInfo = CultureInfo.CurrentCulture - }; - using var workbook = await exportService.GenerateWorkbookAsync(excelArguments, TestDataProvider.GetTestData()); + using var workbook = await exportService.GenerateWorkbookAsync(CultureInfo.CurrentCulture, TestDataProvider.GetTestData()); workbook.SaveAs(exportPath); - using var errorCheckedWorkbook = await exportService.ValidateAndAnnotateWorkbookAsync(excelArguments, workbook); + using var errorCheckedWorkbook = await exportService.ValidateAndAnnotateWorkbookAsync(CultureInfo.CurrentCulture, workbook); errorCheckedWorkbook.SaveAs(validationExportPath); - var importedWorkbook = await exportService.ReadWorkbookAsync(excelArguments, workbook); + var importedWorkbook = await exportService.ReadWorkbookAsync(CultureInfo.CurrentCulture, workbook); #region write_output foreach (var dataItem in importedWorkbook.Records) diff --git a/SharpExcel.TestApplication/TestData/TestDataProvider.cs b/SharpExcel.TestApplication/TestData/TestDataProvider.cs index 870262c..9641d54 100644 --- a/SharpExcel.TestApplication/TestData/TestDataProvider.cs +++ b/SharpExcel.TestApplication/TestData/TestDataProvider.cs @@ -9,7 +9,7 @@ public static List GetTestData() new() { Id = 0, FirstName = "John", LastName = "Doe", Budget = 2400.34m, Email = "john.doe@example.com", TestDepartment = TestDepartment.Unknown, Status = TestStatus.Employed }, new() { Id = 1, FirstName = "Jane", LastName = "Doe", Budget = -200.42m, Email = "jane.doe@example.com", TestDepartment = TestDepartment.ValueB, Status = TestStatus.Fired }, new() { Id = 2, FirstName = "John", LastName = "Neutron", Budget = 0.0m, Email = null, TestDepartment = TestDepartment.ValueB, Status = TestStatus.Employed }, - new() { Id = 3, FirstName = "Ash", LastName = "Ketchum", Budget = 69m, Email = "ash@example.com", TestDepartment = TestDepartment.ValueC, Status = TestStatus.Fired }, + new() { Id = 3, FirstName = "Ash", LastName = "Ketchum", Budget = 69m, Email = null, TestDepartment = TestDepartment.ValueC, Status = TestStatus.Fired }, new() { Id = 4, FirstName = "Inspector", LastName = "Gadget", Budget = 1337m, Email = "gogogadget@example.com", TestDepartment = TestDepartment.ValueC, Status = TestStatus.Employed }, new() { Id = 5, FirstName = "Mindy", LastName = "", Budget = 2400.34m, Email = "mmouse@example.com", TestDepartment = TestDepartment.ValueA, Status = TestStatus.Employed }, new() { Id = 6, FirstName = "ThisIsLongerThan10", LastName = "Mouse", Budget = 2400.34m, Email = "mmouse@example.com", TestDepartment = TestDepartment.ValueA, Status = TestStatus.Employed }, diff --git a/SharpExcel.Tests/ExcelImportTests.cs b/SharpExcel.Tests/ExcelImportTests.cs index d52ab9b..18d5597 100644 --- a/SharpExcel.Tests/ExcelImportTests.cs +++ b/SharpExcel.Tests/ExcelImportTests.cs @@ -1,8 +1,9 @@ +using System.Globalization; using ClosedXML.Excel; using Microsoft.Extensions.Options; using SharpExcel.Tests.Shared; -using SharpExcel.Models.Arguments; using SharpExcel.Models.Configuration.Constants; +using SharpExcel.Models.Styling.Constants; using Shouldly; using Xunit; @@ -23,15 +24,15 @@ public ExcelImportTests() [Fact] public async Task CreateWorkbookTest() { - var workbook = await _synchronizer.GenerateWorkbookAsync(new ExcelArguments(){ SheetName = "TestSheet"}, CreateTestData()); - workbook.Worksheets.FirstOrDefault(x => x.Name == "TestSheet").ShouldNotBeNull(); + var workbook = await _synchronizer.GenerateWorkbookAsync(CultureInfo.CurrentCulture, CreateTestData()); + workbook.Worksheets.FirstOrDefault(x => x.Name == ExcelTargetingConstants.DefaultTargetingRule.SheetName).ShouldNotBeNull(); workbook.ShouldNotBeNull(); //there should be 2 worksheets, a visible one for the data, and a hidden one to pull data from for the enum dropdowns workbook.Worksheets.Count.ShouldBe(2); //main data worksheet - workbook.Worksheet(1).Name.ShouldBe("TestSheet"); + workbook.Worksheet(1).Name.ShouldBe(ExcelTargetingConstants.DefaultTargetingRule.SheetName); workbook.Worksheet(1).Visibility.ShouldBe(XLWorksheetVisibility.Visible); //hidden worksheet for enum dropdowns @@ -41,12 +42,11 @@ public async Task CreateWorkbookTest() [Fact] public async Task ReadWorkbookTest() { - var args = new ExcelArguments() { SheetName = "TestSheet" }; //create test workbook - var workbook = await _synchronizer.GenerateWorkbookAsync( args, CreateTestData()); + var workbook = await _synchronizer.GenerateWorkbookAsync(CultureInfo.InvariantCulture, CreateTestData()); //read workbook - var output = await _synchronizer.ReadWorkbookAsync(args, workbook); + var output = await _synchronizer.ReadWorkbookAsync(CultureInfo.InvariantCulture, workbook); output.Records.Count.ShouldBe(2); diff --git a/SharpExcel/Abstraction/ISharpExcelSynchronizer.cs b/SharpExcel/Abstraction/ISharpExcelSynchronizer.cs index 50dadef..f979a32 100644 --- a/SharpExcel/Abstraction/ISharpExcelSynchronizer.cs +++ b/SharpExcel/Abstraction/ISharpExcelSynchronizer.cs @@ -1,5 +1,5 @@ -using ClosedXML.Excel; -using SharpExcel.Models.Arguments; +using System.Globalization; +using ClosedXML.Excel; using SharpExcel.Models.Results; namespace SharpExcel.Abstraction; @@ -14,10 +14,10 @@ public interface ISharpExcelSynchronizer /// /// Generates a workbook based on the provided data /// - /// Collection of arguments + /// /// The data to generate the workbook from /// - public Task GenerateWorkbookAsync(ExcelArguments arguments, IEnumerable data); + public Task GenerateWorkbookAsync(CultureInfo cultureInfo, ICollection data); /// /// Reads a workbook to convert it into the given model @@ -26,13 +26,13 @@ public interface ISharpExcelSynchronizer /// /// /// - public Task> ReadWorkbookAsync(ExcelArguments arguments, XLWorkbook workbook); + public Task> ReadWorkbookAsync(CultureInfo arguments, XLWorkbook workbook); /// /// Reads, then returns the supplied workbook, but highlights cells containing invalid data, using standard System.ComponentModel.DataAnnotations validation on the model /// - /// Collection of arguments + /// /// The workbook /// The highlighted workbook - public Task ValidateAndAnnotateWorkbookAsync(ExcelArguments arguments, XLWorkbook workbook); + public Task ValidateAndAnnotateWorkbookAsync(CultureInfo cultureInfo, XLWorkbook workbook); } \ No newline at end of file diff --git a/SharpExcel/Exporters/BaseSharpExcelSynchronizer.cs b/SharpExcel/Exporters/BaseSharpExcelSynchronizer.cs index b2823d8..0fb44d6 100644 --- a/SharpExcel/Exporters/BaseSharpExcelSynchronizer.cs +++ b/SharpExcel/Exporters/BaseSharpExcelSynchronizer.cs @@ -1,12 +1,14 @@ using System.ComponentModel.DataAnnotations; using System.Globalization; using ClosedXML.Excel; +using DocumentFormat.OpenXml.Spreadsheet; using Microsoft.Extensions.Options; using SharpExcel.Abstraction; using SharpExcel.Extensions; -using SharpExcel.Models.Arguments; using SharpExcel.Models.Configuration; +using SharpExcel.Models.Data; using SharpExcel.Models.Results; +using SharpExcel.Models.Styling.Constants; namespace SharpExcel.Exporters; @@ -22,76 +24,127 @@ public BaseSharpExcelSynchronizer(IOptions> options) { _options = options; } + /// - public async Task ValidateAndAnnotateWorkbookAsync(ExcelArguments arguments, XLWorkbook workbook) + public async Task ValidateAndAnnotateWorkbookAsync(CultureInfo cultureInfo, XLWorkbook workbook) { - var parsedWorkbook = await ReadWorkbookAsync(arguments, workbook); - ExporterHelpers.ApplyCellValidation(arguments.SheetName!, workbook, parsedWorkbook); + var parsedWorkbook = await ReadWorkbookAsync(cultureInfo, workbook); + ExporterHelpers.ApplyCellValidation(workbook, parsedWorkbook); return workbook; } + + public Task> ReadWorkbookAsync(CultureInfo cultureInfo, XLWorkbook workbook) + { + var output = new ExcelReadResult(); + var instanceData = CreateReadInstanceData(cultureInfo, workbook); + var rules = _options.Value.Targeting.Rules.GroupBy(rule => rule.SheetName); - /// - public virtual async Task GenerateWorkbookAsync(ExcelArguments arguments, IEnumerable data) + foreach (var ruleGroup in rules) + { + if (!instanceData.Workbook.Worksheets.TryGetWorksheet(ruleGroup.Key, out var worksheet)) + { + continue; + } + + foreach (var rule in ruleGroup) + { + ReadSheetAsync(rule, output, instanceData, worksheet); + } + } + + return Task.FromResult(output); + } + + public virtual async Task GenerateWorkbookAsync(CultureInfo cultureInfo, ICollection data) { var workbook = new XLWorkbook(); + + if (!_options.Value.Targeting.Rules.Any()) + { + _options.Value.Targeting.Rules = [ExcelTargetingConstants.DefaultTargetingRule]; + } + + Dictionary, IEnumerable> dataGroupedByTargetingRule = new(); + + foreach (var targetingRule in _options.Value.Targeting.Rules) + { + dataGroupedByTargetingRule.Add(targetingRule, data.Where(x => targetingRule.RulePredicate != null && targetingRule.RulePredicate(x)).ToList()); + if (!workbook.Worksheets.TryGetWorksheet(targetingRule.SheetName, out var _)) + { + workbook.Worksheets.Add(targetingRule.SheetName); + } + } - var instanceData = CreateWriteInstanceData(arguments, workbook); + var instanceData = CreateWriteInstanceData(cultureInfo, workbook); + EnumExporter.AddEnumDropdownMappingsToSheet(instanceData); - if (instanceData.HeaderStyle.RowHeight.HasValue) + foreach (var targetingRuleData in dataGroupedByTargetingRule) { - instanceData.MainWorksheet.Rows().Height = instanceData.HeaderStyle.RowHeight.Value; + await GenerateSheetAsync(targetingRuleData.Key, instanceData, targetingRuleData.Value); } - //start at Row 1 because Excel starts at 1 - var rowIndex = 1; - var dropdownDataMappings = EnumExporter.AddEnumDropdownMappingsToSheet(instanceData); + return workbook; + } + + + /// + internal virtual Task GenerateSheetAsync(TargetingRule targetingRule, SharpExcelWriterInstanceData instanceData, IEnumerable data) + { + if (!instanceData.Workbook.Worksheets.TryGetWorksheet(targetingRule.SheetName, out var _)) + { + instanceData.Workbook.Worksheets.Add(targetingRule.SheetName); + } + + //start at Row 1 if not defined because Excel starts at 1 + var rowIndex = targetingRule.Row ?? 1; - ExporterHelpers.WriteHeaderRow(instanceData, rowIndex); + ExporterHelpers.WriteHeaderRow(targetingRule, instanceData, rowIndex, targetingRule.Column); //go to next row to start inserting data rowIndex++; foreach (var dataItem in data) { - ExporterHelpers.WriteDataRow(instanceData, dataItem, rowIndex, dropdownDataMappings); + ExporterHelpers.WriteDataRow(targetingRule, instanceData, dataItem, rowIndex, targetingRule.Column); rowIndex++; } - - return await Task.FromResult(workbook); + + return Task.CompletedTask; } /// - public Task> ReadWorkbookAsync(ExcelArguments arguments, XLWorkbook workbook) + internal void ReadSheetAsync(TargetingRule rule, ExcelReadResult result, SharpExcelWriterInstanceData instanceData, IXLWorksheet worksheet) { - var instanceData = CreateReadInstanceData(arguments, workbook); + var usedArea = worksheet.RangeUsed(); + if (usedArea is null) + { + return; + } - var output = new ExcelReadResult(); - - var usedArea = instanceData.MainWorksheet.RangeUsed(); - var headerRowIndex = FindAndMapHeaderRow(instanceData, usedArea); - var remainingRows = usedArea.Rows(headerRowIndex + 1, usedArea.RowCount()).ToList(); + var headerRowIndex = FindAndMapHeaderRow(rule, instanceData, usedArea); + var remainingRows = usedArea.Rows(headerRowIndex, usedArea.RowCount()).ToList(); //parse remaining data rows foreach (var row in remainingRows) { - var data = ReadRow(instanceData, row, out var validationResults); + var data = ReadRow(worksheet, instanceData, row.WorksheetRow(), out var validationResults); if (data == null) { - //skip to next record if we can read record + //skip to next record if we can't read record continue; } - output.Records.Add(data); + result.Records.Add(data); //add validation results if (validationResults.Any()) { foreach (var validationResult in validationResults) { - output.ValidationResults.Add(data, new ExcelCellValidationResult() + result.ValidationResults.Add(data, new ExcelCellValidationResult() { Address = validationResult.Key, ValidationResults = validationResult.Value @@ -100,20 +153,20 @@ public Task> ReadWorkbookAsync(ExcelArguments arguments, } } - - return Task.FromResult(output); } /// /// Reads a row and tries to convert it to the given model /// + /// /// instance data /// row to read - /// A dictionary containing validatio nresults of previous rows + /// A dictionary containing validation results of previous rows /// private static TModel? ReadRow( + IXLWorksheet sheet, SharpExcelWriterInstanceData instance, - IXLRangeRow row, + IXLRow row, out Dictionary> validationResults) { var data = new TModel(); @@ -129,10 +182,11 @@ public Task> ReadWorkbookAsync(ExcelArguments arguments, RowNumber = row.RowNumber(), ColumnId = cell.Address.ColumnNumber, ColumnName = cell.Address.ColumnLetter, - HeaderName = columnData.Name + HeaderName = columnData.Name, + SheetName = sheet.Name }; - var dataValue = ExporterHelpers.TrySetCellValue(instance.Properties.EnumMappings, columnData, cell, + var dataValue = ExporterHelpers.TryGetCellValue(instance.Properties.EnumMappings, columnData, cell, instance.CultureInfo ?? CultureInfo.CurrentCulture); if (columnData.PropertyInfo.PropertyType == dataValue?.GetType()) @@ -163,6 +217,7 @@ public Task> ReadWorkbookAsync(ExcelArguments arguments, /// /// private static int FindAndMapHeaderRow( + TargetingRule rule, SharpExcelWriterInstanceData instance, IXLRange usedArea) { @@ -170,17 +225,25 @@ private static int FindAndMapHeaderRow( x => !string.IsNullOrWhiteSpace(x.NormalizedName)) .Select(x => x.NormalizedName?.ToLowerInvariant())! ); + //find header row - var headerRowIndex = usedArea + var headerRow = usedArea .Rows(x => x.Cells() .Any(c => headerNames.Contains(c.Value.ToString().ToLowerInvariant()))) - .FirstOrDefault() - ?.RowNumber() ?? -1; + .FirstOrDefault()?.WorksheetRow(); + + var headerRowId = headerRow!.RowNumber(); var propertiesByColumnName = instance.Properties.PropertyMappings.ToDictionary(x => x.NormalizedName); - foreach (var cell in instance.MainWorksheet.Row(headerRowIndex).Cells()) + var startIndex = usedArea.FirstCell().WorksheetColumn().ColumnNumber(); + + if (rule.Column != null && rule.Column > startIndex) + startIndex = rule.Column ?? 1; + + for (int i = startIndex; i <= usedArea.ColumnCount(); i++) { + var cell = headerRow!.Cell(i); if (!cell.TryGetValue(out string cellValue)) continue; @@ -199,7 +262,7 @@ private static int FindAndMapHeaderRow( } } - return headerRowIndex; + return headerRowId <= 1 ? 2 : headerRowId - 1; } /// @@ -208,7 +271,7 @@ private static int FindAndMapHeaderRow( /// arguments to use /// workbook to use /// - private SharpExcelWriterInstanceData CreateWriteInstanceData(ExcelArguments arguments, XLWorkbook workbook) + private SharpExcelWriterInstanceData CreateWriteInstanceData(CultureInfo cultureInfo, XLWorkbook workbook) { var random = new Random(); var randomNumber = random.Next(0, 1000000); @@ -220,9 +283,9 @@ private SharpExcelWriterInstanceData CreateWriteInstanceData(ExcelArgume ErrorStyle = _options.Value.Styling.DefaultErrorStyle, Properties = TypeMapper.GetModelMetaData(), StylingRuleLookup = _options.Value.Styling.ToStylingRuleLookup(), - MainWorksheet = workbook.AddWorksheet(arguments.SheetName), + Workbook = workbook, DropdownSourceWorksheet = workbook.AddWorksheet("Dropdowns_" + randomNumber.ToString("000000")).Hide(), - CultureInfo = arguments.CultureInfo + CultureInfo = cultureInfo }; return run; @@ -234,7 +297,7 @@ private SharpExcelWriterInstanceData CreateWriteInstanceData(ExcelArgume /// arguments to use /// workbook to use /// - private SharpExcelWriterInstanceData CreateReadInstanceData(ExcelArguments arguments, XLWorkbook workbook) + private SharpExcelWriterInstanceData CreateReadInstanceData(CultureInfo cultureInfo, XLWorkbook workbook) { return new SharpExcelWriterInstanceData() { @@ -243,8 +306,9 @@ private SharpExcelWriterInstanceData CreateReadInstanceData(ExcelArgumen ErrorStyle = _options.Value.Styling.DefaultErrorStyle, Properties = TypeMapper.GetModelMetaData(), StylingRuleLookup = _options.Value.Styling.ToStylingRuleLookup(), - MainWorksheet = workbook.Worksheet(arguments.SheetName), - CultureInfo = arguments.CultureInfo + TargetingRules = _options.Value.Targeting.Rules, + Workbook = workbook, + CultureInfo = cultureInfo }; } } \ No newline at end of file diff --git a/SharpExcel/Exporters/EnumExporter.cs b/SharpExcel/Exporters/EnumExporter.cs index 3290e5f..b891b65 100644 --- a/SharpExcel/Exporters/EnumExporter.cs +++ b/SharpExcel/Exporters/EnumExporter.cs @@ -12,7 +12,7 @@ internal class EnumExporter /// /// instance of this run /// - public static Dictionary AddEnumDropdownMappingsToSheet(SharpExcelWriterInstanceData instance) + public static void AddEnumDropdownMappingsToSheet(SharpExcelWriterInstanceData instance) where TModel : class { int dropDownWorkbookColumn = 1; @@ -32,7 +32,7 @@ public static Dictionary AddEnumDropdownMappingsToSheet(Sh dropDownWorkbookColumn++; } - return dropdownDataMappings; + instance.DropdownMappings = dropdownDataMappings; } /// @@ -44,7 +44,7 @@ public static Dictionary AddEnumDropdownMappingsToSheet(Sh /// /// public static void WriteEnumValue(SharpExcelWriterInstanceData instance, Type type, object dataValue, - IXLCell cell, Dictionary dropdownDataMappings) + IXLCell cell) where TModel : class { if (instance.Properties.EnumMappings.TryGetValue(type, out var enumValues)) @@ -61,7 +61,7 @@ public static void WriteEnumValue(SharpExcelWriterInstanceData i } } - if (dropdownDataMappings.TryGetValue(type, out var range)) + if (instance.DropdownMappings.TryGetValue(type, out var range)) { cell.CreateDataValidation().List(instance.DropdownSourceWorksheet.Range(range), true); } diff --git a/SharpExcel/Exporters/ExporterHelpers.cs b/SharpExcel/Exporters/ExporterHelpers.cs index d241b03..af1137d 100644 --- a/SharpExcel/Exporters/ExporterHelpers.cs +++ b/SharpExcel/Exporters/ExporterHelpers.cs @@ -3,6 +3,7 @@ using ClosedXML.Excel; using SharpExcel.Exporters.Helpers; using SharpExcel.Extensions; +using SharpExcel.Models.Data; using SharpExcel.Models.Results; using SharpExcel.Models.Styling; using SharpExcel.Models.Styling.Constants; @@ -21,12 +22,12 @@ internal static class ExporterHelpers /// /// /// - public static void ApplyCellValidation(string sheetName, XLWorkbook workbook, ExcelReadResult parsedWorkbook) + public static void ApplyCellValidation(XLWorkbook workbook, ExcelReadResult parsedWorkbook) where TModel : class, new() { foreach (var result in parsedWorkbook.ValidationResults) { - var cell = workbook.Worksheet(sheetName).Cell(result.Value.Address.RowNumber, result.Value.Address.ColumnId); + var cell = workbook.Worksheet(result.Value.Address.SheetName).Cell(result.Value.Address.RowNumber, result.Value.Address.ColumnId); var stringBuilder = new StringBuilder(); foreach (var item in result.Value.ValidationResults) { @@ -45,7 +46,7 @@ public static void ApplyCellValidation(string sheetName, XLWorkbook work /// cell to wrtie to /// /// - public static object? TrySetCellValue(Dictionary> enumMappings, PropertyData columnData, IXLCell cell, CultureInfo cultureInfo) + public static object? TryGetCellValue(Dictionary> enumMappings, PropertyData columnData, IXLCell cell, CultureInfo cultureInfo) { //extract underlying nullable type if there is one var actualType = Nullable.GetUnderlyingType(columnData.PropertyInfo.PropertyType) ?? columnData.PropertyInfo.PropertyType; @@ -99,29 +100,31 @@ public static void ApplyCellValidation(string sheetName, XLWorkbook work return default; } - + /// /// Writes a row of data cells /// + /// targeting rule for this sheet /// instance data for this run /// the current data item being processed /// index of the row to write to - /// dropdown data mappings for the hidden enum dropdown sheet + /// optional offset column /// type of the data item being processed public static void WriteDataRow( + TargetingRule targetingRule, SharpExcelWriterInstanceData instance, TModel dataItem, - int rowIndex, - Dictionary dropdownDataMappings) + int rowIndex, + int? columnIndex) where TModel : class { for (var i = 0; i < instance.Properties.PropertyMappings.Count; i++) { var mapping = instance.Properties.PropertyMappings[i]; - var row = instance.MainWorksheet.Row(rowIndex); - var cell = instance.MainWorksheet.Cell(rowIndex, i + 1 /* use +1 because Excel starts at 1 */); - WriteDataCell(instance, dataItem, dropdownDataMappings, mapping, cell); + var row = instance.Workbook.Worksheet(targetingRule.SheetName).Row(rowIndex); + var cell = instance.Workbook.Worksheet(targetingRule.SheetName).Cell(rowIndex, i + (columnIndex ?? 1) /* use +1 because Excel starts at 1 */); + WriteDataCell(instance, dataItem, mapping, cell); cell.Style.ApplyStyle(GetCellStyle(instance, dataItem, mapping, row)); } } @@ -138,7 +141,6 @@ public static void WriteDataRow( private static void WriteDataCell( SharpExcelWriterInstanceData instance, TModel dataItem, - Dictionary dropdownDataMappings, PropertyData mapping, IXLCell cell) where TModel : class @@ -148,7 +150,7 @@ private static void WriteDataCell( //handle enums if (mapping.PropertyInfo.PropertyType.IsEnum) { - EnumExporter.WriteEnumValue(instance, mapping.PropertyInfo.PropertyType, dataValue, cell, dropdownDataMappings); + EnumExporter.WriteEnumValue(instance, mapping.PropertyInfo.PropertyType, dataValue, cell); } //handle format else if (mapping.Format != null) @@ -198,24 +200,27 @@ private static ExcelCellStyle GetCellStyle(SharpExcelWriterInstanceData< /// /// Writes header row to cell /// + /// Targeting rule for this sheet /// instance data for this run /// the row index to write to + /// optional column offset /// type of the data item being processed public static void WriteHeaderRow( + TargetingRule targetingRule, SharpExcelWriterInstanceData instance, - int rowIndex) + int rowIndex, int? columnOffset) where TModel : class { for (var columnIndex = 0; columnIndex < instance.Properties.PropertyMappings.Count; columnIndex++) { var mapping = instance.Properties.PropertyMappings[columnIndex]; - var cell = instance.MainWorksheet.Cell(rowIndex, columnIndex + 1 /* use +1 because Excel starts at 1 */); + var cell = instance.Workbook.Worksheet(targetingRule.SheetName).Cell(rowIndex, columnIndex + (columnOffset ?? 1) /* use +1 because Excel starts at 1 */); cell.Style.ApplyStyle(instance.HeaderStyle); if (mapping.ColumnWidth > 0) { - instance.MainWorksheet.Column(columnIndex + 1).Width = mapping.ColumnWidth; + instance.Workbook.Worksheet(targetingRule.SheetName).Column(columnIndex + (columnOffset ?? 1)).Width = mapping.ColumnWidth; } cell.SetValue(mapping.Name); diff --git a/SharpExcel/Exporters/SharpExcelWriterInstanceData.cs b/SharpExcel/Exporters/SharpExcelWriterInstanceData.cs index 4bafa75..bf819ac 100644 --- a/SharpExcel/Exporters/SharpExcelWriterInstanceData.cs +++ b/SharpExcel/Exporters/SharpExcelWriterInstanceData.cs @@ -1,6 +1,7 @@ using System.Globalization; using ClosedXML.Excel; using SharpExcel.Exporters.Helpers; +using SharpExcel.Models.Data; using SharpExcel.Models.Styling; using SharpExcel.Models.Styling.Rules; @@ -33,15 +34,25 @@ internal class SharpExcelWriterInstanceData /// public Dictionary>> StylingRuleLookup { get; set; } = new(); + /// + /// List of targeting rules, so we can look up rules for properties faster + /// + public List> TargetingRules { get; set; } = new(); + + /// + /// DropdownMappings for this instance + /// + public Dictionary DropdownMappings { get; set; } = new(); + /// /// Collection of property metadata (column name etc.) /// public PropertyDataCollection Properties { get; set; } = new(); /// - /// Main worksheet to use for reading/writing + /// Workbook for this instance /// - public IXLWorksheet MainWorksheet { get; set; } = null!; + public IXLWorkbook Workbook { get; set; } = null!; /// /// Hidden worksheet to serve as source for all generated dropdown menus