Generate Reports in C# Like Crystal Reports (.NET 10)
using IronPDF 在 C# .NET 中將 HTML 轉為 PDF 報告,可將 Crystal Reports 的專有 .rpt 設計器替換為標準的 HTML、CSS 和 Razor 範本,讓 .NET 開發人員能運用現有的網頁開發技能,建立資料驅動的商業報告。 這包括對動態表格、JavaScript 驅動的圖表、條件格式化、多文件批次處理,以及在任何運行 .NET 的環境中進行跨平台部署的全面支援。
TL;DR:快速入門指南
本教學將介紹如何在 C# .NET 中以 HTML 轉 PDF 報表生成技術取代 Crystal Reports,內容涵蓋從基礎範本到批次處理及排程生成的完整流程。
- 適用對象:正在替換 Crystal Reports 或從頭開始建置新報表系統的 .NET 開發人員。
- 您將建構的內容:三種完整的報表實作(銷售發票、員工名錄、庫存報表),Plus Chart.js 可視化功能、品牌化頁首/頁尾、目錄生成、子報表合併,以及並行批次處理。
- 支援環境:.NET 10、.NET 8 LTS、.NET Framework 4.6.2 以上版本,以及 .NET Standard 2.0。無需依賴僅限 Windows 使用的 COM 元件。
- 何時適用此方案:當 Crystal Reports 缺乏 .NET Core 支援、受限於 Windows 系統,或授權機制過於複雜而成為瓶頸時。
- 技術層面的重要性:HTML/CSS 在各平台上的呈現效果完全一致,可與 CI/CD 整合,並能執行用於製作圖表的 JavaScript,且無需專屬設計工具或按文件計費。
若要配合程式碼範例操作,請透過 NuGet (Install-Package IronPdf) 安裝 IronPdf。 只需幾行程式碼,即可生成您的第一份報告:
-
using NuGet 套件管理員安裝 https://www.nuget.org/packages/IronPdf
PM > Install-Package IronPdf -
請複製並執行此程式碼片段。
// Install-Package IronPdf var pdf = new IronPdf.ChromePdfRenderer() .RenderHtmlAsPdf("<h1>Sales Report</h1><table><tr><td>Q1</td><td>$50,000</td></tr></table>") .SaveAs("sales-report.pdf"); -
部署至您的生產環境進行測試
立即透過免費試用,在您的專案中開始使用 IronPDF
購買 IronPDF 或註冊 30 天試用版後,請在應用程式啟動時輸入您的授權金鑰。
IronPdf.License.LicenseKey = "KEY";
IronPdf.License.LicenseKey = "KEY";
Imports IronPdf
IronPdf.License.LicenseKey = "KEY"
立即透過免費試用,在您的專案中開始使用 IronPDF。
目錄
- 重點摘要:快速入門指南
- HTML 範本轉 PDF 架構
- 為何要在 .NET 應用程式中取代 Crystal Reports
- 在 .NET 10 中設定 C# 報表產生器
- 使用 C# 建立資料驅動的 PDF 報表
- 運用 IronPDF 進行進階 C# 報表生成
- 從 Crystal Reports 遷移至 IronPDF
- .NET 中的批次報表生成與排程
- 下載完整的測試專案
C# Report Generator: HTML Templates to PDF
HTML 轉 PDF 的生成過程依賴於線性架構的處理流程。該應用程式不採用專有檔案格式,而是使用標準資料模型來填充 Razor 視圖或 HTML 範本。 生成的 HTML 字串隨後會傳遞給 IronPdf 之類的渲染引擎,該引擎會將視覺輸出擷取為 PDF 文件。 此方法將報表設計與執行環境解耦,使完全相同的程式碼能在任何支援 .NET 的平台上執行。
此工作流程與標準的網頁開發流程相符。 前端開發人員可使用 CSS 建立版面配置,並立即在任何瀏覽器中預覽效果。 後端開發人員隨後使用 C# 將資料進行綁定。 這種分離讓團隊能夠像處理應用程式其他部分一樣,對報告使用現有的版本控制、程式碼審查和持續部署流程。
HTML 提供了 Crystal Reports 所不具備的功能:互動式圖表、響應式表格,以及用於維持品牌一致性的共用樣式。
為何要在 .NET 應用程式中取代 Crystal Reports
放棄 Crystal Reports 並非源於單一重大問題,亦非 SAP 突然放棄該產品。而是由於各種摩擦點的累積,使得該平台在新專案中越來越難以被接受,在現有解決方案中的維護也變得更加困難。 釐清這些痛點,有助於說明為何許多團隊正在搜尋替代方案,以及在評估替代選項時哪些標準最為重要。
不支援 .NET 8 或 .NET Core
Crystal Reports 不支援 .NET Core 或 .NET 5-10。SAP 已在論壇上聲明,他們不打算新增此支援。 此 SDK 使用 COM 元件,而這些元件與跨平台的 .NET 不相容。 若要支援現代版 .NET,則需進行全面重寫,而 SAP 已拒絕進行此項工作。
因此,在現行 .NET 版本上開發新應用的團隊將無法使用 Crystal Reports。 已採用 .NET Standard 8 或 .NET Standard 10 作為標準平台的組織無法進行整合。 對於現有應用程式,若要升級至現代化的 .NET 執行環境,必須先更換報表系統。
複雜的授權條款與隱藏成本
Crystal Reports 的授權模式區分為設計者授權、執行時授權、伺服器部署及嵌入式使用。 桌面版、網頁版及終端服務的規則各不相同。 某種設定下的合規性,在另一種設定下可能需要額外的授權。 若在部署後出現漏洞,將導致意外成本。許多組織認為,這種不確定性比遷移至授權條款更明確的解決方案還要糟糕。
僅限 Windows 的平台鎖定
Crystal Reports 僅能在搭載舊版 .NET Framework 的 Windows 系統上運行。 您無法將這些應用程式部署至 Linux 容器、基於 Linux 的 Azure App Service、AWS Lambda 或 Google Cloud Run。 隨著組織採用容器化、平台無關及無伺服器系統,這些限制變得愈發重要。
正在建置微服務的開發團隊面臨額外的挑戰。 若九項服務在輕量級 Linux 容器中運行,但其中一項因需使用 Crystal Reports 而必須在 Windows 環境下運作,部署過程便會更加複雜。 您需要 Windows 容器映像檔、與 Windows 相容的主機服務,以及獨立的部署設定。 報告服務成為例外,阻礙了標準化。
Set Up a C# Report Generator in .NET 10
開始使用 IronPDF 非常簡單。 請透過 NuGet 安裝此函式庫,如同安裝其他 .NET 依賴項一樣。 無需額外下載軟體,亦無需為生產伺服器安裝獨立的執行環境安裝程式。
選擇範本方式:Razor、HTML 或混合式
IronPDF 支援三種不同的報表範本建置方式。 每種方法皆具備特定優勢,具體取決於團隊組成、專案需求以及長期維護考量。
Razor Views 為已在 .NET 生態系統中工作的團隊提供最豐富的開發體驗。 提供在 Visual Studio 和 VS Code 中具備完整 IntelliSense 支援的強類型模型、編譯時檢查,以及 C# 所具備的完整功能,包括迴圈、條件語句、空值處理和字串格式化。 對於曾開發過 .NET Core 應用程式的人來說,Razor 的語法相當熟悉,因此無需像使用其他生態系統的模板引擎那樣經歷學習曲線。 這些範本與其他原始檔一同存放於專案中,會參與重構操作,並作為常規建置流程的一部分進行編譯。
對於較簡單的報表,或偏好將範本與 .NET 程式碼完全分離的團隊而言,採用字串插值功能的純 HTML 格式效果甚佳。 HTML 範本可儲存為編譯至程序集中的內嵌資源、與應用程式一同部署的外部檔案,甚至可在執行時從資料庫或內容管理系統中擷取。基本資料綁定使用 string.Replace() 處理單一值,或採用 Scriban 或 Fluid 等輕量級範本庫來處理更進階的場景。 此方法能最大化可移植性,讓設計師無需安裝任何 .NET 工具即可編輯範本,僅需使用文字編輯器和網頁瀏覽器進行預覽。
混合式方法結合了這兩種技術,適用於需要靈活性的情境。 例如,Razor 檢視頁面可能會被渲染以產生主要 HTML 結構,隨後針對無法完美融入檢視模型的動態元素,進行額外的字串替換後處理。 此外,亦可載入由非開發者設計的 HTML 範本,並利用 Razor 局部檢視(partial views)僅渲染複雜的資料驅動區塊,最後再將所有內容整合。 HTML 轉 PDF 功能不依賴於原始 HTML 格式,讓您能根據每份報告的需求靈活組合各種方法。
在這些選項中,本教學主要聚焦於 Razor 檢視,因為它們在典型的商業報表情境中,能為類型安全性、可維護性與功能豐富度提供最佳的平衡。 若未來需求涉及處理純 HTML 範本,相關技能可直接轉移,因為這兩種方法都會產生 HTML 字串。
Build a Data-Driven PDF Report in C
本節將完整示範銷售發票報表的建立流程,從頭到尾。 此範例涵蓋所有報告通用的基本模式:定義用於結構化資料的模型、建立將資料轉換為格式化 HTML 的 Razor 範本、將該範本渲染為 HTML 字串,並將 HTML 轉換為可供檢視、寄送電子郵件或歸檔的 PDF 文件。
建立 HTML/CSS 報表範本
第一步是定義資料模型。 一份真正的發票必須包含客戶資訊、附有描述與價格的明細項目、計算出的總額、稅務處理,以及公司品牌元素。 範例類別的結構應反映以下分類:
// Invoice data model with customer, company, and line item details
public class InvoiceModel
{
public string InvoiceNumber { get; set; } = string.Empty;
public DateTime InvoiceDate { get; set; }
public DateTime DueDate { get; set; }
public CompanyInfo Company { get; set; } = new();
public CustomerInfo Customer { get; set; } = new();
public List<LineItem> Items { get; set; } = new();
// Computed totals - business logic stays in the model
public decimal Subtotal => Items.Sum(x => x.Total);
public decimal TaxRate { get; set; } = 0.08m;
public decimal TaxAmount => Subtotal * TaxRate;
public decimal GrandTotal => Subtotal + TaxAmount;
}
// Company details for invoice header
public class CompanyInfo
{
public string Name { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
public string Phone { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string LogoPath { get; set; } = string.Empty;
}
// Customer billing information
public class CustomerInfo
{
public string Name { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
// Individual invoice line item
public class LineItem
{
public string Description { get; set; } = string.Empty;
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal Total => Quantity * UnitPrice;
}
// Invoice data model with customer, company, and line item details
public class InvoiceModel
{
public string InvoiceNumber { get; set; } = string.Empty;
public DateTime InvoiceDate { get; set; }
public DateTime DueDate { get; set; }
public CompanyInfo Company { get; set; } = new();
public CustomerInfo Customer { get; set; } = new();
public List<LineItem> Items { get; set; } = new();
// Computed totals - business logic stays in the model
public decimal Subtotal => Items.Sum(x => x.Total);
public decimal TaxRate { get; set; } = 0.08m;
public decimal TaxAmount => Subtotal * TaxRate;
public decimal GrandTotal => Subtotal + TaxAmount;
}
// Company details for invoice header
public class CompanyInfo
{
public string Name { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
public string Phone { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string LogoPath { get; set; } = string.Empty;
}
// Customer billing information
public class CustomerInfo
{
public string Name { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
// Individual invoice line item
public class LineItem
{
public string Description { get; set; } = string.Empty;
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal Total => Quantity * UnitPrice;
}
Imports System
Imports System.Collections.Generic
Imports System.Linq
' Invoice data model with customer, company, and line item details
Public Class InvoiceModel
Public Property InvoiceNumber As String = String.Empty
Public Property InvoiceDate As DateTime
Public Property DueDate As DateTime
Public Property Company As New CompanyInfo()
Public Property Customer As New CustomerInfo()
Public Property Items As New List(Of LineItem)()
' Computed totals - business logic stays in the model
Public ReadOnly Property Subtotal As Decimal
Get
Return Items.Sum(Function(x) x.Total)
End Get
End Property
Public Property TaxRate As Decimal = 0.08D
Public ReadOnly Property TaxAmount As Decimal
Get
Return Subtotal * TaxRate
End Get
End Property
Public ReadOnly Property GrandTotal As Decimal
Get
Return Subtotal + TaxAmount
End Get
End Property
End Class
' Company details for invoice header
Public Class CompanyInfo
Public Property Name As String = String.Empty
Public Property Address As String = String.Empty
Public Property City As String = String.Empty
Public Property Phone As String = String.Empty
Public Property Email As String = String.Empty
Public Property LogoPath As String = String.Empty
End Class
' Customer billing information
Public Class CustomerInfo
Public Property Name As String = String.Empty
Public Property Address As String = String.Empty
Public Property City As String = String.Empty
Public Property Email As String = String.Empty
End Class
' Individual invoice line item
Public Class LineItem
Public Property Description As String = String.Empty
Public Property Quantity As Integer
Public Property UnitPrice As Decimal
Public ReadOnly Property Total As Decimal
Get
Return Quantity * UnitPrice
End Get
End Property
End Class
模型中已包含 TaxAmount 及 GrandTotal 的計算屬性。 這些計算應置於模型中而非範本中,讓 Razor 檢視專注於呈現,而由模型處理業務邏輯。 這種分離使單元測試變得簡單明瞭,讓計算結果能在不渲染任何 HTML 的情況下進行驗證。
現在請建立一個 Razor 檢視,將此模型轉換為格式 Professional 的發票。請將其儲存至 Views 資料夾中,並命名為 InvoiceTemplate.cshtml:
@model InvoiceModel
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
/* Reset and base styles */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Arial, sans-serif; font-size: 12px; color: #333; line-height: 1.5; }
.invoice-container { max-width: 800px; margin: 0 auto; padding: 40px; }
/* Header with company info and invoice title */
.header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 40px; padding-bottom: 20px; border-bottom: 2px solid #3498db; }
.company-info h1 { font-size: 24px; color: #2c3e50; margin-bottom: 10px; }
.company-info p { color: #7f8c8d; font-size: 11px; }
.invoice-title { text-align: right; }
.invoice-title h2 { font-size: 32px; color: #3498db; margin-bottom: 10px; }
.invoice-title p { font-size: 12px; color: #7f8c8d; }
/* Address blocks */
.addresses { display: flex; justify-content: space-between; margin-bottom: 30px; }
.address-block { width: 45%; }
.address-block h3 { font-size: 11px; text-transform: uppercase; color: #95a5a6; margin-bottom: 8px; letter-spacing: 1px; }
.address-block p { font-size: 12px; }
/* Line items table */
.items-table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }
.items-table th { background-color: #3498db; color: white; padding: 12px; text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; }
.items-table th:last-child, .items-table td:last-child { text-align: right; }
.items-table td { padding: 12px; border-bottom: 1px solid #ecf0f1; }
.items-table tr:nth-child(even) { background-color: #f9f9f9; }
/* Totals section */
.totals { float: right; width: 300px; }
.totals-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #ecf0f1; }
.totals-row.grand-total { border-bottom: none; border-top: 2px solid #3498db; font-size: 16px; font-weight: bold; color: #2c3e50; padding-top: 12px; }
/* Footer */
.footer { clear: both; margin-top: 60px; padding-top: 20px; border-top: 1px solid #ecf0f1; text-align: center; color: #95a5a6; font-size: 10px; }
</style>
</head>
<body>
<div class="invoice-container">
<div class="header">
<div class="company-info">
<h1>@Model.Com/pany.Name</h1>
<p>@Model.Com/pany.Address</p>
<p>@Model.Com/pany.City</p>
<p>@Model.Com/pany.Phone | @Model.Com/pany.Email</p>
</div>
<div class="invoice-title">
<h2>INVOICE</h2>
<p>Invoice #: @Model.InvoiceNumber</p>
<p>Date: @Model.InvoiceDate.ToString("MMMM dd, yyyy")</p>
<p>Due Date: @Model.DueDate.ToString("MMMM dd, yyyy")</p>
</div>
</div>
<div class="addresses">
<div class="address-block">
<h3>Bill To</h3>
<p>@Model.Customer.Name</p>
<p>@Model.Customer.Address</p>
<p>@Model.Customer.City</p>
<p>@Model.Customer.Email</p>
</div>
</div>
<table class="items-table">
<thead>
<tr><th>Description</th><th>Quantity</th><th>Unit Price</th><th>Total</th></tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr>
<td>@item.Description</td>
<td>@item.Quantity</td>
<td>@item.UnitPrice.ToString("C")</td>
<td>@item.Total.ToString("C")</td>
</tr>
}
</tbody>
</table>
<div class="totals">
<div class="totals-row"><span>Subtotal:</span><span>@Model.Subtotal.ToString("C")</span></div>
<div class="totals-row"><span>Tax (@(Model.TaxRate * 100)%):</span><span>@Model.TaxAmount.ToString("C")</span></div>
<div class="totals-row grand-total"><span>Total Due:</span><span>@Model.GrandTotal.ToString("C")</span></div>
</div>
<div class="footer">
<p>Thank you for your business!</p>
<p>Payment is due within 30 days. Please include invoice number with your payment.</p>
</div>
</div>
</body>
</html>
@model InvoiceModel
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
/* Reset and base styles */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Arial, sans-serif; font-size: 12px; color: #333; line-height: 1.5; }
.invoice-container { max-width: 800px; margin: 0 auto; padding: 40px; }
/* Header with company info and invoice title */
.header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 40px; padding-bottom: 20px; border-bottom: 2px solid #3498db; }
.company-info h1 { font-size: 24px; color: #2c3e50; margin-bottom: 10px; }
.company-info p { color: #7f8c8d; font-size: 11px; }
.invoice-title { text-align: right; }
.invoice-title h2 { font-size: 32px; color: #3498db; margin-bottom: 10px; }
.invoice-title p { font-size: 12px; color: #7f8c8d; }
/* Address blocks */
.addresses { display: flex; justify-content: space-between; margin-bottom: 30px; }
.address-block { width: 45%; }
.address-block h3 { font-size: 11px; text-transform: uppercase; color: #95a5a6; margin-bottom: 8px; letter-spacing: 1px; }
.address-block p { font-size: 12px; }
/* Line items table */
.items-table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }
.items-table th { background-color: #3498db; color: white; padding: 12px; text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; }
.items-table th:last-child, .items-table td:last-child { text-align: right; }
.items-table td { padding: 12px; border-bottom: 1px solid #ecf0f1; }
.items-table tr:nth-child(even) { background-color: #f9f9f9; }
/* Totals section */
.totals { float: right; width: 300px; }
.totals-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #ecf0f1; }
.totals-row.grand-total { border-bottom: none; border-top: 2px solid #3498db; font-size: 16px; font-weight: bold; color: #2c3e50; padding-top: 12px; }
/* Footer */
.footer { clear: both; margin-top: 60px; padding-top: 20px; border-top: 1px solid #ecf0f1; text-align: center; color: #95a5a6; font-size: 10px; }
</style>
</head>
<body>
<div class="invoice-container">
<div class="header">
<div class="company-info">
<h1>@Model.Com/pany.Name</h1>
<p>@Model.Com/pany.Address</p>
<p>@Model.Com/pany.City</p>
<p>@Model.Com/pany.Phone | @Model.Com/pany.Email</p>
</div>
<div class="invoice-title">
<h2>INVOICE</h2>
<p>Invoice #: @Model.InvoiceNumber</p>
<p>Date: @Model.InvoiceDate.ToString("MMMM dd, yyyy")</p>
<p>Due Date: @Model.DueDate.ToString("MMMM dd, yyyy")</p>
</div>
</div>
<div class="addresses">
<div class="address-block">
<h3>Bill To</h3>
<p>@Model.Customer.Name</p>
<p>@Model.Customer.Address</p>
<p>@Model.Customer.City</p>
<p>@Model.Customer.Email</p>
</div>
</div>
<table class="items-table">
<thead>
<tr><th>Description</th><th>Quantity</th><th>Unit Price</th><th>Total</th></tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr>
<td>@item.Description</td>
<td>@item.Quantity</td>
<td>@item.UnitPrice.ToString("C")</td>
<td>@item.Total.ToString("C")</td>
</tr>
}
</tbody>
</table>
<div class="totals">
<div class="totals-row"><span>Subtotal:</span><span>@Model.Subtotal.ToString("C")</span></div>
<div class="totals-row"><span>Tax (@(Model.TaxRate * 100)%):</span><span>@Model.TaxAmount.ToString("C")</span></div>
<div class="totals-row grand-total"><span>Total Due:</span><span>@Model.GrandTotal.ToString("C")</span></div>
</div>
<div class="footer">
<p>Thank you for your business!</p>
<p>Payment is due within 30 days. Please include invoice number with your payment.</p>
</div>
</div>
</body>
</html>
此範本中嵌入的 CSS 負責處理所有視覺樣式,例如顏色、字型、間距及表格格式。 IronPDF 亦支援 flexbox、網格佈局及 CSS 變數等現代 CSS 功能。 生成的 PDF 與 Chrome 的列印預覽完全一致,這使得除錯過程十分簡便:若 PDF 中出現異常,只需在瀏覽器中開啟 HTML 檔案,並使用開發者工具檢查及調整樣式即可。
將資料綁定至範本
在模型與範本就緒後,渲染 PDF 需透過 IronPDF 的 ChromePdfRenderer 將兩者連結起來。 關鍵步驟是將 Razor 視圖轉換為 HTML 字串,然後將該字串傳遞給渲染器:
using IronPdf;
// Service class for generating invoice PDFs from Razor views
public class InvoiceReportService
{
private readonly IRazorViewEngine _razorViewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly IServiceProvider _serviceProvider;
public InvoiceReportService(
IRazorViewEngine razorViewEngine,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider)
{
_razorViewEngine = razorViewEngine;
_tempDataProvider = tempDataProvider;
_serviceProvider = serviceProvider;
}
// Generate PDF from invoice model
public async Task<byte[]> GenerateInvoicePdfAsync(InvoiceModel invoice)
{
// Render Razor view to HTML string
string html = await RenderViewToStringAsync("InvoiceTemplate", invoice);
// Configure PDF renderer with margins and paper size
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.MarginTop = 10;
renderer.RenderingOptions.MarginBottom = 10;
renderer.RenderingOptions.MarginLeft = 10;
renderer.RenderingOptions.MarginRight = 10;
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.Letter;
// Convert HTML to PDF and return bytes
var pdfDocument = renderer.RenderHtmlAsPdf(html);
return pdfDocument.BinaryData;
}
// Helper method to render a Razor view to string
private async Task<string> RenderViewToStringAsync<TModel>(string viewName, TModel model)
{
var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
using var stringWriter = new StringWriter();
var viewResult = _razorViewEngine.FindView(actionContext, viewName, false);
if (!viewResult.Success)
throw new InvalidOperationException($"View '{viewName}' not found.");
var viewDictionary = new ViewDataDictionary<TModel>(
new EmptyModelMetadataProvider(), new ModelStateDictionary()) { Model = model };
var viewContext = new ViewContext(actionContext, viewResult.View, viewDictionary,
new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
stringWriter, new HtmlHelperOptions());
await viewResult.View.RenderAsync(viewContext);
return stringWriter.ToString();
}
}
using IronPdf;
// Service class for generating invoice PDFs from Razor views
public class InvoiceReportService
{
private readonly IRazorViewEngine _razorViewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly IServiceProvider _serviceProvider;
public InvoiceReportService(
IRazorViewEngine razorViewEngine,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider)
{
_razorViewEngine = razorViewEngine;
_tempDataProvider = tempDataProvider;
_serviceProvider = serviceProvider;
}
// Generate PDF from invoice model
public async Task<byte[]> GenerateInvoicePdfAsync(InvoiceModel invoice)
{
// Render Razor view to HTML string
string html = await RenderViewToStringAsync("InvoiceTemplate", invoice);
// Configure PDF renderer with margins and paper size
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.MarginTop = 10;
renderer.RenderingOptions.MarginBottom = 10;
renderer.RenderingOptions.MarginLeft = 10;
renderer.RenderingOptions.MarginRight = 10;
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.Letter;
// Convert HTML to PDF and return bytes
var pdfDocument = renderer.RenderHtmlAsPdf(html);
return pdfDocument.BinaryData;
}
// Helper method to render a Razor view to string
private async Task<string> RenderViewToStringAsync<TModel>(string viewName, TModel model)
{
var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
using var stringWriter = new StringWriter();
var viewResult = _razorViewEngine.FindView(actionContext, viewName, false);
if (!viewResult.Success)
throw new InvalidOperationException($"View '{viewName}' not found.");
var viewDictionary = new ViewDataDictionary<TModel>(
new EmptyModelMetadataProvider(), new ModelStateDictionary()) { Model = model };
var viewContext = new ViewContext(actionContext, viewResult.View, viewDictionary,
new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
stringWriter, new HtmlHelperOptions());
await viewResult.View.RenderAsync(viewContext);
return stringWriter.ToString();
}
}
Imports IronPdf
Imports Microsoft.AspNetCore.Mvc.Razor
Imports Microsoft.AspNetCore.Mvc.ViewFeatures
Imports Microsoft.Extensions.DependencyInjection
Imports Microsoft.AspNetCore.Http
Imports Microsoft.AspNetCore.Mvc
Imports Microsoft.AspNetCore.Routing
Imports System.IO
Imports System.Threading.Tasks
' Service class for generating invoice PDFs from Razor views
Public Class InvoiceReportService
Private ReadOnly _razorViewEngine As IRazorViewEngine
Private ReadOnly _tempDataProvider As ITempDataProvider
Private ReadOnly _serviceProvider As IServiceProvider
Public Sub New(razorViewEngine As IRazorViewEngine, tempDataProvider As ITempDataProvider, serviceProvider As IServiceProvider)
_razorViewEngine = razorViewEngine
_tempDataProvider = tempDataProvider
_serviceProvider = serviceProvider
End Sub
' Generate PDF from invoice model
Public Async Function GenerateInvoicePdfAsync(invoice As InvoiceModel) As Task(Of Byte())
' Render Razor view to HTML string
Dim html As String = Await RenderViewToStringAsync("InvoiceTemplate", invoice)
' Configure PDF renderer with margins and paper size
Dim renderer As New ChromePdfRenderer()
renderer.RenderingOptions.MarginTop = 10
renderer.RenderingOptions.MarginBottom = 10
renderer.RenderingOptions.MarginLeft = 10
renderer.RenderingOptions.MarginRight = 10
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.Letter
' Convert HTML to PDF and return bytes
Dim pdfDocument = renderer.RenderHtmlAsPdf(html)
Return pdfDocument.BinaryData
End Function
' Helper method to render a Razor view to string
Private Async Function RenderViewToStringAsync(Of TModel)(viewName As String, model As TModel) As Task(Of String)
Dim httpContext As New DefaultHttpContext With {.RequestServices = _serviceProvider}
Dim actionContext As New ActionContext(httpContext, New RouteData(), New ActionDescriptor())
Using stringWriter As New StringWriter()
Dim viewResult = _razorViewEngine.FindView(actionContext, viewName, False)
If Not viewResult.Success Then
Throw New InvalidOperationException($"View '{viewName}' not found.")
End If
Dim viewDictionary As New ViewDataDictionary(Of TModel)(
New EmptyModelMetadataProvider(), New ModelStateDictionary()) With {.Model = model}
Dim viewContext As New ViewContext(actionContext, viewResult.View, viewDictionary,
New TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
stringWriter, New HtmlHelperOptions())
Await viewResult.View.RenderAsync(viewContext)
Return stringWriter.ToString()
End Using
End Function
End Class
在較簡單的場景中,例如控制台應用程式或背景服務,若無需完整的 ASP.NET Core MVC 設定,您只需使用帶有插值的 HTML 字串,並以 StringBuilder 表示動態部分。
翻譯範例
添加頁首、頁尾和頁碼
Professional報告通常會在所有頁面中使用一致的頁首與頁尾,顯示公司品牌標識、文件標題、生成日期及頁碼。 IronPDF 提供兩種實現方式:針對僅需基本格式設定的簡單內容,可使用文字型標頭;若需透過標誌與自訂版面配置來全面控制樣式,則可使用 HTML 標頭。
基於文字的標題適用於基本資訊,且由於無需額外解析 HTML,因此渲染速度較快:
:path=/static-assets/pdf/content-code-examples/tutorials/crystal-reports-alternative-csharp/text-headers-footers.cs
using IronPdf;
using IronSoftware.Drawing;
// Configure text-based headers and footers
var renderer = new ChromePdfRenderer();
// Set starting page number
renderer.RenderingOptions.FirstPageNumber = 1;
// Add centered header with divider line
renderer.RenderingOptions.TextHeader = new TextHeaderFooter
{
CenterText = "CONFIDENTIAL - Internal Use Only",
DrawDividerLine = true,
Font = FontTypes.Arial,
FontSize = 10
};
// Add footer with date on left, page numbers on right
renderer.RenderingOptions.TextFooter = new TextHeaderFooter
{
LeftText = "{date} {time}",
RightText = "Page {page} of {total-pages}",
DrawDividerLine = true,
Font = FontTypes.Arial,
FontSize = 9
};
// Set margins to accommodate header/footer
renderer.RenderingOptions.MarginTop = 25;
renderer.RenderingOptions.MarginBottom = 20;
Imports IronPdf
Imports IronSoftware.Drawing
' Configure text-based headers and footers
Dim renderer As New ChromePdfRenderer()
' Set starting page number
renderer.RenderingOptions.FirstPageNumber = 1
' Add centered header with divider line
renderer.RenderingOptions.TextHeader = New TextHeaderFooter With {
.CenterText = "CONFIDENTIAL - Internal Use Only",
.DrawDividerLine = True,
.Font = FontTypes.Arial,
.FontSize = 10
}
' Add footer with date on left, page numbers on right
renderer.RenderingOptions.TextFooter = New TextHeaderFooter With {
.LeftText = "{date} {time}",
.RightText = "Page {page} of {total-pages}",
.DrawDividerLine = True,
.Font = FontTypes.Arial,
.FontSize = 9
}
' Set margins to accommodate header/footer
renderer.RenderingOptions.MarginTop = 25
renderer.RenderingOptions.MarginBottom = 20
可用的合併欄位包括:{page} 代表當前頁碼、{total-pages} 代表文件總頁數、{date} 和 {time} 代表生成時間戳記,以及 {url} 代表來源網址(若從網頁渲染),以及 {html-title} 和 {pdf-title} 代表文件標題。 這些佔位符會在渲染過程中自動替換。
對於包含標誌、自訂字型或複雜多欄佈局的標題,請使用支援完整 CSS 樣式的 HTML 標題:
:path=/static-assets/pdf/content-code-examples/tutorials/crystal-reports-alternative-csharp/html-headers-footers.cs
using IronPdf;
using System;
var renderer = new ChromePdfRenderer();
// Configure HTML header with logo and custom layout
renderer.RenderingOptions.HtmlHeader = new HtmlHeaderFooter
{
MaxHeight = 30,
HtmlFragment = @"
<div style='display: flex; justify-content: space-between; align-items: center;
width: 100%; font-family: Arial; font-size: 10px; color: #666;'>
<img src='logo.png' style='height: 25px;'>
<span>Company Name Inc.</span>
<span>Invoice Report</span>
</div>",
BaseUrl = new Uri(@"C:\assets\images\").AbsoluteUri
};
// Configure HTML footer with page info and generation date
renderer.RenderingOptions.HtmlFooter = new HtmlHeaderFooter
{
MaxHeight = 20,
HtmlFragment = @"
<div style='text-align: center; font-size: 9px; color: #999;
border-top: 1px solid #ddd; padding-top: 5px;'>
Page {page} of {total-pages} | Generated on {date}
</div>",
DrawDividerLine = false
};
Imports IronPdf
Imports System
Dim renderer As New ChromePdfRenderer()
' Configure HTML header with logo and custom layout
renderer.RenderingOptions.HtmlHeader = New HtmlHeaderFooter With {
.MaxHeight = 30,
.HtmlFragment = "
<div style='display: flex; justify-content: space-between; align-items: center;
width: 100%; font-family: Arial; font-size: 10px; color: #666;'>
<img src='logo.png' style='height: 25px;'>
<span>Company Name Inc.</span>
<span>Invoice Report</span>
</div>",
.BaseUrl = New Uri("C:\assets\images\").AbsoluteUri
}
' Configure HTML footer with page info and generation date
renderer.RenderingOptions.HtmlFooter = New HtmlHeaderFooter With {
.MaxHeight = 20,
.HtmlFragment = "
<div style='text-align: center; font-size: 9px; color: #999;
border-top: 1px solid #ddd; padding-top: 5px;'>
Page {page} of {total-pages} | Generated on {date}
</div>",
.DrawDividerLine = False
}
翻譯範例
建立動態表格與重複區段
報告通常需要顯示橫跨多頁的資料集合。 Razor 的迴圈結構能自然地處理此需求,透過遍歷集合,並針對每個項目產生表格列或卡片元素。
以下是一個完整的"員工名錄"範例,展示如何透過部門分區呈現分組資料:
// Employee directory data models
public class EmployeeDirectoryModel
{
public List<Department> Departments { get; set; } = new();
public DateTime GeneratedDate { get; set; } = DateTime.Now;
}
// Department grouping with manager info
public class Department
{
public string Name { get; set; } = string.Empty;
public string ManagerName { get; set; } = string.Empty;
public List<Employee> Employees { get; set; } = new();
}
// Individual employee details
public class Employee
{
public string Name { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Phone { get; set; } = string.Empty;
public string PhotoUrl { get; set; } = string.Empty;
public DateTime HireDate { get; set; }
}
// Employee directory data models
public class EmployeeDirectoryModel
{
public List<Department> Departments { get; set; } = new();
public DateTime GeneratedDate { get; set; } = DateTime.Now;
}
// Department grouping with manager info
public class Department
{
public string Name { get; set; } = string.Empty;
public string ManagerName { get; set; } = string.Empty;
public List<Employee> Employees { get; set; } = new();
}
// Individual employee details
public class Employee
{
public string Name { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Phone { get; set; } = string.Empty;
public string PhotoUrl { get; set; } = string.Empty;
public DateTime HireDate { get; set; }
}
' Employee directory data models
Public Class EmployeeDirectoryModel
Public Property Departments As List(Of Department) = New List(Of Department)()
Public Property GeneratedDate As DateTime = DateTime.Now
End Class
' Department grouping with manager info
Public Class Department
Public Property Name As String = String.Empty
Public Property ManagerName As String = String.Empty
Public Property Employees As List(Of Employee) = New List(Of Employee)()
End Class
' Individual employee details
Public Class Employee
Public Property Name As String = String.Empty
Public Property Title As String = String.Empty
Public Property Email As String = String.Empty
Public Property Phone As String = String.Empty
Public Property PhotoUrl As String = String.Empty
Public Property HireDate As DateTime
End Class
部門類別中的 CSS 屬性 page-break-inside: avoid 會指示 PDF 渲染器,在可能的情況下將部門區塊集中於同一頁面上。 若某個區段的內容會導致頁面在區段中途換行,渲染器會將整個區段移至下一頁。 使用 .department:not(:first-child) 與 page-break-before: always 的選取器,會強制將第一個部門之後的每個部門都置於新頁面上,從而使整個目錄中各區段的分隔清晰明確。
翻譯範例
Advanced C# Report Generation With IronPDF
商業報告通常需要超越靜態表格與文字的功能。 圖表能將趨勢視覺化呈現,若僅以表格形式呈現則較難理解。 條件格式化可讓需要採取行動的項目更引人注目。 子報告將來自多個來源的資料整合成連貫的文件。 本節將說明如何使用 IronPDF 的 Chromium 渲染引擎來實作這些功能。
在 PDF 報告中加入圖表與圖形
由於 JavaScript 會在渲染過程中執行,因此您可以使用任何前端圖表庫,直接在報表中生成視覺化圖表。 圖表會作為頁面的一部分進行光柵化處理,並在最終的 PDF 檔案中呈現與螢幕上完全一致的樣貌。 Chart.js 在簡易性、功能與文件說明之間取得了絕佳的平衡,能滿足大多數報表製作需求。
從 CDN 載入 Chart.js,並使用從 C# 模型序列化的資料來設定您的圖表:
@model SalesReportModel
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<canvas id="salesChart"></canvas>
<script>
// Initialize bar chart with data from C# model
const ctx = document.getElementById('salesChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
// Serialize model data to JavaScript arrays
labels: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.MonthLabels)),
datasets: [{
label: 'Monthly Sales',
data: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.MonthlySales)),
backgroundColor: 'rgba(52, 152, 219, 0.7)'
}]
}
});
</script>
@model SalesReportModel
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<canvas id="salesChart"></canvas>
<script>
// Initialize bar chart with data from C# model
const ctx = document.getElementById('salesChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
// Serialize model data to JavaScript arrays
labels: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.MonthLabels)),
datasets: [{
label: 'Monthly Sales',
data: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.MonthlySales)),
backgroundColor: 'rgba(52, 152, 219, 0.7)'
}]
}
});
</script>
在渲染包含 JavaScript 生成內容的頁面時,請將渲染器設定為在擷取頁面之前,先等待腳本執行完畢:
:path=/static-assets/pdf/content-code-examples/tutorials/crystal-reports-alternative-csharp/javascript-wait-rendering.cs
using IronPdf;
string html = "<h1>Report</h1>";
// Configure renderer to wait for JavaScript execution
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.WaitFor.JavaScript(500); // Wait 500ms for JS to complete
var pdf = renderer.RenderHtmlAsPdf(html);
Imports IronPdf
Dim html As String = "<h1>Report</h1>"
' Configure renderer to wait for JavaScript execution
Dim renderer As New ChromePdfRenderer()
renderer.RenderingOptions.WaitFor.JavaScript(500) ' Wait 500ms for JS to complete
Dim pdf = renderer.RenderHtmlAsPdf(html)
翻譯範例
套用條件格式設定與商業邏輯
庫存報告若能搭配視覺化指標,可立即引導使用者關注需要採取行動的項目。 與其強迫使用者翻閱數百行資料尋找問題,條件格式化能讓例外情況在視覺上一目了然。 使用 Razor 的內嵌式運算式,根據資料值套用 CSS 類別:
@foreach (var item in Model.Items.OrderBy(x => x.Quantity))
{
// Apply CSS class based on stock level thresholds
var rowClass = item.Quantity <= Model.CriticalStockThreshold ? "stock-critical" :
item.Quantity <= Model.LowStockThreshold ? "stock-low" : "";
<tr class="@rowClass">
<td>@item.SKU</td>
<td>@item.ProductName</td>
<td class="text-right">
<span class="quantity-badge @(item.Quantity <= 5 ? "badge-critical" : "badge-ok")">
@item.Quantity
</span>
</td>
</tr>
}
@foreach (var item in Model.Items.OrderBy(x => x.Quantity))
{
// Apply CSS class based on stock level thresholds
var rowClass = item.Quantity <= Model.CriticalStockThreshold ? "stock-critical" :
item.Quantity <= Model.LowStockThreshold ? "stock-low" : "";
<tr class="@rowClass">
<td>@item.SKU</td>
<td>@item.ProductName</td>
<td class="text-right">
<span class="quantity-badge @(item.Quantity <= 5 ? "badge-critical" : "badge-ok")">
@item.Quantity
</span>
</td>
</tr>
}
翻譯範例
建立子報表與分節符
若要將獨立產生的報告合併為單一文件,請使用 IronPDF 的合併功能:
using IronPdf;
// Combine multiple reports into a single PDF document
public byte[] GenerateCombinedReport(SalesReportModel sales, InventoryReportModel inventory)
{
var renderer = new ChromePdfRenderer();
// Render each report section separately
var salesPdf = renderer.RenderHtmlAsPdf(RenderSalesReport(sales));
var inventoryPdf = renderer.RenderHtmlAsPdf(RenderInventoryReport(inventory));
// Merge PDFs into one document
var combined = PdfDocument.Merge(salesPdf, inventoryPdf);
return combined.BinaryData;
}
using IronPdf;
// Combine multiple reports into a single PDF document
public byte[] GenerateCombinedReport(SalesReportModel sales, InventoryReportModel inventory)
{
var renderer = new ChromePdfRenderer();
// Render each report section separately
var salesPdf = renderer.RenderHtmlAsPdf(RenderSalesReport(sales));
var inventoryPdf = renderer.RenderHtmlAsPdf(RenderInventoryReport(inventory));
// Merge PDFs into one document
var combined = PdfDocument.Merge(salesPdf, inventoryPdf);
return combined.BinaryData;
}
Imports IronPdf
' Combine multiple reports into a single PDF document
Public Function GenerateCombinedReport(sales As SalesReportModel, inventory As InventoryReportModel) As Byte()
Dim renderer As New ChromePdfRenderer()
' Render each report section separately
Dim salesPdf = renderer.RenderHtmlAsPdf(RenderSalesReport(sales))
Dim inventoryPdf = renderer.RenderHtmlAsPdf(RenderInventoryReport(inventory))
' Merge PDFs into one document
Dim combined = PdfDocument.Merge(salesPdf, inventoryPdf)
Return combined.BinaryData
End Function
翻譯範例
產生目錄
IronPDF 可根據 HTML 中的標題元素自動生成目錄:
:path=/static-assets/pdf/content-code-examples/tutorials/crystal-reports-alternative-csharp/table-of-contents.cs
using IronPdf;
// Generate PDF with automatic table of contents
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.TableOfContents = TableOfContentsTypes.WithPageNumbers;
var pdf = renderer.RenderHtmlFileAsPdf("report.html");
Imports IronPdf
' Generate PDF with automatic table of contents
Dim renderer As New ChromePdfRenderer()
renderer.RenderingOptions.TableOfContents = TableOfContentsTypes.WithPageNumbers
Dim pdf = renderer.RenderHtmlFileAsPdf("report.html")
從 Crystal Reports 遷移至 IronPDF
遷移現有的報表系統需要仔細規劃,以盡量減少中斷,同時把握機會進行現代化與簡化。 若能理解 Crystal Reports 的概念如何對應至基於 HTML 的方法,您將能更有效率地進行開發,而非試圖逐字複製每個功能或保留原始報表的每項特殊設定。
將 Crystal Reports 概念對應至 IronPDF
理解概念對應關係有助於您系統化地翻譯現有報告:
| Crystal Reports | IronPDF 對應版本 |
|---|---|
| 報告章節 | 具有 CSS page-break 屬性的 HTML div 元素 |
| 參數欄位 | 傳遞至 Razor 檢視的模型屬性 |
| 公式欄位 | 模型類別中的 C# 計算屬性 |
| 累計總數 | LINQ 聚合 |
| 子報告 | 部分檢視或合併的 PDF 文件 |
| 分組/排序 | 在將資料傳遞至範本之前執行 LINQ 運算 |
| 交叉表報表 | 使用嵌套迴圈的 HTML 表格 |
| 條件格式化 | 帶有 CSS 類別的 Razor @if 區塊 |
轉換 .rpt 範本的最佳策略
請勿嘗試透過程式碼解析 .rpt 檔案。 相反地,應將現有的 PDF 輸出視為視覺規格,並透過一套系統化的四步驟策略來重建邏輯:
-
清點:彙整所有 .rpt 檔案,並記錄其用途、資料來源及使用頻率。 移除過時的報告以縮小遷移範圍。
-
優先順序:優先遷移高頻率使用的報表。鎖定版面配置簡單或長期存在維護問題的報表。
-
參考:將現有的 Crystal Reports 匯出為 PDF 檔案。 請將這些內容作為開發人員應遵循的視覺規範。
- 驗證:使用實際生產環境的資料量進行測試。 原本能即時渲染 10 行資料的範本,在處理 10,000 行時可能會變慢。
.NET 中的批次報表生成與排程
生產系統通常需要同時生成大量報告,或依照排程執行報告工作。 IronPDF 的線程安全設計能高效支援這兩種情境。
並行產生多份報告
針對批次處理,請使用 Parallel.ForEachAsync 或搭配 Task.WhenAll 的非同步模式:
using IronPdf;
using System.Co/llections.Concurrent;
// Generate multiple invoices in parallel using async processing
public async Task<List<ReportResult>> GenerateInvoiceBatchAsync(List<InvoiceModel> invoices)
{
var results = new ConcurrentBag<ReportResult>();
// Process invoices concurrently with controlled parallelism
await Parallel.ForEachAsync(invoices,
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
async (invoice, token) =>
{
// Each thread gets its own renderer instance
var renderer = new ChromePdfRenderer();
string html = BuildInvoiceHtml(invoice);
var pdf = await renderer.RenderHtmlAsPdfAsync(html);
// Save individual invoice PDF
string filename = $"Invoice_{invoice.InvoiceNumber}.pdf";
await pdf.SaveAsAsync(filename);
results.Add(new ReportResult { InvoiceNumber = invoice.InvoiceNumber, Success = true });
});
return results.ToList();
}
using IronPdf;
using System.Co/llections.Concurrent;
// Generate multiple invoices in parallel using async processing
public async Task<List<ReportResult>> GenerateInvoiceBatchAsync(List<InvoiceModel> invoices)
{
var results = new ConcurrentBag<ReportResult>();
// Process invoices concurrently with controlled parallelism
await Parallel.ForEachAsync(invoices,
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
async (invoice, token) =>
{
// Each thread gets its own renderer instance
var renderer = new ChromePdfRenderer();
string html = BuildInvoiceHtml(invoice);
var pdf = await renderer.RenderHtmlAsPdfAsync(html);
// Save individual invoice PDF
string filename = $"Invoice_{invoice.InvoiceNumber}.pdf";
await pdf.SaveAsAsync(filename);
results.Add(new ReportResult { InvoiceNumber = invoice.InvoiceNumber, Success = true });
});
return results.ToList();
}
Imports IronPdf
Imports System.Collections.Concurrent
Imports System.Threading.Tasks
' Generate multiple invoices in parallel using async processing
Public Async Function GenerateInvoiceBatchAsync(invoices As List(Of InvoiceModel)) As Task(Of List(Of ReportResult))
Dim results As New ConcurrentBag(Of ReportResult)()
' Process invoices concurrently with controlled parallelism
Await Task.Run(Async Function()
Await Parallel.ForEachAsync(invoices,
New ParallelOptions With {.MaxDegreeOfParallelism = Environment.ProcessorCount},
Async Function(invoice, token)
' Each thread gets its own renderer instance
Dim renderer As New ChromePdfRenderer()
Dim html As String = BuildInvoiceHtml(invoice)
Dim pdf = Await renderer.RenderHtmlAsPdfAsync(html)
' Save individual invoice PDF
Dim filename As String = $"Invoice_{invoice.InvoiceNumber}.pdf"
Await pdf.SaveAsAsync(filename)
results.Add(New ReportResult With {.InvoiceNumber = invoice.InvoiceNumber, .Success = True})
End Function)
End Function)
Return results.ToList()
End Function
翻譯範例
此批次處理範例可並行產生多張發票。 以下是其中一張生成的批次發票:
將報表生成功能與 ASP.NET Core 背景服務整合
排程報告生成功能自然地融入了 ASP.NET Core 的託管服務架構中:
// Background service for scheduled report generation
public class DailyReportService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Calculate next run time (6 AM daily)
var nextRun = DateTime.Now.Date.AddDays(1).AddHours(6);
await Task.Delay(nextRun - DateTime.Now, stoppingToken);
// Create scoped service for report generation
using var scope = _serviceProvider.CreateScope();
var reportService = scope.ServiceProvider.GetRequiredService<IReportGenerationService>();
// Generate and distribute daily report
var salesReport = await reportService.GenerateDailySalesSummaryAsync();
// Email or save reports as needed
}
}
}
// Background service for scheduled report generation
public class DailyReportService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Calculate next run time (6 AM daily)
var nextRun = DateTime.Now.Date.AddDays(1).AddHours(6);
await Task.Delay(nextRun - DateTime.Now, stoppingToken);
// Create scoped service for report generation
using var scope = _serviceProvider.CreateScope();
var reportService = scope.ServiceProvider.GetRequiredService<IReportGenerationService>();
// Generate and distribute daily report
var salesReport = await reportService.GenerateDailySalesSummaryAsync();
// Email or save reports as needed
}
}
}
Imports System
Imports System.Threading
Imports System.Threading.Tasks
Imports Microsoft.Extensions.DependencyInjection
' Background service for scheduled report generation
Public Class DailyReportService
Inherits BackgroundService
Private ReadOnly _serviceProvider As IServiceProvider
Protected Overrides Async Function ExecuteAsync(stoppingToken As CancellationToken) As Task
While Not stoppingToken.IsCancellationRequested
' Calculate next run time (6 AM daily)
Dim nextRun = DateTime.Now.Date.AddDays(1).AddHours(6)
Await Task.Delay(nextRun - DateTime.Now, stoppingToken)
' Create scoped service for report generation
Using scope = _serviceProvider.CreateScope()
Dim reportService = scope.ServiceProvider.GetRequiredService(Of IReportGenerationService)()
' Generate and distribute daily report
Dim salesReport = Await reportService.GenerateDailySalesSummaryAsync()
' Email or save reports as needed
End Using
End While
End Function
End Class
下載完整的測試專案
本教學中的所有程式碼範例,皆收錄於一個可直接執行的 .NET 10 測試專案中。 下載內容包含完整的原始碼、資料模型、HTML 範本,以及一個可產生上述所有範例 PDF 的測試執行程式。
後續步驟
本指南中的範例均顯示,IronPDF 能滿足各類商業報表需求:包含明細項目與總額的簡易發票、具備分組資料與照片的複雜員工名錄、採用條件格式與圖表的庫存報表、數百份文件的並行批次處理,以及透過背景服務進行的排程生成。
若您正在評估現有 Crystal Reports 部署的替代方案,建議從一份高價值的報表開始著手。 請參照此處展示的 HTML 轉 PDF 範例進行重建,比較開發體驗與輸出品質,並以此為基礎進一步擴展。 許多團隊發現,首次轉換報告時,由於需要建立模式和基礎範本,通常需耗時數小時;但隨後報告只需數分鐘即可完成,因為他們會重複使用 Razor 範本和樣式。 為確保版面精準度,像素級精準渲染指南詳述了如何讓 Crystal Reports 輸出與 CSS 完全對應。
準備開始建置了嗎? 下載 IronPDF 並透過免費試用版體驗其功能。 同一個函式庫可處理從單一報表渲染到跨 .NET 環境的大量批次生成等所有需求。 若您對遷移報告有任何疑問,或需要架構方面的指導,請聯繫我們的工程支援團隊。
常見問題
什麼是 IronPDF?
IronPDF 是一款 C# 函式庫,讓開發人員能夠透過程式化方式建立、編輯及產生 PDF 文件,為 Crystal Reports 等傳統報表工具提供了一種現代化的替代方案。
IronPDF 如何作為 Crystal Reports 的替代方案?
IronPDF 提供了一種靈活且現代的報表生成方式,允許開發人員使用 HTML/CSS 範本,這些範本可輕鬆進行樣式設定與修改,這與 Crystal Reports 較為僵化的結構形成鮮明對比。
我可以使用 IronPDF 來建立發票嗎?
是的,您可以使用 IronPDF 的 HTML/CSS 範來建立詳細且客製化的發票,輕鬆設計出 Professional 外觀的文件。
是否可以使用 IronPDF 生成員工名錄?
沒問題。IronPDF 讓您能透過動態資料與 HTML/CSS,生成內容詳盡的員工名錄,呈現清晰且條理分明的介面。
IronPDF 如何協助處理庫存報告?
IronPDF 可透過 HTML/CSS 範本簡化庫存報表的建立流程,這些範本能動態填入資料,以提供最新且視覺上吸引人的報表。
在 IronPDF 中使用 HTML/CSS 範本有哪些優勢?
在 IronPDF 中使用 HTML/CSS 範本,能提供設計上的靈活性、簡化更新流程,並與網頁技術相容,從而更輕鬆地維護與強化報表版面配置。
IronPDF 是否支援 .NET 10?
是的,IronPDF 相容於 .NET 10,確保開發人員能利用最新的 .NET 功能與改進,滿足其報表生成需求。
IronPDF 如何提升報表生成速度?
IronPDF 經過效能優化,能透過高效處理 HTML/CSS 並將其渲染為高品質的 PDF 文件,從而快速生成報告。

