Dynamic & data-driven content
Most documents that matter are generated from data: an invoice from line items, a report from a database query, a catalog from a product list. Because the Rivoli PDF document model is plain C# objects, you build it with ordinary loops, conditionals, and LINQ: there’s no template language to learn.
Build content in a loop
Section titled “Build content in a loop”The core move: iterate your data and Add a block (or row, or item) per element.
using Rivoli.Pdf;using Rivoli.Pdf.Model;
public Document BuildCatalog(IEnumerable<Product> products){ var doc = Document.Create("Product Catalog"); var section = doc.AddSection();
foreach (var product in products) { var para = new Paragraph(); para.Inlines.Add(new Run(product.Name) { Style = new Style().Bold() }); para.Inlines.Add(new Run($", ${product.Price:0.00}")); section.Add(para); }
return doc;}Data-driven tables
Section titled “Data-driven tables”Tables are the most common data-driven shape. Build the header once, then loop the rows:
public Block BuildLineItems(IReadOnlyList<LineItem> items){ var table = new Table().Header("Item", "Qty", "Unit price", "Total");
foreach (var item in items) { table.AddDataRow( item.Description, item.Quantity.ToString(), item.UnitPrice.ToString("C"), (item.Quantity * item.UnitPrice).ToString("C")); }
table.AddDataRow("", "", "Total", items.Sum(i => i.Quantity * i.UnitPrice).ToString("C")); return table;}Header(...), AddDataRow(...), and Sum(...) are ordinary calls: the table is
just a C# object you populate.
Data-driven lists
Section titled “Data-driven lists”var list = List.Unordered();foreach (var finding in report.Findings) list.Add(finding.Summary);
section.Add(list);
// Or, more fluently, with the Items extension:section.Add(List.Ordered().Items(steps.Select(s => s.Title)));Conditional content
Section titled “Conditional content”Conditionals decide whether a block appears. Plain if statements work because
the model is built imperatively:
var section = doc.AddSection();
if (report.HasExecutiveSummary) section.AddParagraph(report.Summary);
section.AddParagraph(report.Body);
if (report.IsConfidential) section.AddParagraph("CONFIDENTIAL", s => s.Bold().WithForegroundColor(Color.Red));For optional content that’s expensive to compute, guard the construction, not just
the Add: there’s no point building a block you won’t use.
Group and section by data
Section titled “Group and section by data”Use LINQ to shape the data, then map each group to a section or a heading:
var byCategory = products .GroupBy(p => p.Category) .OrderBy(g => g.Key);
foreach (var group in byCategory){ var section = doc.AddSection(); section.H2(group.Key);
foreach (var product in group.OrderBy(p => p.Name)) section.AddParagraph($"{product.Name}, ${product.Price:0.00}");}Combine with components and helpers
Section titled “Combine with components and helpers”Data-driven code stays readable when each element maps to a component or helper, so the loop body is one line:
foreach (var order in orders) section.Add(OrderCard(order)); // OrderCard returns a Block
// where OrderCard is a component:static Block OrderCard(Order order) => new ContainerBlock() .WithBorder(Color.Gray, 1) .WithPadding(10) .Add(new Paragraph($"Order #{order.Id}") { Style = new Style().Bold() }) .Add(new Paragraph($"Placed {order.Date:d}, {order.Total:C}"));The loop describes the structure (“a card per order”); the component describes the content (“what a card looks like”). That separation is what keeps generated documents maintainable as both the data and the design evolve.
Guidelines
Section titled “Guidelines”- Loop to repeat,
ifto include: content is just imperative C#. - Shape data with LINQ first (group, order, filter), then map to document structure.
- Push per-element rendering into a component so loop bodies stay one line.
- Format values at the edge (
ToString("C"),:0.00) where you add them, close to the data.