How To Create Or Generate PDF Reports Or Files Using DinkToPDF in .NET 5 or .NET Core Console Application
Good day!
In this tutorial, I will demonstrate on how to create or generate PDF reports or files using DinkToPDF in .NET 5 or .NET Core Console Application. This library is a C# .NET Core wrapper for wkhtmltopdf library that uses the Webkit engine to convert HTML files or page to PDF and was developed by 'rdvojmoc Rok Dvojmoč' from Slovenia. To begin with lets start by establishing the project structure.
Project Structure
1. Create a .NET 5 or .NET Core Console Application Project.
2. Add DinkToPDF and Unity packages using NuGet.
3. Copy the libwkhtmltox (dll, dylib and so) files from DinkToPDF and attach those files to your project. Make sure to set the Copy to Output Directory attribute of those files to Copy Always. Note that there are two versions for these files,32 and 64 bit.
4. Create three folders to the project and in each folder add the empty classes or interface shown in step #6.
5. Add an empty html file called ReportTemplate inside the project and set it's Copy to Output Directory attribute to Copy Always.
6. Once completed with the project structure, it may resemble like the screenshot below.
Model
Define a model for the project that tackles an employee and it's dependents.
namespace DinkToPDFCore.Model { public class EmployeeViewModel { public Employee Employee { get; set; } public List<Dependent> DependentsList { get; set; } public EmployeeViewModel() { Employee = new Employee(); DependentsList = new List<Dependent>(); } } public class Employee { public int EmpID { get; set; } public string EmpName { get; set; } public string EmpStatus { get; set; } public Employee() { EmpID = 0; EmpName = string.Empty; EmpStatus = string.Empty; } } public class Dependent { public int DepID { get; set; } public string DepName { get; set; } public int DepAge { get; set; } public Dependent() { DepID = 0; DepAge = 0; DepName = string.Empty; } } }
ReportTemplate.html
The HTML file is a template that we need to pass to DinkToPDF library that will be converted to PDF file. As you can see in the template, I added tokens that are enclosed in square brackets. These tokens will be replaced with C# values from the model.
<!DOCTYPE html> <html> <head> <title>Employee Information</title> <style type="text/css"> .tblEmployeeHeader { width: 100%; border-collapse: collapse; margin-bottom: 30px; font-family: "Arial Narrow", Times, serif; font-size: 9px; } .tblEmployeeDependentsDetails { width: 100%; border-collapse: collapse; margin-top: 10px; font-family: "Arial Narrow", Times, serif; font-size: 9px; margin-top: 30px; border: 1px solid black; } .tblEmployeeDependentsDetails td { border: 1px solid black; } .tblEmployeeDependentsDetails th { border: 1px solid black; } .divEmployeeHeader { font-size: 25px; font-weight: bold; text-align: center !important; } .tdEmployeeLeft { width: 20%; } </style> </head> <body> <div id="divEmployeeHeader"> <table class='tblEmployeeHeader'> <tr> <td colspan=2> <div class='divEmployeeHeader'>Employee Information</div> </td> </tr> <tr> <td colspan=2> <div> </div> </td> </tr> <tr> <td class="tdEmployeeLeft"><strong>Employee ID:</strong></td> <td>[EmpID]</td> </tr> <tr> <td class="tdEmployeeLeft"><strong>Employee Name:</strong></td> <td>[EmpName]</td> </tr> <tr> <td class="tdEmployeeLeft"><strong>Employee Status:</strong></td> <td>[EmpStatus]</td> </tr> </table> </div> <hr/> <table class='tblEmployeeDependentsDetails'> <tr> <th>ID</th> <th>Name</th> <th>Age</th> </tr> <tr> <td>[DepID1]</td> <td>[DepName1]</td> <td>[DepAge1]</td> </tr> <tr> <td>[DepID2]</td> <td>[DepName2]</td> <td>[DepAge2]</td> </tr> </table> </body> </html>
Service Folder
1. IDocumentService.cs - This interface contains the contract for generating a PDF report. It only has one function definition that returns a byte array.
public interface IDocumentService { Task<byte[]> GeneratePdfReport(string template); }
2. DocumentService.cs - This class implements the method to generate reports defined in IDocumentService.cs. This method creates an HtmlToPdfDocument object that initialized two of it's important properties GlobalSettings and Objects. The GlobalSettings value came from the GlobalSettings() function that returns a GlobalSettings object which include the report's Margins, PaperSize, Orientation and etc. The Objects property value came from the GetObjectSettings() method. This method returns an ObjectSettings object with report properties such as PagesCount, WebSettings, HtmlContent (HTML template), HeaderSettings, FooterSettings and much more.
public class DocumentService : IDocumentService { private GlobalSettings globalSettings; private ObjectSettings objectSettings; private WebSettings webSettings; private HeaderSettings headerSettings; private FooterSettings footerSettings; private readonly IConverter _converter; public DocumentService(IConverter converter) { objectSettings = new ObjectSettings(); webSettings = new WebSettings(); headerSettings = new HeaderSettings(); footerSettings = new FooterSettings(); globalSettings = new GlobalSettings(); _converter = converter; } public async Task<byte[]> GeneratePdfReport(string template) { byte[] result; HtmlToPdfDocument htmlToPdfDocument; htmlToPdfDocument = new HtmlToPdfDocument() { GlobalSettings = GetGlobalSettings(), Objects = { GetObjectSettings(template) } }; result = await Task.FromResult(_converter.Convert(htmlToPdfDocument)); return result; } private GlobalSettings GetGlobalSettings() { globalSettings.ColorMode = ColorMode.Color; globalSettings.Orientation = Orientation.Portrait; globalSettings.PaperSize = PaperKind.Letter; globalSettings.Margins = new MarginSettings { Top = 1, Bottom = 1, Left = .5, Right = .5, Unit = Unit.Inches }; return globalSettings; } private WebSettings WebSettings() { webSettings.DefaultEncoding = "UTF-8"; return webSettings; } private ObjectSettings GetObjectSettings(string template) { objectSettings.PagesCount = true; objectSettings.WebSettings = WebSettings(); objectSettings.HtmlContent = template; objectSettings.HeaderSettings = HeaderSettings(); objectSettings.FooterSettings = FooterSettings(); return objectSettings; } private HeaderSettings HeaderSettings() { headerSettings.FontSize = 6; headerSettings.FontName = "Times New Roman"; headerSettings.Right = "Page [page] of [toPage]"; headerSettings.Left = "XYZ Company Inc."; headerSettings.Line = true; return headerSettings; } private FooterSettings FooterSettings() { footerSettings.FontSize = 5; footerSettings.FontName = "Times New Roman"; footerSettings.Center = "Revision As Of August 2021"; footerSettings.Line = true; return footerSettings; } }
3. IReportService.cs - This interface contains the contract for the DocumentService.
public interface IReportService { IDocumentService DocumentService { get; } }
4. ReportService.cs - This class implements the IReportService contract and declares a public IDocumentService property. The constructor will inject an instance of the DocumentService class and assign that to the IDocumentService property.
public class ReportService : IReportService { public IDocumentService DocumentService { get; private set; } public ReportService(IDocumentService documentService) { DocumentService = documentService; } }
Helpers Folder
1. STASynchronizedConverter.cs - This class is the fix to this issue "Qt: Could not initialize OLE (error 80010106)" when using the DinkToPDF library. Credits to distantcam for providing the solution.public class STASynchronizedConverter : BasicConverter { Thread conversionThread; BlockingCollection<Task> conversions = new BlockingCollection<Task>(); bool kill = false; private readonly object startLock = new object(); public STASynchronizedConverter(ITools tools) : base(tools) { } public override byte[] Convert(IDocument document) { return Invoke(() => base.Convert(document)); } public TResult Invoke<TResult>(Func<TResult> @delegate) { StartThread(); Task<TResult> task = new Task<TResult>(@delegate); lock (task) { //add task to blocking collection conversions.Add(task); //wait for task to be processed by conversion thread Monitor.Wait(task); } //throw exception that happened during conversion if (task.Exception != null) { throw task.Exception; } return task.Result; } private void StartThread() { lock (startLock) { if (conversionThread == null) { conversionThread = new Thread(Run) { IsBackground = true, Name = "wkhtmltopdf worker thread" }; conversionThread.SetApartmentState(ApartmentState.STA); kill = false; conversionThread.Start(); } } } private void StopThread() { lock (startLock) { if (conversionThread != null) { kill = true; while (conversionThread.ThreadState == ThreadState.Stopped) { } conversionThread = null; } } } private void Run() { while (!kill) { //get next conversion taks from blocking collection Task task = conversions.Take(); lock (task) { //run taks on thread that called RunSynchronously method task.RunSynchronously(); //notify caller thread that task is completed Monitor.Pulse(task); } } } }
2. UnityContainerResolver.cs - This class registers the service type and it's specific implementation type. We also need to register the IConverter interface and the STASynchronizedConverter class and mark it's InstanceLifetime to Singleton.
public class UnityContainerResolver { private UnityContainer container; public UnityContainerResolver() { container = new UnityContainer(); RegisterTypes(); } public void RegisterTypes() { container.RegisterType<IDocumentService, DocumentService>(); container.RegisterInstance(typeof(IConverter), new STASynchronizedConverter(new PdfTools()), InstanceLifetime.Singleton); container.RegisterType<IReportService, ReportService>(); } public ReportService Resolver() { return container.Resolve<ReportService>(); } }
Program.cs
The main entry point class contains a method that will generate the PDF report. This creates an employee object with a list of dependents, retrieves the html template and replaced the tokens defined in the template with the model values. Once the template tokens have been replaced with actual values, we then call the GeneratePdfReport() method and passed the template string which will then return a byte array. We can then make use of the byte array by saving this to disk with an associated filename in a specific location.
class Program { static void Main(string[] args) { CreateReport(); Console.WriteLine("Report Creation Successful"); Console.ReadLine(); } private static async void CreateReport() { UnityContainerResolver resolver; EmployeeViewModel model; string template; string reportPath; string templatePath; WebClient webClient; IReportService service; byte[] rptBytes; webClient = new WebClient(); model = new EmployeeViewModel(); resolver = new UnityContainerResolver(); //set model values model.Employee.EmpID = 100005; model.Employee.EmpName = "John Doe"; model.Employee.EmpStatus = "Married"; model.DependentsList.Add(new Dependent() { DepID = 100001, DepAge = 15, DepName = "Mark Hanson" }); model.DependentsList.Add(new Dependent() { DepID = 100002, DepAge = 25, DepName = "Janet Mills" }); //get template source templatePath = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "ReportTemplate.html"); template = webClient.DownloadString(templatePath); //replace template tokens with values from model if (!string.IsNullOrEmpty(template)) { template = template.Replace("[EmpID]", model.Employee.EmpID.ToString()); template = template.Replace("[EmpName]", model.Employee.EmpName); template = template.Replace("[EmpStatus]", model.Employee.EmpStatus); for (int i = 0; i < 2; i++) { template = template.Replace($"[DepID{i + 1}]", model.DependentsList[i].DepID.ToString()); template = template.Replace($"[DepName{i + 1}]", model.DependentsList[i].DepName); template = template.Replace($"[DepAge{i + 1}]", model.DependentsList[i].DepAge.ToString()); } } //generate the pdf report service = resolver.Resolver(); rptBytes = await service.DocumentService.GeneratePdfReport(template); reportPath = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "EmployeeReport.pdf"); //save report File.WriteAllBytes(reportPath, rptBytes); } }
Output
Report Generated
Source Code: GitHub - Dink To PDF
After rendering the PDf from HTML template does it generates the PDf with CSS classes used in the HTML too?
ReplyDeleteYes, the CSS classes will affect the HTML in the PDF report.
DeleteCould you clearify what's that Unity stuff is exactly doing there? Doesn't show up in the original DinkToPdf docs...
ReplyDeleteWe were using Unity DI container as part of our project. We had to register the STASynchronizedConverter class lifetime to Singleton.
Delete