You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用QuestPDF在Blazor Server中创建形式发票模板报错求助

问题:Blazor Server + QuestPDF 生成发票触发DocumentComposeException错误

我正在开发基于Blazor Server的订单管理应用,使用QuestPDF(2024.7.3版本)生成PDF形式的发票。添加特定内容段时触发以下错误:

QuestPDF.Drawing.Exceptions.DocumentComposeException: You should not assign multiple child elements to a single-child container. This may happen when a container variable is used outside of its scope/closure OR the container is used in multiple fluent API chains OR the container is used incorrectly in a loop. This exception is thrown to help you detect that some part of the code is overriding fragments of the document layout with a new content - essentially destroying existing content.

相关代码

ProformaInvoiceDocument.cs

using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using Image = QuestPDF.Infrastructure.Image;

namespace IMS.CoreBusiness;

public class ProformaInvoiceDocument : IDocument
{
    public static Image LogoImage { get; } = Image.FromFile("wwwroot/css/images/logo.png");

    public ProformaInvoice Model { get; }

    public ProformaInvoiceDocument(ProformaInvoice model)
    {
        Model = model;
    }

    public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
    public DocumentSettings GetSettings() => DocumentSettings.Default;

    public void Compose(IDocumentContainer container)
    {
        container.Page(page =>
        {
            page.Size(PageSizes.A4);
            page.Margin(1, Unit.Centimetre);
            page.PageColor(Colors.White);
            page.DefaultTextStyle(x => x.FontSize(12));

            page.Header().Component(new InvoiceHeader(LogoImage));
            page.Content().Component(new InvoiceContent(Model.InvoiceNumber, Model.InvoiceDate, Model.CustomerName, Model.CustomerAddress, Model.Items, Model.Subtotal, Model.VAT, Model.Total, Model.BankDetails, Model.Notes));
            //page.Footer().Component(new InvoiceFooter(Model.FooterText));
        });
    }
}

InvoiceHeader.cs

using QuestPDF.Fluent;
using QuestPDF.Infrastructure;

public class InvoiceHeader : IComponent
{
    private readonly Image LogoImage;

    public InvoiceHeader(Image logoImage)
    {
        LogoImage = logoImage;
    }

    public void Compose(IContainer container)
    {
        container.Row(row =>
        {
            row.ConstantItem(170).Image(LogoImage);
        });
    }
}

调用代码

void Submit(Order arg)
{
    var invoiceDocument = new IMS.CoreBusiness.ProformaInvoice
    {
        InvoiceNumber = "123456",
        InvoiceDate = DateTime.Now,
        CustomerName = "Example Customer",
        CustomerAddress = "123 Example Street",
        Items = new List<InvoiceItem>
        {
            new InvoiceItem { Code = "ABC123", Description = "Item 1", ManufacturerCode = "MAN123", ManufacturerName = "Company A", Price = 100 },
            new InvoiceItem { Code = "XYZ456", Description = "Item 2", ManufacturerCode = "MAN456", ManufacturerName = "Company B", Price = 200 }
        },
        Subtotal = 300,
        VAT = 54,
        Total = 354,
        BankDetails = "Your Bank Details Here",
        Notes = "Terms and conditions apply.",
        FooterText = "Company Address, Contact Info, etc."
    };

    var document = new ProformaInvoiceDocument(invoiceDocument);
    document.GeneratePdfAndShow();
    // document.ShowInPreviewer();
}

错误原因

  1. 单容器多子元素冲突:原InvoiceContent.Compose方法中,直接对container连续调用Row()和Column(),QuestPDF的IContainer是单子元素容器,只能接受一个直接子元素,多次赋值会覆盖原有布局并触发异常。
  2. 表格行写法错误:循环中直接调用TextExtensions.Text(table.Cell(), ...)会重复使用同一个Cell容器,同样违反单容器规则。

解决方案与修正后代码

修正后的InvoiceContent.cs

using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
using System.Collections.Generic;

public class InvoiceContent : IComponent
{
    private readonly List<InvoiceItem> Items;
    private readonly decimal Subtotal;
    private readonly decimal VAT;
    private readonly decimal Total;
    private readonly string BankDetails;
    private readonly string Notes;
    private readonly string InvoiceNumber;
    private readonly DateTime InvoiceDate;
    private readonly string CustomerName;
    private readonly string CustomerAddress;

    public InvoiceContent(string invoiceNumber, DateTime invoiceDate, string customerName, string customerAddress, List<InvoiceItem> items, decimal subtotal, decimal vat, decimal total, string bankDetails, string notes)
    {
        InvoiceNumber = invoiceNumber;
        InvoiceDate = invoiceDate;
        CustomerName = customerName;
        CustomerAddress = customerAddress;
        Items = items;
        Subtotal = subtotal;
        VAT = vat;
        Total = total;
        BankDetails = bankDetails;
        Notes = notes;
    }

    public void Compose(IContainer container)
    {
        // 将所有内容包裹在一个顶级Column容器中,确保container只有一个直接子元素
        container.Column(mainColumn =>
        {
            mainColumn.Spacing(20);

            // 发票信息区域
            mainColumn.Item().Row(row =>
            {
                row.Spacing(30);
                row.RelativeItem().Column(infoColumn =>
                {
                    infoColumn.Item().Text(text =>
                    {
                        text.Span("Invoice No: ").SemiBold();
                        text.Span($"{InvoiceNumber}");
                    });
                    infoColumn.Item().Text(text =>
                    {
                        text.Span("Date: ").SemiBold();
                        text.Span($"{InvoiceDate:dd/MM/yyyy}");
                    });
                    infoColumn.Item().Text(text =>
                    {
                        text.Span("Customer: ").SemiBold();
                        text.Span($"{CustomerName}");
                    });
                    infoColumn.Item().Text(text =>
                    {
                        text.Span("Address: ").SemiBold();
                        text.Span($"{CustomerAddress}");
                    });
                });
            });

            // 商品表格及后续内容
            mainColumn.Item().Column(contentColumn =>
            {
                contentColumn.Spacing(10);

                // 商品表格
                contentColumn.Item().Table(table =>
                {
                    table.ColumnsDefinition(columns =>
                    {
                        columns.RelativeColumn();
                        columns.RelativeColumn();
                        columns.RelativeColumn();
                        columns.RelativeColumn();
                    });

                    // 表头
                    table.Header(header =>
                    {
                        header.Cell().Text("Ürün Kodu").Bold().Padding(5);
                        header.Cell().Text("Ürün Açıklaması").Bold().Padding(5);
                        header.Cell().Text("Üretici Kodu").Bold().Padding(5);
                        header.Cell().Text("Üretici Adı").Bold().Padding(5);
                        header.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2);
                    });

                    // 表体行:每个商品对应一行,正确创建Cell实例
                    foreach (var item in Items)
                    {
                        table.Row(row =>
                        {
                            row.Cell().Text(item.Code).Padding(5);
                            row.Cell().Text(item.Description).Padding(5);
                            row.Cell().Text(item.ManufacturerCode).Padding(5);
                            row.Cell().Text(item.ManufacturerName).Padding(5);
                        });
                    }
                });

                // 总计区域(右对齐优化)
                contentColumn.Item().PaddingVertical(10).AlignRight().Row(row =>
                {
                    row.Spacing(20);
                    row.AutoItem().Text($"Subtotal: {Subtotal:C2}").FontSize(12);
                    row.AutoItem().Text($"VAT: {VAT:C2}").FontSize(12);
                    row.AutoItem().Text($"Total: {Total:C2}").FontSize(14).Bold();
                });

                // 银行信息
                contentColumn.Item().Text("Bank Details:").Bold();
                contentColumn.Item().Text(BankDetails).FontSize(11);

                // 备注信息
                contentColumn.Item().Text("Notes/Terms:").Bold();
                contentColumn.Item().Text(Notes).FontSize(11);
            });
        });
    }
}

额外优化建议

  1. Logo路径处理:Blazor Server中Image.FromFile依赖物理路径,建议使用绝对路径或嵌入资源,避免部署时路径失效。
  2. 样式统一:可将字体、颜色等样式提取到DocumentSettings中,或使用QuestPDF的Style功能统一管理。
  3. 表格可读性优化:给表格添加边框,例如在Table配置中添加table.Cell().Border(1).BorderColor(Colors.Grey.Lighten3);。

内容的提问来源于stack exchange,提问作者raysefo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 11:19:51