Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,027 changes: 680 additions & 347 deletions Source/Excel.Report.PDF/ExcelOverWriter.cs

Large diffs are not rendered by default.

153 changes: 153 additions & 0 deletions Source/Excel.Report.PDF/ExcelPageLoopProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
using System.Collections;
using ClosedXML.Excel;

namespace Excel.Report.PDF
{
/// <summary>
/// Builds page plans and creates body-page worksheets before OverWrite runs.
/// Paging stays separate because it can add and remove worksheets.
/// </summary>
internal static class ExcelPageLoopProcessor
{
/// <summary>
/// Identifies a worksheet's role in a paged report.
/// </summary>
internal enum PageType
{
First,
Body,
Last
}

/// <summary>
/// Stores the items and sheet names needed to process one paged loop.
/// </summary>
internal sealed class PageLoopPlan
{
public List<object?> AllItems { get; } = new();
public string FirstPageSheetName { get; set; } = string.Empty;
public int FirstPageItemCount { get; set; }
public string BodyTemplateSheetName { get; set; } = string.Empty;
public List<string> BodyPageSheetNames { get; } = new();
public int BodyPageItemCount { get; set; }
public string LastPageSheetName { get; set; } = string.Empty;
public int LastPageItemCount { get; set; }
public List<object?> FirstPageItems { get; set; } = new();
public List<List<object?>> BodyPageItems { get; } = new();
public List<object?> LastPageItems { get; set; } = new();
}

/// <summary>
/// Copies the body template for each page and removes the original source sheet.
/// </summary>
public static void MaterializeBodyPageSheets(XLWorkbook book, Dictionary<string, PageLoopPlan> pagePlans)
{
foreach (var entry in pagePlans)
{
if (string.IsNullOrEmpty(entry.Value.BodyTemplateSheetName)) continue;
var bodySheet = book.Worksheet(entry.Value.BodyTemplateSheetName);
for (var i = 0; i < entry.Value.BodyPageItems.Count; i++)
bodySheet.CopyTo($"{entry.Value.BodyTemplateSheetName}_{i}", bodySheet.Position + i);

book.Worksheets.Delete(entry.Value.BodyTemplateSheetName);
}
}

/// <summary>
/// Reads and splits paged-loop data once so converter work is not repeated for every page.
/// </summary>
public static async Task<Dictionary<string, PageLoopPlan>> BuildPagePlansAsync(XLWorkbook book, IExcelSymbolConverter converter)
{
var pagePlans = new Dictionary<string, PageLoopPlan>();
foreach (var sheet in book.Worksheets)
{
ExcelUtils.GetRowColCount(sheet, out var rowCount, out _);
var leftCells = new List<string>();
var pagedLoopCount = 0;
for (var i = 0; i <= rowCount; i++)
{
var text = sheet.GetText(i + 1, 1).Trim();
if (text.StartsWith("#PagedLoopRows")) pagedLoopCount++;
if (pagedLoopCount > 1) throw new Exception($"One sheet can have only one #PagedLoopRows. SheetName:{sheet.Name}");
leftCells.Add(text);
}

foreach (var leftCell in leftCells)
{
if (!leftCell.StartsWith("#PagedLoopRows")) continue;
var args = leftCell.Replace("#PagedLoopRows", "").Replace("(", "").Replace(")", "").Split(',').Select(e => e.Trim()).ToArray();
if (args.Length != 5 || !args[2].StartsWith("$")) break;

var items = args[2].Substring(1);
if (!pagePlans.TryGetValue(items, out var plan))
{
plan = new PageLoopPlan();
var enumerable = (await converter.GetData(items))?.Value as IEnumerable;
if (enumerable == null) break;
plan.AllItems.AddRange(enumerable.Cast<object?>());
pagePlans[items] = plan;
}

if (!Enum.TryParse<PageType>(args[0], out var pageType) ||
!int.TryParse(args[1], out var rowsPerPage) ||
!int.TryParse(args[4], out _)) break;

switch (pageType)
{
case PageType.First:
plan.FirstPageSheetName = sheet.Name;
plan.FirstPageItemCount = rowsPerPage;
break;
case PageType.Body:
plan.BodyTemplateSheetName = sheet.Name;
plan.BodyPageItemCount = rowsPerPage;
break;
case PageType.Last:
plan.LastPageSheetName = sheet.Name;
plan.LastPageItemCount = rowsPerPage;
break;
}

break;
}
}

// Split each source list once because all page sheets use the same boundaries.
foreach (var entry in pagePlans)
{
var plan = entry.Value;
if (!plan.AllItems.Any()) continue;

var bodyCount = plan.AllItems.Count - plan.FirstPageItemCount - plan.LastPageItemCount;
if (bodyCount == 0)
{
var firstPageCount = plan.FirstPageItemCount;
var lastPageCount = plan.AllItems.Count - plan.FirstPageItemCount;
if (lastPageCount <= 0)
{
lastPageCount = 1;
firstPageCount = plan.AllItems.Count - 1;
}

plan.FirstPageItems = plan.AllItems.Take(firstPageCount).ToList();
plan.LastPageItems = plan.AllItems.Skip(firstPageCount).Take(lastPageCount).ToList();
}
else
{
var rest = plan.AllItems.Skip(plan.FirstPageItemCount).ToList();
plan.FirstPageItems = plan.AllItems.Take(plan.FirstPageItemCount).ToList();
while (plan.LastPageItemCount < rest.Count)
{
plan.BodyPageItems.Add(rest.Take(plan.BodyPageItemCount).ToList());
rest = rest.Skip(plan.BodyPageItemCount).ToList();
}

plan.LastPageItems = rest;
for (var i = 0; i < plan.BodyPageItems.Count; i++) plan.BodyPageSheetNames.Add($"{plan.BodyTemplateSheetName}_{i}");
}
}

return pagePlans;
}
}
}
11 changes: 11 additions & 0 deletions Source/Excel.Report.PDF/IExcelSymbolConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,14 @@ public interface IExcelSymbolConverter
Task<ExcelOverWriteCell?> GetData(string symbol);
}
}

namespace Excel.Report.PDF
{
// Internal fast path for the built-in converter.
// It avoids creating an async wrapper for every cell; custom converters still use GetData above.
internal interface ISynchronousExcelSymbolConverter
{
// Lets the writer resolve a value without creating a Task.
bool TryGetData(string symbol, out object? value);
}
}
110 changes: 90 additions & 20 deletions Source/Excel.Report.PDF/ObjectExcelSymbolConverter.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
namespace Excel.Report.PDF
{
public class ObjectExcelSymbolConverter : IExcelSymbolConverter
public class ObjectExcelSymbolConverter : IExcelSymbolConverter, ISynchronousExcelSymbolConverter
{
object? _obj;
string _name = string.Empty;
// Caches property getters because the same symbols are often used many times.
sealed class PropertyAccessor
{
public PropertyAccessor(bool exists, Func<object, object?> getter)
{
Exists = exists;
Getter = getter;
}

public bool Exists { get; }
public Func<object, object?> Getter { get; }
}

static readonly PropertyAccessor MissingProperty = new(false, _ => null); // Cache missing properties too.
static readonly System.Collections.Concurrent.ConcurrentDictionary<(Type Type, string Name), PropertyAccessor> PropertyAccessors = new(); // Reuse getters across rows.
static readonly Task<ExcelOverWriteCell?> MissingData = Task.FromResult<ExcelOverWriteCell?>(null); // Reuse the common missing result.

readonly object? _obj;
readonly string _name = string.Empty;
public ObjectExcelSymbolConverter(object? obj) => _obj = obj;

ObjectExcelSymbolConverter(object? obj, string name)
Expand All @@ -15,29 +32,82 @@ public class ObjectExcelSymbolConverter : IExcelSymbolConverter
public IExcelSymbolConverter CreateChildExcelSymbolConverter(object? obj, string name)
=> new ObjectExcelSymbolConverter(obj, name);

public async Task<ExcelOverWriteCell?> GetData(string symbol)
public Task<ExcelOverWriteCell?> GetData(string symbol)
{
// Keep the public async API while using the faster synchronous lookup internally.
if (!TryGetData(symbol, out var value)) return MissingData;
return Task.FromResult<ExcelOverWriteCell?>(new ExcelOverWriteCell { Value = value });
}

public bool TryGetData(string symbol, out object? value)
{
value = null;

if (_obj == null) return false;

var propertyName = symbol;
if (!string.IsNullOrEmpty(_name))
{
var prefix = _name + ".";
if (!symbol.StartsWith(prefix, StringComparison.Ordinal)) return false;
propertyName = symbol.Substring(prefix.Length);
}

return TryGetPropertyValue(_obj, propertyName, out value);
}

public Task<ExcelOverWriteCell?> GetData(object? element, string elementName, string symbol)
{
if (_obj == null) return MissingData;

var prefix = elementName + ".";
if (!symbol.StartsWith(prefix, StringComparison.Ordinal)) return MissingData;
if (element == null)
return Task.FromResult<ExcelOverWriteCell?>(new ExcelOverWriteCell());

return TryGetPropertyValue(element, symbol.Substring(prefix.Length), out var value)
? Task.FromResult<ExcelOverWriteCell?>(new ExcelOverWriteCell { Value = value })
: MissingData;
}

static bool TryGetPropertyValue(object target, string propertyName, out object? value)
{
await Task.CompletedTask;
// Create each type/property lookup only once.
var accessor = PropertyAccessors.GetOrAdd(
(target.GetType(), propertyName),
static key => CreatePropertyAccessor(key.Type, key.Name));

if (_obj == null)
return null;
if (!accessor.Exists)
{
value = null;
return false;
}

if(!string.IsNullOrEmpty(_name))
return await GetData(_obj, _name, symbol);
var prop = _obj.GetType().GetProperty(symbol);
return prop == null ? null : new ExcelOverWriteCell { Value = prop.GetValue(_obj) };
value = accessor.Getter(target);
return true;
}

public async Task<ExcelOverWriteCell?> GetData(object? element, string elementName, string symbol)
static PropertyAccessor CreatePropertyAccessor(Type type, string propertyName)
{
await Task.CompletedTask;

if (_obj == null)
return null;
if (!symbol.StartsWith(elementName + ".")) return null;
if (element == null) return new ExcelOverWriteCell();
var prop = element.GetType().GetProperty(symbol.Substring((elementName + ".").Length));
return prop == null ? null : new ExcelOverWriteCell { Value = prop.GetValue(element) };
var property = type.GetProperty(propertyName);
if (property == null || property.GetMethod == null)
return MissingProperty;

try
{
// Compiled getters avoid reflection on every output cell.
var target = System.Linq.Expressions.Expression.Parameter(typeof(object), "target");
var typedTarget = System.Linq.Expressions.Expression.Convert(target, type);
var propertyValue = System.Linq.Expressions.Expression.Property(typedTarget, property);
var boxedValue = System.Linq.Expressions.Expression.Convert(propertyValue, typeof(object));
var getter = System.Linq.Expressions.Expression.Lambda<Func<object, object?>>(boxedValue, target).Compile();
return new PropertyAccessor(true, getter);
}
catch (ArgumentException)
{
// Fall back to reflection for properties that cannot be compiled.
return new PropertyAccessor(true, property.GetValue);
}
}

}
Expand Down
Loading