Skip to content

Dynamic composition: mixed content blocks ​

Many documents are not designed as fixed layouts but rather as sequences of content blocks: headings, paragraphs, images, quotes, and so on. The exact order and number of blocks is usually known only at runtime, for example when the content is loaded from a database, a CMS, or provided by the end-user.

Because QuestPDF describes documents with plain C# code, this scenario does not require any special API. You can model the content as a simple class hierarchy and use pattern matching to translate each block into its visual representation.

Data model ​

Each supported block type is modeled as a record deriving from a common base type:

csharp
public abstract record ContentBlock;
public sealed record HeadingBlock(string Text) : ContentBlock;
public sealed record ParagraphBlock(string Text) : ContentBlock;
public sealed record ImageBlock(byte[] Data) : ContentBlock;
public sealed record QuoteBlock(string Text, string Author) : ContentBlock;

In a real application, this collection would likely be built from external data. For this example, it is created in code with help of the Placeholders class:

csharp
var blocks = new List<ContentBlock>
{
    new HeadingBlock("Quarterly Product Update"),
    new ParagraphBlock(Placeholders.Paragraph()),
    new ImageBlock(Placeholders.Image(600, 200)),
    new QuoteBlock("This release cut our document generation time in half.", "Anna Kowalska, Operations Lead"),
    new ParagraphBlock(Placeholders.Paragraph())
};

Block composition ​

A dedicated method uses pattern matching to decide how each block type is rendered. Simple blocks map to a single element, while more complex ones (such as the quote) can use any layout structure.

csharp
private void ComposeBlock(IContainer container, ContentBlock block)
{
    if (block is HeadingBlock heading)
    {
        container.Text(heading.Text).FontSize(24).SemiBold().FontColor(Colors.Blue.Darken2);
        return;
    }

    if (block is ParagraphBlock paragraph)
    {
        container.Text(paragraph.Text);
        return;
    }

    if (block is ImageBlock image)
    {
        container.Image(image.Data);
        return;
    }

    if (block is QuoteBlock quote)
    {
        container
            .BorderLeft(3)
            .BorderColor(Colors.Blue.Medium)
            .PaddingLeft(15)
            .Column(column =>
            {
                column.Item().Text(quote.Text).Italic();
                column.Item().PaddingTop(5).Text($"— {quote.Author}").FontColor(Colors.Grey.Darken1);
            });

        return;
    }

    throw new NotSupportedException($"Unsupported content block: {block.GetType().Name}");
}

TIP

The final throw statement is intentional. When a new block type is added to the model but not yet supported by the composition logic, the document generation fails immediately instead of silently skipping content.

Usage ​

The final document is a simple column that iterates over the collection and delegates each block to the composition method:

csharp
container.Column(column =>
{
    column.Spacing(15);

    foreach (var block in blocks)
        column.Item().Element(blockContainer => ComposeBlock(blockContainer, block));
});

example