12/30/2024 Admin
Develop and Display RDLC Reports in Microsoft Blazor
You can design and display RDLC reports, developed using Visual Studio, in Microsoft Blazor.
This allows you to provide pixel perfect, printable reports, entirely in your Blazor application, without the need for an external report server.
This is made possible primarily by the functionally provided by these two projects:
The code covered in this article is available here: https://github.com/ADefWebserver/BlazorRDLCViewer (Blazor Server)
and here: https://github.com/ADefWebserver/BlazorRDLCViewerWebAssembly (Blazor WebAssembly)
The Sample Application
When you download the code from: https://github.com/ADefWebserver/BlazorRDLCViewer and run it in Visual Studio, on the Home page, you will see a screen that allows you to set a Report Title and enter inventory items.
When you have entered all your values, you can click the Generate Report button.
This will pass the values to the RDLC report and display the report on the page in a JavaScript viewer using code from PdfJs.
You will have the option to zoom in and out, and if there are multiple pages, navigate those pages using the previous and next buttons.
Finally, you will be able to initiate printing of the report using the print button.
The print dialog will show and allow you to select a printer.
You can then click the print button to complete the process.
The Report Viewer
The functionality of the report viewer, that displays the PDF created by Report Viewer Core, is provided by the PdfJs library.
It’s JavaScript code is referenced in the App.Razor page.
Custom code, written for this project, is contained in the wwwroot/js/site.js file.
That file is also referenced in the App.Razor page.
Finally, JavaScript methods to call the code in site.js is contained on the App.Razor page (noted in green in the image above).
The Home.razor page contains this markup to first display the controls to collect data, and the toolbar and the div to display the report when the Generate Report button is clicked.
Note the div to display the report uses the code:
so that it can be passed by reference to the JavaScript code so that it can place the rendered PDF in that location.@ref="pdfContainer"
<PageTitle>Home</PageTitle>@if (isReportGenerated){<div>Page: <span>@CurrentPage</span> / <span id="page_count">0</span> <button @onclick="PreviousPage">Previous</button><button @onclick="NextPage">Next</button><button @onclick="ZoomIn">Zoom In</button><button @onclick="ZoomOut">Zoom Out</button><button @onclick="PrintPdf">Print</button><div @ref="pdfContainer" id="pdf-container"style="border: 0px solid #ccc; width: 600px; height: 800px;"></div></div>}else{<!-- Display text box to obtain report title bound to ReportTitle --><label for="reportTitle">Report Title:</label><input type="text" id="reportTitle" @bind="ReportTitle" /><br /><br /><Datasheet Sheet="sheet"/><br /><br /><button @onclick="PrepareReportAsync">Generate Report</button>}
When the Generate Report button is clicked, the following code runs to open the RDLC report, pass the data to it, render it as a PDF, and pass it to the JavaScript viewer:
private async Task PrepareReportAsync(){// Set the flag to indicate the report is generatedisReportGenerated = true;// Create the reportusing var report = new LocalReport();// Define the report parametersvar parameters = new[] { new ReportParameter("Title", ReportTitle) };// Load the report definitionvar reportPath = System.IO.Path.Combine("Reports", "Report.rdlc");if (!System.IO.File.Exists(reportPath)){throw new FileNotFoundException("Report file not found.", reportPath);}// Load the report definition (the RDLC file)await using var reportStream = System.IO.File.OpenRead(reportPath);using var reportReader = new System.IO.StreamReader(reportStream);report.LoadReportDefinition(reportReader);// Get all the values from the sheetvar items = new List<ReportItem>();// Iterate through the rows in the sheetfor (int i = 0; i < sheet?.NumRows; i++){// Create a new report itemvar item = new ReportItem{Description = sheet.Cells[i, 0].Value?.ToString() ?? string.Empty,Price =decimal.TryParse(sheet.Cells[i, 1].Value?.ToString(), out var price) ? price : 0m,Qty = int.TryParse(sheet.Cells[i, 2].Value?.ToString(), out var qty) ? qty : 0};// Only add items with a descriptionif (!string.IsNullOrWhiteSpace(item.Description)){// Add the item to the listitems.Add(item);}}// Add the data source to the reportreport.DataSources.Add(new ReportDataSource("Items", items));report.SetParameters(parameters);// Render the report to a PDF byte arraybyte[] pdfBytes = report.Render("PDF");// To display properly we need to save the PDF as a file and re-load itvar pdfPath = System.IO.Path.Combine("Reports",$"{DateTime.Now.Ticks.ToString()}-TempReport.pdf");// Save the pdf to the temp fileSystem.IO.File.WriteAllBytes(pdfPath, pdfBytes);// Open the temp PDF file and read the bytesawait using (var pdfStream = System.IO.File.OpenRead(pdfPath)){using (var memoryStream = new System.IO.MemoryStream()){await pdfStream.CopyToAsync(memoryStream);pdfBytes = memoryStream.ToArray();}}// Convert the PDF bytes to a base64 stringvar pdfBase64 = Convert.ToBase64String(pdfBytes);// Call the JavaScript function with the element reference and the PDF dataawait jsRuntime.InvokeVoidAsync("blazorPdfViewer.renderPdf", pdfContainer, pdfBase64);// Delete the temp pdf fileSystem.IO.File.Delete(pdfPath);}
Note: the Blazor WebAssembly version uses code like this to get the .rdlc report, and the data, and pass to a server side controller:
// Create the reportusing var report = new LocalReport();// Define the report parametersvar parameters = new[] { new ReportParameter("Title", ReportTitle) };var assembly = typeof(Program).Assembly;// The resource name depends on your project's default namespace + folder structure.string resourceName = "BlazorRDLCViewerWebAssembly.Client.Reports.Report.rdlc";using Stream? reportStream = assembly.GetManifestResourceStream(resourceName);// Check that reportStream is not nullif (reportStream == null){// Throw an exceptionthrow new Exception("Report.rdlc not found");}// Get all the values from the sheetvar items = new List<ReportItem>();// Iterate through the rows in the sheetfor (int i = 0; i < sheet?.NumRows; i++){// Create a new report itemvar item = new ReportItem{Description = sheet.Cells[i, 0].Value?.ToString() ?? string.Empty,Price =decimal.TryParse(sheet.Cells[i, 1].Value?.ToString(), out var price) ? price : 0m,Qty = int.TryParse(sheet.Cells[i, 2].Value?.ToString(), out var qty) ? qty : 0};// Only add items with a descriptionif (!string.IsNullOrWhiteSpace(item.Description)){// Add the item to the listitems.Add(item);}}// Convert the list of items to a list of dictionariesvar ItemsAsDict = items.Select(item => new Dictionary<string, object>{{ "Description", item.Description ?? "" },{ "Price", item.Price },{ "Qty", item.Qty },{ "Total", item.Price * item.Qty }}).ToList();// Convert the RDLC stream to a byte arrayusing var ms = new MemoryStream();await reportStream.CopyToAsync(ms);byte[] rdlcBytes = ms.ToArray();// 2. Build the request payloadvar requestPayload = new MyReportRequest{RdlcBytes = rdlcBytes,Title = ReportTitle, // parameter for the RDLCRows = ItemsAsDict // our list of dictionaries};// 3. POST to the server endpointHttpResponseMessage response = await Http.PostAsJsonAsync("Reports/GenerateReport",requestPayload);
The server side controller uses this code to render the report and return a PDF:
using Microsoft.AspNetCore.Mvc;using Microsoft.Extensions.Logging;using System.IO;using System.Threading.Tasks;using System.Collections.Generic;using System;using System.Data;using Microsoft.Reporting.NETCore;using Microsoft.AspNetCore.Http.HttpResults;using Microsoft.ReportingServices.ReportProcessing.ReportObjectModel;public class MyReportRequest{public byte[]? RdlcBytes { get; set; }public string? Title { get; set; }public List<Dictionary<string, object>>? Rows { get; set; }}[ApiController][Route("[controller]")]public class ReportsController : ControllerBase{[HttpPost("GenerateReport")]public IActionResult GenerateReport([FromBody] MyReportRequest request){// 1. Validate requestif (request.RdlcBytes == null || request.RdlcBytes.Length == 0)return BadRequest("No RDLC data received.");if (request.Rows == null || request.Rows.Count == 0)return BadRequest("No row data received.");// 2. Convert rows to a DataTablevar dataTable = ConvertToDataTable(request.Rows);// 3. Create the LocalReportusing var report = new LocalReport();// 4. Load the RDLC definition from the byte arrayusing var ms = new MemoryStream(request.RdlcBytes);using var sr = new StreamReader(ms);report.LoadReportDefinition(sr);// 5. Add the data source (the name "Items" must match your RDLC dataset)report.DataSources.Clear();report.DataSources.Add(new ReportDataSource("Items", dataTable));// 6. Set the report parameter (if your RDLC uses "Title")if (!string.IsNullOrEmpty(request.Title)){report.SetParameters(new ReportParameter("Title", request.Title));}// 7. Render the report into PDFbyte[] pdfBytes = report.Render("PDF");// 8. Return the PDF to the clientreturn File(pdfBytes, "application/pdf");}private DataTable ConvertToDataTable(List<Dictionary<string, object>> rows){// Create a DataTableDataTable dt = new DataTable("MyDataTable");if (rows.Count == 0) return dt;// Get all fields from the first rowvar fields = rows[0].Keys;// Add columns for all fields (replace spaces with underscores)foreach (var field in fields){string columnName = field;dt.Columns.Add(columnName, typeof(object));}// Fill the DataTable rowsforeach (var item in rows){DataRow newRow = dt.NewRow();foreach (var field in fields){string columnName = field;newRow[columnName] = (item[field] != null) ? item[field].ToString() : null;}dt.Rows.Add(newRow);}return dt;}}
Finally, the .razor page receives the PDF and uses this code to display it in the report viewer:
if (response.IsSuccessStatusCode){// 5. Read the PDF bytes from the responsevar pdfBytes = await response.Content.ReadAsByteArrayAsync();// Convert the PDF bytes to a base64 stringvar pdfBase64 = Convert.ToBase64String(pdfBytes);// Call the JavaScript function with the element reference and the PDF dataawait jsRuntime.InvokeVoidAsync("blazorPdfViewer.renderPdf", pdfContainer, pdfBase64);}else{// Handle errorvar errorMessage = await response.Content.ReadAsStringAsync();throw new Exception($"Failed to generate report: {errorMessage}");}
Designing Reports
To design reports you need to use Visual Studio 2022 (or higher) and you must install this extension: Microsoft RDLC Report Designer 2022.
You can make a new .rdlc file using the following code:
<?xml version="1.0" encoding="utf-8"?><Report xmlns="http://schemas.microsoft.com/sqlserver/reporting/2016/01/reportdefinition" xmlns:rd="http://schemas.microsoft.com/SQLServer/reporting/reportdesigner"><AutoRefresh>0</AutoRefresh><ReportSections><ReportSection><Body><Height>2in</Height><Style /></Body><Width>6.5in</Width><Page><LeftMargin>0.7874in</LeftMargin><RightMargin>0.7874in</RightMargin><TopMargin>0.7874in</TopMargin><BottomMargin>0.7874in</BottomMargin><ColumnSpacing>0.05118in</ColumnSpacing><Style /></Page></ReportSection></ReportSections><ReportParametersLayout><GridLayoutDefinition><NumberOfColumns>4</NumberOfColumns><NumberOfRows>2</NumberOfRows></GridLayoutDefinition></ReportParametersLayout><rd:ReportUnitType>Inch</rd:ReportUnitType><rd:ReportID>156b21d2-9542-4902-99ef-7ec823774bb2</rd:ReportID></Report>
However, when you open it in the designer, and display the Report Data pane (go to the View menu in the top toolbar.Select Report Data from the dropdown)…
Initially you won’t have any object data sources available.
To create one, create a class like this, that represents the format of the data you will have on your report:
namespace BlazorRDLCViewer{public class ReportItem{public string? Description { get; set; }public decimal Price { get; set; }public int Qty { get; set; }public decimal Total => Price * Qty;}}
Reference that class by placing it in the types collection in the CreateSchemas method in the Schemas.razor file:
private void CreateSchemas(){var types = new[] { typeof(ReportItem) };var xri = new System.Xml.Serialization.XmlReflectionImporter();var xss = new System.Xml.Serialization.XmlSchemas();var xse = new System.Xml.Serialization.XmlSchemaExporter(xss);foreach (var type in types){var xtm = xri.ImportTypeMapping(type);xse.ExportTypeMapping(xtm);}using var sw = new System.IO.StreamWriter("Reports/ReportItemSchemas.xsd", false, Encoding.UTF8);for (int i = 0; i < xss.Count; i++){var xs = xss[i];xs.Id = "ReportItemSchemas";xs.Write(sw);}status = "ReportItemSchemas.xsd created.";}
Run the application, select the Create Schemas page, and click the Create ReportItemSchemas.xsd button.
Stop the application and note that the ReportItemSchemas.xsd has been created.
Now a Data source and dataset can be selected.
Reports can be designed.
Notes
- Creating PDF reports when this code is running on Linux doesn’t work. See: Linux rendering workaround
- Also see other limitations at: What doesn't work
Download
The project is available at the following locations:
- https://github.com/ADefWebserver/BlazorRDLCViewer (Blazor Server)
- https://github.com/ADefWebserver/BlazorRDLCViewerWebAssembly (Blazor WebAssembly)
You must have Visual Studio 2022 (or higher) installed to run the code.
