C# 打印表单到PDF - 完整开发者指南
并行PDF模板的问题
Razor视图已经构建完成。 发票详情页面呈现项目列表,计算总数,并应用公司的样式表。 项目状态页面显示任务细分,只有在里程碑落后时才会出现条件部分。 工资单视图将收入、扣减和年度累计数据格式化成一个表,HR团队花了两周时间才完成。 所有这些工作都已完成,然后某个利益相关者要求添加一个"下载为PDF"按钮。
标准响应是构建第二个模板:一个HTML字符串或报告定义,以再现PDF路径的相同步骤。 第二个模板最初是第一个模板的复制,然后立即开始分歧。 UI设计师在第14次开发迭代中更新了发票视图的表格样式。直到用户报告下载的发票看起来与屏幕上的不同时,才会更新PDF模板。 现在有两个真相来源,一个总是略微错误。
客户端JavaScript PDF库避免了重复,但丢失了服务器渲染的数据、已认证的数据、服务器端计算的总数以及由视图模型驱动的条件部分在转交给浏览器端渲染器时无法进行。 服务器端无头浏览器自动化不稳定,增加了基础架构开销,在容器化环境中会不可预测地失败。 浏览器打印为PDF适用于打印手动操作的单用户; 它不是生产应用程序中的一个"下载PDF"按钮。
真实场景体现真正成本:电子商务管理者下载订单详情页以便履行订单,客户从项目管理工具中导出项目状态页,员工下载工资单,调度员打印路线摘要。 他们都希望PDF看起来与屏幕上一样。
解决方案:渲染现有视图而不是其副本
IronPDF允许ASP.NET Core应用程序将现有Razor视图——与浏览器使用的相同——直接渲染为PDF。 PDF控制器动作通过标准视图引擎将Razor视图渲染为HTML字符串,将该字符串传递给ChromePdfRenderer.RenderHtmlAsPdf(),然后以文件下载的形式返回结果。
一个视图,两个输出。 当Razor视图更改时,PDF输出会自动随之更改,无需任何协调。 没有要维护的平行模板,没有要调试的客户端解决方法,没有要维持的无头浏览器进程。渲染在现有.NET应用程序中以单个NuGet包运行。
实际应用示例
1. 视图已存在:PDF动作是新添加的
位于/invoices/{id}的发票详情页在为浏览器或生成PDF时呈现相同的数据模型。 模型包括项目、总数、客户详细信息和公司品牌,视图所需的所有数据。 现有的InvoicesController具有Details动作来填充该模型。 PDF动作是它的兄弟而不是替代品。
当用户点击"下载PDF"时,请求将发送到/invoices/{id}/pdf。 PDF动作调用相同的服务来获取相同的ViewModel,该模型是相同的。 不同之处在于接下来发生的事情。
2. 将Razor视图渲染为HTML字符串
而不是返回一个ViewResult,PDF动作使用一个视图渲染服务来对视图文件和ViewModel调用Razor引擎,将输出捕获为一个字符串。 这是ASP.NET Core中的一个常见模式,IViewRenderService注入到控制器中调用ICompositeViewEngine,在一个假动作上下文中执行视图,并返回渲染的HTML。
渲染后的HTML字符串是完整的:所有数据都已填充,所有条件部分都已解决,所有CSS类名都已存在。 这是浏览器将收到相同的HTML,由服务器端捕获。
3. ChromePdfRenderer将HTML字符串转换为PDF格式
using IronPdf;
[HttpGet("{id}/pdf")]
public async Task<IActionResult> DownloadInvoicePdf(int id)
{
var model = await _invoiceService.GetInvoiceViewModelAsync(id);
// Render the existing Razor view to an HTML string
string html = await _viewRenderer.RenderToStringAsync("Invoices/Details", model);
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.CssMediaType = IronPdf.Rendering.PdfCssMediaType.Print;
renderer.RenderingOptions.MarginTop = 15;
renderer.RenderingOptions.MarginBottom = 15;
PdfDocument pdf = renderer.RenderHtmlAsPdf(html);
return File(pdf.BinaryData, "application/pdf"
$"Invoice-{model.InvoiceNumber}.pdf");
}
using IronPdf;
[HttpGet("{id}/pdf")]
public async Task<IActionResult> DownloadInvoicePdf(int id)
{
var model = await _invoiceService.GetInvoiceViewModelAsync(id);
// Render the existing Razor view to an HTML string
string html = await _viewRenderer.RenderToStringAsync("Invoices/Details", model);
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.CssMediaType = IronPdf.Rendering.PdfCssMediaType.Print;
renderer.RenderingOptions.MarginTop = 15;
renderer.RenderingOptions.MarginBottom = 15;
PdfDocument pdf = renderer.RenderHtmlAsPdf(html);
return File(pdf.BinaryData, "application/pdf"
$"Invoice-{model.InvoiceNumber}.pdf");
}
Imports IronPdf
Imports Microsoft.AspNetCore.Mvc
<HttpGet("{id}/pdf")>
Public Async Function DownloadInvoicePdf(id As Integer) As Task(Of IActionResult)
Dim model = Await _invoiceService.GetInvoiceViewModelAsync(id)
' Render the existing Razor view to an HTML string
Dim html As String = Await _viewRenderer.RenderToStringAsync("Invoices/Details", model)
Dim renderer As New ChromePdfRenderer()
renderer.RenderingOptions.CssMediaType = IronPdf.Rendering.PdfCssMediaType.Print
renderer.RenderingOptions.MarginTop = 15
renderer.RenderingOptions.MarginBottom = 15
Dim pdf As PdfDocument = renderer.RenderHtmlAsPdf(html)
Return File(pdf.BinaryData, "application/pdf", $"Invoice-{model.InvoiceNumber}.pdf")
End Function
生成的PDF文档
CssMediaType.Print应用了视图样式表中已有的任何@media print规则:隐藏导航栏,抑制动作按钮,应用特定于打印的间距,而无需对Razor视图本身进行任何更改。
4. 在不触碰视图的情况下微调PDF输出
特定于PDF的调整,如页码、自定义边距、带有文档标题的页眉,在渲染器上配置而不是在Razor视图中配置。 这使得打印逻辑与模板分离:
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.CssMediaType = IronPdf.Rendering.PdfCssMediaType.Print;
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4;
renderer.RenderingOptions.MarginTop = 20;
renderer.RenderingOptions.MarginBottom = 20;
renderer.RenderingOptions.HtmlFooter = new HtmlHeaderFooter
{
HtmlFragment = @"
<div style='font-size:9px; color:#888; text-align:center; width:100%;'>
Invoice — Page {page} of {total-pages}
</div>",
DrawDividerLine = true
};
PdfDocument pdf = renderer.RenderHtmlAsPdf(html);
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.CssMediaType = IronPdf.Rendering.PdfCssMediaType.Print;
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4;
renderer.RenderingOptions.MarginTop = 20;
renderer.RenderingOptions.MarginBottom = 20;
renderer.RenderingOptions.HtmlFooter = new HtmlHeaderFooter
{
HtmlFragment = @"
<div style='font-size:9px; color:#888; text-align:center; width:100%;'>
Invoice — Page {page} of {total-pages}
</div>",
DrawDividerLine = true
};
PdfDocument pdf = renderer.RenderHtmlAsPdf(html);
Imports IronPdf
Dim renderer As New ChromePdfRenderer()
renderer.RenderingOptions.CssMediaType = IronPdf.Rendering.PdfCssMediaType.Print
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4
renderer.RenderingOptions.MarginTop = 20
renderer.RenderingOptions.MarginBottom = 20
renderer.RenderingOptions.HtmlFooter = New HtmlHeaderFooter With {
.HtmlFragment = "
<div style='font-size:9px; color:#888; text-align:center; width:100%;'>
Invoice — Page {page} of {total-pages}
</div>",
.DrawDividerLine = True
}
Dim pdf As PdfDocument = renderer.RenderHtmlAsPdf(html)
输出 PDF 文件
Razor视图不需要知道它是渲染到浏览器还是PDF。 控制器动作拥有PDF特定配置,并且视图保持一个纯显示模板。
实际好处
零模板复制。 Razor视图是文档布局和内容的唯一真理来源。 浏览器和PDF从同一个文件中渲染,没有供维护的第二个模板,无需修正漂移。
即时采用。 如果视图已经存在,PDF导出只需一个控制器动作即可实现。 无须重新设计布局、重建模板或将条件逻辑移植到不同的渲染系统。
像素精确输出。 基于Chromium的渲染意味着CSS网格、弹性框、网页字体和媒体查询都在PDF中工作。 输出与浏览器生成的一致,而不是一个降级版的近似。
特定于打印的样式。 视图样式表中已有的@media print规则控制着PDF中出现的内容:隐藏导航、调整打印纸的列宽,或重新流布局。 没有单独的模板,无需单独管理内联打印样式。
可维护性。 更新Razor视图,浏览器输出和PDF输出都反映该更改。 没有第二个系统要协调更新,没有设计师的更改传到浏览器却没有到PDF的风险。
无每文件成本。 渲染在Web应用程序内部进行。 没有外部API调用,没有使用计量,也没有与下载量成比例的成本模型。
结语
如果Razor视图已经构建完成,PDF导出不是一个新功能,而是现有工作的一个新交付路径。 相同的模型,相同的视图,相同的样式:唯一的新增是捕获视图HTML输出并通过渲染器返回为文件的控制器动作。
这种架构保持代码库干净,PDF输出与浏览器永远同步。 IronPDF在ironpdf.com全方位处理C#中的PDF生成生命周期,从渲染HTML到保存、流化和操作文档。 如果您准备向现有Razor视图添加PDF导出,开始免费30天试用,并在发布功能之前根据当前的浏览器渲染验证输出。




