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 generated
isReportGenerated = true;
// Create the report
using var report = new LocalReport();// Define the report parameters
var parameters = new[] { new ReportParameter("Title", ReportTitle) };// Load the report definition
var 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 sheet
var items = new List<ReportItem>();
// Iterate through the rows in the sheet
for (int i = 0; i < sheet?.NumRows; i++){// Create a new report item
var 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 description
if (!string.IsNullOrWhiteSpace(item.Description)){// Add the item to the list
items.Add(item);}}// Add the data source to the report
report.DataSources.Add(new ReportDataSource("Items", items));report.SetParameters(parameters);// Render the report to a PDF byte array
byte[] pdfBytes = report.Render("PDF");// To display properly we need to save the PDF as a file and re-load it
var pdfPath = System.IO.Path.Combine("Reports",
$"{DateTime.Now.Ticks.ToString()}-TempReport.pdf");
// Save the pdf to the temp file
System.IO.File.WriteAllBytes(pdfPath, pdfBytes);// Open the temp PDF file and read the bytes
await 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 string
var pdfBase64 = Convert.ToBase64String(pdfBytes);// Call the JavaScript function with the element reference and the PDF data
await jsRuntime.InvokeVoidAsync("blazorPdfViewer.renderPdf", pdfContainer, pdfBase64);
// Delete the temp pdf file
System.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 report
using var report = new LocalReport();// Define the report parameters
var 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 null
if (reportStream == null){// Throw an exception
throw new Exception("Report.rdlc not found");}// Get all the values from the sheet
var items = new List<ReportItem>();
// Iterate through the rows in the sheet
for (int i = 0; i < sheet?.NumRows; i++){// Create a new report item
var 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 description
if (!string.IsNullOrWhiteSpace(item.Description)){// Add the item to the list
items.Add(item);}}// Convert the list of items to a list of dictionaries
var 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 array
using var ms = new MemoryStream();await reportStream.CopyToAsync(ms);byte[] rdlcBytes = ms.ToArray();
// 2. Build the request payload
var requestPayload = new MyReportRequest
{RdlcBytes = rdlcBytes,Title = ReportTitle, // parameter for the RDLC
Rows = ItemsAsDict // our list of dictionaries
};// 3. POST to the server endpoint
HttpResponseMessage 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 request
if (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 DataTable
var dataTable = ConvertToDataTable(request.Rows);// 3. Create the LocalReport
using var report = new LocalReport();// 4. Load the RDLC definition from the byte array
using 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 PDF
byte[] pdfBytes = report.Render("PDF");// 8. Return the PDF to the client
return File(pdfBytes, "application/pdf");}private DataTable ConvertToDataTable(List<Dictionary<string, object>> rows){// Create a DataTable
DataTable dt = new DataTable("MyDataTable");if (rows.Count == 0) return dt;// Get all fields from the first row
var 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 rows
foreach (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 response
var pdfBytes = await response.Content.ReadAsByteArrayAsync();// Convert the PDF bytes to a base64 string
var pdfBase64 = Convert.ToBase64String(pdfBytes);// Call the JavaScript function with the element reference and the PDF data
await jsRuntime.InvokeVoidAsync("blazorPdfViewer.renderPdf", pdfContainer, pdfBase64);
}else
{// Handle error
var 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.