From 26f900ccee1a83dd494fb70d4237a878636d64dd Mon Sep 17 00:00:00 2001 From: Neil Brommer Date: Mon, 10 Jul 2023 13:25:29 -0700 Subject: [PATCH] Add Excel auto filler post --- src/posts/ExcelAutofiller.md | 442 +++++++++++++++++++++++++++++++++++ 1 file changed, 442 insertions(+) create mode 100644 src/posts/ExcelAutofiller.md diff --git a/src/posts/ExcelAutofiller.md b/src/posts/ExcelAutofiller.md new file mode 100644 index 0000000..5983c12 --- /dev/null +++ b/src/posts/ExcelAutofiller.md @@ -0,0 +1,442 @@ +--- +title: Excel Autofiller +description: A set of functions for automatically filling excel sheets +tags: [ Programming, C# ] +--- + +These functions will take either an `IEnumerable` or `DataTable` and uses reflection to fill an Excel sheet. This is built for [EPPlus](https://epplussoftware.com/), but it could probably be easily converted to use other libraries. + +Attributes on the model can be used to customize the output. The attributes used are the same as the [table generator](/posts/TableAutoGenerator/) so you can use the same model for both. + +## How To Use + +The model in the `IEnumerable`: + +```csharp +public class Thing +{ + // Don't make a column for this property + [Display(AutoGenerateField = false)] + public int ThingId { get; set; } + + // Use "Thing Name" for the column header instead of "ThingName" + [Display(Name = "Thing Name")] + public string ThingName { get; set; } + + // Use string.Format with the DataFormatString + [DisplayFormat(DataFormatString = "{0:C}")] + public decimal Price { get; set; } +} +``` + +Then to fill the Excel sheet: + +```csharp +using ExcelPackage package = new(); +package.Workbook.Worksheets.Add("Data") + .FillReport(reportData, "Title"); +``` + +Other functions can be used to more granularly control how the sheet is filled. + +## Code + +```csharp +using OfficeOpenXml.Style; +using OfficeOpenXml; +using System.ComponentModel.DataAnnotations; +using System.Data; +using System.Reflection; + +/// +/// Extension methods for filling an Excel sheet with data +/// +public static class ExcelFiller +{ + #region Fill from IEnumerable + + /// + /// Fill an Excel sheet with a title, column headers, and data. + /// + /// The type of data to fill the sheet with + /// The worksheet to fill with data + /// The title to put at the top of the worksheet + /// The data to use to fill the worksheet + /// + /// The . This is mutable - returning it only helps with + /// chaining. + /// + public static ExcelWorksheet FillReport(this ExcelWorksheet worksheet, + IEnumerable data, string title) + { + // Set our printer settings + worksheet.PrinterSettings.Orientation = eOrientation.Landscape; + //workSheet.PrinterSettings.FitToPage = true; + //workSheet.PrinterSettings.FitToHeight = 0; + worksheet.PrinterSettings.BottomMargin = .5m; + worksheet.PrinterSettings.LeftMargin = .5m; + worksheet.PrinterSettings.RightMargin = .5m; + worksheet.PrinterSettings.TopMargin = .5m; + + PropertyInfo[] propsToDisplay = GetPropertiesForExcelColumns(); + + // Set the title + using (ExcelRange titleCells = worksheet.Cells[1, 1, 1, propsToDisplay.Count()]) + { + titleCells.Merge = true; + titleCells.Value = title; + titleCells.Style.Font.Bold = true; + titleCells.Style.Font.Size = 12; + titleCells.Style.HorizontalAlignment = ExcelHorizontalAlignment.Left; + } + + worksheet + .InsertDataWithColumnHeaders(data, propsToDisplay, 2) + .Cells[worksheet.Dimension.Address].AutoFitColumns(); + + return worksheet; + } + + /// + /// Fill the given excel worksheet with the given data beginning at the given starting row. + /// Column headers are generated based on the type + /// + /// The type of data in the list + /// The worksheet to fill + /// The data to fill the worksheet with + /// The row to begin filling the sheet at. ONE INDEXED. + /// + /// The . This is mutable - returning it only helps with + /// chaining. + /// + public static ExcelWorksheet InsertDataWithColumnHeaders(this ExcelWorksheet worksheet, + IEnumerable data, int startRow = 1) + { + PropertyInfo[] propsToDisplay = GetPropertiesForExcelColumns(); + + return worksheet + .InsertColumnHeaders(propsToDisplay, startRow) + .InsertData(data, propsToDisplay, startRow + 1); + } + + /// + /// Fill the given excel worksheet with the given data beginning at the given starting row. + /// Column headers are generated based on the type + /// + /// The type of data in the list + /// The worksheet to fill + /// The data to fill the worksheet with + /// + /// The properties of to display + /// + /// The row to begin filling the sheet at. ONE INDEXED. + /// + /// The . This is mutable - returning it only helps with + /// chaining. + /// + public static ExcelWorksheet InsertDataWithColumnHeaders(this ExcelWorksheet worksheet, + IEnumerable data, PropertyInfo[] propsToDisplay, int startRow = 1) + { + return worksheet + .InsertColumnHeaders(propsToDisplay, startRow) + .InsertData(data, propsToDisplay, startRow + 1); + } + + /// + /// Fill the given excel worksheet with the given data beginning at the given starting row. + /// Column headers are not created. + /// + /// The type of data in the list + /// The worksheet to fill + /// The data to fill the worksheet with + /// The row to begin filling the sheet at. ONE INDEXED. + /// + /// The . This is mutable - returning it only helps with + /// chaining. + /// + public static ExcelWorksheet InsertData(this ExcelWorksheet worksheet, + IEnumerable data, int startRow = 1) + { + PropertyInfo[] propsToDisplay = GetPropertiesForExcelColumns(); + return worksheet + .InsertData(data, propsToDisplay, startRow); + } + + /// + /// Fill the given excel worksheet with the given data beginning at the given starting row. + /// Column headers are not created. + /// + /// The type of data in the list + /// The worksheet to fill + /// The data to fill the worksheet with + /// + /// The properties of to display + /// + /// The row to begin filling the sheet at. ONE INDEXED. + /// + /// The . This is mutable - returning it only helps with + /// chaining. + /// + public static ExcelWorksheet InsertData(this ExcelWorksheet worksheet, + IEnumerable data, PropertyInfo[] propsToDisplay, int startRow = 1) + { + (PropertyInfo Property, Attribute Attribute)[] propAttributes = propsToDisplay + .Select(p => + { + // Currently [DisplayFormat] is the only attribute used + // You can add more with ?? + var attributes = p.GetCustomAttributes(); + Attribute attrib = attributes + .OfType() + .FirstOrDefault(); + return (p, attrib); + }) + .ToArray(); + + // Fill the data + int currentRow = startRow; + foreach (var row in data) + { + int currentColumn = 1; // Excel indexes start at 1, not 0 + foreach ((PropertyInfo property, Attribute attribute) in propAttributes) + { + var cellValue = property.GetValue(row); + ExcelRange cell = worksheet.Cells[currentRow, currentColumn]; + cell.Value = FormatCellValue(cellValue, attribute); + + currentColumn++; + } + + currentRow++; + } + + return worksheet; + } + + /// + /// Fill out the column headers for the type T + /// + /// The type to use to determine column headers + /// The worksheet to add the column headers to + /// The row to insert the column headers in + /// + /// The . This is mutable - returning it only helps with + /// chaining. + /// + public static ExcelWorksheet InsertColumnHeaders(this ExcelWorksheet worksheet, + int columnHeaderRow = 1) + { + PropertyInfo[] propsToDisplay = GetPropertiesForExcelColumns(); + return worksheet + .InsertColumnHeaders(propsToDisplay, columnHeaderRow); + } + + /// + /// Fill out the column headers for the type + /// + /// The type to use to determine column headers + /// The worksheet to add the column headers to + /// The properties that should be displayed + /// The row to insert the column headers in + /// + /// The . This is mutable - returning it only helps with + /// chaining. + /// + public static ExcelWorksheet InsertColumnHeaders(this ExcelWorksheet worksheet, + PropertyInfo[] propsToDisplay, int columnHeaderRow = 1) + { + worksheet.Row(columnHeaderRow).Style.Font.Bold = true; + for (int i = 0; i < propsToDisplay.Count(); i++) + { + // The Property/Attribute array indexes are 0 indexed, but the cells are 1 indexed + + DisplayAttribute[] displayAttributes = (DisplayAttribute[])propsToDisplay[i] + .GetCustomAttributes(typeof(DisplayAttribute), false); + + if (displayAttributes.Length > 0 && displayAttributes[0].Name != null) + worksheet.Cells[columnHeaderRow, i + 1].Value = displayAttributes[0].Name; + else + worksheet.Cells[columnHeaderRow, i + 1].Value = propsToDisplay[i].Name; + } + + return worksheet; + } + + /// + /// Get the properties on that should be displayed in Excel sheets + /// + /// The type to get the properties of + public static PropertyInfo[] GetPropertiesForExcelColumns() + { + return typeof(T).GetProperties() + .Where(p => + { + DisplayAttribute[] displayAttributes = + (DisplayAttribute[])p.GetCustomAttributes(typeof(DisplayAttribute), false); + return displayAttributes.Length == 0 + || displayAttributes[0].GetAutoGenerateField() != false; + }) + .ToArray(); + } + + /// + /// Uses the attribute to determine how to format the value + /// + /// The value to format + /// The attribute to use to format the value + /// The formatted value + private static object FormatCellValue(object value, Attribute attribute) + { + // Format negative currency values as "-$123.45" instead of "($123.45)" + string cultureString = System.Threading.Thread.CurrentThread.CurrentCulture.ToString(); + System.Globalization.NumberFormatInfo currencyFormat = + new System.Globalization.CultureInfo(cultureString).NumberFormat; + currencyFormat.CurrencyNegativePattern = 1; + + if (attribute is DisplayFormatAttribute dfAttribute + && dfAttribute.DataFormatString != null) + return string.Format(currencyFormat, dfAttribute.DataFormatString, value); + // AutoFitColumns sets the column too narrow if this is formatted as an actual date + // So use a string instead + else if (value is DateTime dtValue) + return dtValue.ToString("g"); + + return value; + } + + #endregion + + #region Fill from DataTable + + /// + /// Fill an Excel sheet with a title, column headers, and data. + /// + /// The worksheet to fill with data + /// The title to put at the top of the worksheet + /// The data to use to fill the worksheet + /// + /// The . This is mutable - returning it only helps with + /// chaining. + /// + public static ExcelWorksheet FillReport(this ExcelWorksheet worksheet, DataTable data, + string title) + { + // Format negative currency values as "-$123.45" instead of "($123.45)" + string cultureString = System.Threading.Thread.CurrentThread.CurrentCulture.ToString(); + System.Globalization.NumberFormatInfo currencyFormat = new System.Globalization.CultureInfo(cultureString).NumberFormat; + currencyFormat.CurrencyNegativePattern = 1; + + //set our printer settings.... + worksheet.PrinterSettings.Orientation = eOrientation.Landscape; + //workSheet.PrinterSettings.FitToPage = true; + //workSheet.PrinterSettings.FitToHeight = 0; + worksheet.PrinterSettings.BottomMargin = .5m; + worksheet.PrinterSettings.LeftMargin = .5m; + worksheet.PrinterSettings.RightMargin = .5m; + worksheet.PrinterSettings.TopMargin = .5m; + + // Set the report title in the first row + using (ExcelRange cells = worksheet.Cells[1, 1, 1, data.Columns.Count]) + { + cells.Merge = true; + cells.Value = title; + cells.Style.Font.Bold = true; + cells.Style.Font.Size = 12; + cells.Style.HorizontalAlignment = ExcelHorizontalAlignment.Left; + } + + worksheet + .InsertColumnHeaders(data, 2) + .InsertData(data, 3); + + worksheet.Cells[worksheet.Dimension.Address].AutoFitColumns(); + return worksheet; + } + + /// + /// Fill out the column headers using the 's column titles + /// + /// The worksheet to add the column headers to + /// The DataTable to get the column titles from + /// The row to insert the column headers in + /// + /// The . This is mutable - returning it only helps with + /// chaining. + /// + public static ExcelWorksheet InsertColumnHeaders(this ExcelWorksheet worksheet, + DataTable data, int columnHeaderRow = 1) + { + worksheet.Row(columnHeaderRow).Style.Font.Bold = true; + + int currentColumn = 1; + foreach (DataColumn dc in data.Columns) + { + worksheet.Cells[columnHeaderRow, currentColumn].Value = dc.ColumnName; + currentColumn++; + } + + return worksheet; + } + + /// + /// Insert data from a DataTable beginning at the given + /// + /// The worksheet to fill + /// The data to fill the worksheet with + /// The row to begin filling the worksheet at. ONE INDEXED. + /// + /// The . This is mutable - returning it only helps with + /// chaining. + /// + public static ExcelWorksheet InsertData(this ExcelWorksheet worksheet, DataTable data, + int startRow = 1) + { + int currentRow = startRow; + foreach (DataRow dr in data.Rows) + { + int currentColumn = 1; + foreach (object value in dr.ItemArray) + { + ExcelRange cell = worksheet.Cells[currentRow, currentColumn]; + cell.Style.HorizontalAlignment = ExcelHorizontalAlignment.Left; + + cell.Value = FormatCellValue(value); + currentColumn++; + } + + currentRow++; + } + + return worksheet; + } + + /// + /// Handles formatting the given value + /// + /// + /// The value converted to a cleaner type + public static object FormatCellValue(object value) + { + // Some non-text values are stored as strings, convert them to their real type + if (value is string strValue) + { + if (int.TryParse(strValue, out int intValue)) + return intValue; + else if (decimal.TryParse(strValue, out decimal decimalValue)) + return decimalValue; + else if (value.ToString().ToUpper() == true.ToString().ToUpper()) + return 1; + else if (value.ToString().ToUpper() == false.ToString().ToUpper()) + return 0; + else + return value; + } + else if (value is bool boolValue) + return boolValue ? 1 : 0; + else + return value.ToString(); + } + + #endregion +} +```