diff --git a/QuestPDF.Examples/DynamicExamples.cs b/QuestPDF.Examples/DynamicExamples.cs index c03ef65..23f6dea 100644 --- a/QuestPDF.Examples/DynamicExamples.cs +++ b/QuestPDF.Examples/DynamicExamples.cs @@ -9,68 +9,123 @@ using QuestPDF.Infrastructure; namespace QuestPDF.Examples { - public class TableWithSubtotals : IDynamic + public class OrderItem { - private ICollection Values { get; } - private Queue ValuesQueue { get; set; } - - public TableWithSubtotals(ICollection values) + public string ItemName { get; set; } = Placeholders.Label(); + public int Price { get; set; } = Placeholders.Random.Next(1, 11) * 10; + public int Count { get; set; } = Placeholders.Random.Next(1, 11); + } + + public class OrdersTable : IDynamicComponent + { + private ICollection Items { get; } + private ICollection ItemsLeft { get; set; } + + public OrdersTable(ICollection items) { - Values = values; + Items = items; } - public void Reset() + public void Compose(DynamicContext context, IDynamicContainer container) { - ValuesQueue = new Queue(Values); + if (context.Operation == DynamicLayoutOperation.Reset) + { + ItemsLeft = new List(Items); + return; + } + + var header = ComposeHeader(context); + var sampleFooter = ComposeFooter(context, Enumerable.Empty()); + var decorationHeight = header.Size.Height + sampleFooter.Size.Height; + + var rows = GetItemsForPage(context, decorationHeight).ToList(); + var footer = ComposeFooter(context, rows.Select(x => x.Item)); + + if (ItemsLeft.Count > rows.Count) + container.HasMoreContent(); + + if (context.Operation == DynamicLayoutOperation.Draw) + ItemsLeft = ItemsLeft.Skip(rows.Count).ToList(); + + container.Box().Decoration(decoration => + { + decoration.Header().Element(header); + + decoration.Content().Box().Stack(stack => + { + foreach (var row in rows) + stack.Item().Element(row.Element); + }); + + decoration.Footer().Element(footer); + }); } - public bool Compose(DynamicContext context, IContainer container) + private IDynamicElement ComposeHeader(DynamicContext context) { - var internalQueue = new Queue(ValuesQueue); - - container.Box().Border(2).Background(Colors.Grey.Lighten3).Stack(stack => + return context.CreateElement(element => { - var summaryHeight = 40f; - - var totalHeight = summaryHeight; - var total = 0; - - while (internalQueue.Any()) - { - var value = internalQueue.Peek(); - - var structure = context.Content(content => + element + .BorderBottom(1) + .BorderColor(Colors.Grey.Darken2) + .Padding(5) + .Row(row => { - content - .Padding(10) - .Text(value); + var textStyle = TextStyle.Default.SemiBold(); + + row.ConstantColumn(30).Text("#", textStyle); + row.RelativeColumn().Text("Item name", textStyle); + row.ConstantColumn(50).AlignRight().Text("Count", textStyle); + row.ConstantColumn(50).AlignRight().Text("Price", textStyle); + row.ConstantColumn(50).AlignRight().Text("Total", textStyle); }); - - var structureHeight = structure.Measure().Height; - - if (totalHeight + structureHeight > context.AvailableSize.Height) - break; - - totalHeight += structureHeight; - total += value; - - stack.Item().Border(1).Element(structure); - internalQueue.Dequeue(); - } - - stack - .Item() - .ShowEntire() - .Border(2) - .Background(Colors.Grey.Lighten1) - .Padding(10) - .Text($"Total: {total}", TextStyle.Default.SemiBold()); }); + } + + private IDynamicElement ComposeFooter(DynamicContext context, IEnumerable items) + { + var total = items.Sum(x => x.Count * x.Price); - if (context.IsDrawStep) - ValuesQueue = internalQueue; + return context.CreateElement(element => + { + element + .Padding(5) + .AlignRight() + .Text($"Subtotal: {total}$", TextStyle.Default.Size(14).SemiBold()); + }); + } + + private IEnumerable<(OrderItem Item, IDynamicElement Element)> GetItemsForPage(DynamicContext context, float decorationHeight) + { + var totalHeight = decorationHeight; + var counter = Items.Count - ItemsLeft.Count + 1; - return internalQueue.Any(); + foreach (var orderItem in ItemsLeft) + { + var element = context.CreateElement(content => + { + content + .BorderBottom(1) + .BorderColor(Colors.Grey.Lighten2) + .Padding(5) + .Row(row => + { + row.ConstantColumn(30).Text(counter++); + row.RelativeColumn().Text(orderItem.ItemName); + row.ConstantColumn(50).AlignRight().Text(orderItem.Count); + row.ConstantColumn(50).AlignRight().Text($"{orderItem.Price}$"); + row.ConstantColumn(50).AlignRight().Text($"{orderItem.Count*orderItem.Price}$"); + }); + }); + + var elementHeight = element.Size.Height; + + if (totalHeight + elementHeight > context.AvailableSize.Height) + break; + + totalHeight += elementHeight; + yield return (orderItem, element); + } } } @@ -81,12 +136,12 @@ namespace QuestPDF.Examples { RenderingTest .Create() - .PageSize(300, 500) + .PageSize(PageSizes.A5) .FileName() .ShowResults() .Render(container => { - var values = Enumerable.Range(0, 15).ToList(); + var items = Enumerable.Range(0, 25).Select(x => new OrderItem()).ToList(); container .Background(Colors.White) @@ -107,7 +162,7 @@ namespace QuestPDF.Examples decoration .Content() - .Dynamic(new TableWithSubtotals(values)); + .Dynamic(new OrdersTable(items)); }); }); } diff --git a/QuestPDF/Elements/Dynamic.cs b/QuestPDF/Elements/Dynamic.cs index ce8122e..60b62de 100644 --- a/QuestPDF/Elements/Dynamic.cs +++ b/QuestPDF/Elements/Dynamic.cs @@ -1,72 +1,56 @@ using System; using QuestPDF.Drawing; +using QuestPDF.Drawing.Exceptions; using QuestPDF.Infrastructure; namespace QuestPDF.Elements { internal class DynamicHost : Element, IStateResettable { - private IDynamic Child { get; } + private IDynamicComponent Child { get; } - public DynamicHost(IDynamic child) + public DynamicHost(IDynamicComponent child) { Child = child; } public void ResetState() { - Child.Reset(); + GetContent(Size.Zero, DynamicLayoutOperation.Reset); } internal override SpacePlan Measure(Size availableSpace) { - var content = GetContent(availableSpace, false); - var measurement = content.element.Measure(availableSpace); + var content = GetContent(availableSpace, DynamicLayoutOperation.Measure); + var measurement = content.Measure(availableSpace); - if (measurement.Type == SpacePlanType.FullRender) - return content.hasMore ? SpacePlan.PartialRender(measurement) : measurement; - - return measurement; + if (measurement.Type != SpacePlanType.FullRender) + throw new DocumentLayoutException("Dynamic component generated content that does not fit on a single page."); + + return content.HasMoreContent + ? SpacePlan.PartialRender(measurement) + : SpacePlan.FullRender(measurement); } internal override void Draw(Size availableSpace) { - GetContent(availableSpace, true).element.Draw(availableSpace); + GetContent(availableSpace, DynamicLayoutOperation.Draw).Draw(availableSpace); } - (Element element, bool hasMore) GetContent(Size availableSize, bool isDrawState) + DynamicContainer GetContent(Size availableSize, DynamicLayoutOperation operation) { var context = new DynamicContext { + PageNumber = PageContext.GetLocationPage(Infrastructure.PageContext.CurrentPageSlot), PageContext = PageContext, Canvas = Canvas, AvailableSize = availableSize, - IsDrawStep = isDrawState + Operation = operation }; - var container = new Container(); - var hasMore = Child.Compose(context, container); - - container.HandleVisitor(x => x?.Initialize(PageContext, Canvas)); - container.HandleVisitor(x => (x as IStateResettable)?.ResetState()); - - return (container, hasMore); - } - } - - public class DynamicContext - { - internal IPageContext PageContext { get; set; } - internal ICanvas Canvas { get; set; } - - public Size AvailableSize { get; internal set; } - public bool IsDrawStep { get; internal set; } - - public IDynamicElement Content(Action content) - { - var container = new DynamicElement(() => AvailableSize); - content(container); + var container = new DynamicContainer(); + Child.Compose(context, container); container.HandleVisitor(x => x?.Initialize(PageContext, Canvas)); container.HandleVisitor(x => (x as IStateResettable)?.ResetState()); @@ -75,23 +59,62 @@ namespace QuestPDF.Elements } } + public enum DynamicLayoutOperation + { + Reset, + Measure, + Draw + } + + public class DynamicContext + { + internal IPageContext PageContext { get; set; } + internal ICanvas Canvas { get; set; } + + public int PageNumber { get; internal set; } + public Size AvailableSize { get; internal set; } + public DynamicLayoutOperation Operation { get; internal set; } + + public IDynamicElement CreateElement(Action content) + { + var container = new DynamicElement(); + content(container); + + container.HandleVisitor(x => x?.Initialize(PageContext, Canvas)); + container.HandleVisitor(x => (x as IStateResettable)?.ResetState()); + + container.Size = container.Measure(AvailableSize); + + return container; + } + } + + public interface IDynamicContainer : IContainer + { + + } + + internal class DynamicContainer : Container, IDynamicContainer + { + internal bool HasMoreContent { get; set; } + } + + public static class DynamicContainerExtensions + { + public static IDynamicContainer HasMoreContent(this IDynamicContainer container) + { + (container as DynamicContainer).HasMoreContent = true; + return container; + } + } + public interface IDynamicElement : IElement { - Size Measure(); + Size Size { get; } } internal class DynamicElement : ContainerElement, IDynamicElement { - private Func AvailableSizeSource { get; } - - public DynamicElement(Func availableSizeSource) - { - AvailableSizeSource = availableSizeSource; - } - - Size IDynamicElement.Measure() - { - return Measure(AvailableSizeSource()); - } + public Size Size { get; internal set; } } } \ No newline at end of file diff --git a/QuestPDF/Fluent/ElementExtensions.cs b/QuestPDF/Fluent/ElementExtensions.cs index 99d7566..4491231 100644 --- a/QuestPDF/Fluent/ElementExtensions.cs +++ b/QuestPDF/Fluent/ElementExtensions.cs @@ -41,12 +41,12 @@ namespace QuestPDF.Fluent return handler(parent.Container()).Container(); } - public static void Dynamic(this IContainer element) where TDynamic : IDynamic, new() + public static void Dynamic(this IContainer element) where TDynamic : IDynamicComponent, new() { element.Dynamic(new TDynamic()); } - public static void Dynamic(this IContainer element, IDynamic dynamicElement) + public static void Dynamic(this IContainer element, IDynamicComponent dynamicElement) { element.Element(new DynamicHost(dynamicElement)); } diff --git a/QuestPDF/Helpers/Placeholders.cs b/QuestPDF/Helpers/Placeholders.cs index d79abd9..bd11a6a 100644 --- a/QuestPDF/Helpers/Placeholders.cs +++ b/QuestPDF/Helpers/Placeholders.cs @@ -7,7 +7,7 @@ namespace QuestPDF.Helpers { public static class Placeholders { - private static Random Random = new Random(); + public static readonly Random Random = new Random(); #region Word Cache diff --git a/QuestPDF/Infrastructure/IDynamic.cs b/QuestPDF/Infrastructure/IDynamic.cs deleted file mode 100644 index 1208634..0000000 --- a/QuestPDF/Infrastructure/IDynamic.cs +++ /dev/null @@ -1,10 +0,0 @@ -using QuestPDF.Elements; - -namespace QuestPDF.Infrastructure -{ - public interface IDynamic - { - void Reset(); - bool Compose(DynamicContext context, IContainer container); - } -} \ No newline at end of file diff --git a/QuestPDF/Infrastructure/IDynamicComponent.cs b/QuestPDF/Infrastructure/IDynamicComponent.cs new file mode 100644 index 0000000..d31cb9f --- /dev/null +++ b/QuestPDF/Infrastructure/IDynamicComponent.cs @@ -0,0 +1,9 @@ +using QuestPDF.Elements; + +namespace QuestPDF.Infrastructure +{ + public interface IDynamicComponent + { + void Compose(DynamicContext context, IDynamicContainer container); + } +} \ No newline at end of file