Skip to content

Document structure & composition

A real document (a report, a contract, a multi-page invoice) is too big to build in one method without it becoming an unreadable wall of .Add(...) calls. This pattern keeps large documents legible: compose the whole from small, named pieces, each responsible for one part.

Start with a top-level method that lays out the document’s shape and delegates the content of each part to a helper that returns a Block (a component) or configures a Section.

using Rivoli.Pdf;
using Rivoli.Pdf.Model;
public Document BuildReport(ReportData data)
{
var doc = Document.Create(data.Title, data.Author);
doc.DefaultPageSize = PageSize.A4;
doc.DefaultMargin = Margin.Standard;
BuildCover(doc, data);
BuildSummary(doc, data);
BuildFinancials(doc, data);
BuildAppendix(doc, data);
return doc;
}

The top-level method now reads like a table of contents. Each Build* helper owns one concern:

private void BuildCover(Document doc, ReportData data)
{
var cover = doc.AddSection();
cover.H1(data.Title);
cover.AddParagraph(data.Author);
cover.AddPageBreak();
}
private void BuildSummary(Document doc, ReportData data)
{
var section = doc.AddSection();
section.H2("Executive summary");
section.AddParagraph(data.Summary);
}

(H1, H2, AddParagraph, and AddPageBreak are built-in section extensions.)

A Section is the natural unit of composition: it owns its own page size, margins, columns, header/footer, and background. Mapping one section to one logical part of the document means each part can have its own page setup without affecting the others.

private void BuildAppendix(Document doc, ReportData data)
{
var appendix = doc.AddSection();
appendix.PageSize = PageSize.Letter.Rotate(); // landscape, appendix only
appendix.WithColumns(ColumnLayout.TwoColumns);
appendix.H2("Appendix: detailed data");
// ...
}

For documents whose structure is fixed, the WithSection/WithSections extensions read top-down without intermediate variables:

using Rivoli.Pdf.Builders;
var doc = Document.Create("Report", "Team")
.WithSection(cover => cover.H1("Report").AddPageBreak())
.WithSection(summary => summary.H2("Summary").AddParagraph(text))
.WithSection(body => BuildBody(body, data));

Each lambda is a small, self-contained description of one section, and you can still hand off to a named helper (BuildBody) when a section gets complex. See Working with the builder for choosing between these styles.

The most maintainable layout puts structure in one place and content rules in another:

  • Structure: the BuildReport method: which sections exist, in what order, with what page setup.
  • Content: the per-part helpers and components: what each part actually contains.
  • Style: a shared StyleSheet or a set of style extension methods, referenced by both.

When the brand changes, you touch the styles. When the report grows a section, you touch the structure method. When a section’s wording changes, you touch one helper. Each kind of change lives in one place.

  • One method per logical part, named for the part (BuildFinancials, not BuildSection3).
  • Pass the document or section in, return nothing: or return a Block and let the caller place it (the component style). Pick one and be consistent within a document.
  • Hoist page setup to the top. Set DefaultPageSize/DefaultMargin on the document once; override per section only where it genuinely differs.

Next: Local helper methods for keeping the individual Build* methods themselves tidy.