Overview
This example demonstrates how to implement and use custom services, which allow you to save, load, and export reports asynchronously.
This application registers a service with the IReportProviderAsync interface. The latter allows you to perform the following tasks asynchronously:
- open subreports from the master report;
- resolve a unique identifier to a report (an operation that affects the Designer Preview and Document Viewer performance).
In general, asynchronous operation mode gives you the following advantages:
- It handles threads in web applications more cautiously and returns threads to the thread pool while the operation is in progress.
- You can write safe asynchronous code in a project with a third-party library that uses only asynchronous API.
You can use an asynchronous engine in the following scenarios:
- Handle the End User Report Designer and Web Document Viewer events to load data or create a document asynchronously. The key point is that these actions can be performed in the context of the current HTTP request in the WebDocumentViewerOperationLogger and PreviewReportCustomizationService class methods.
- Drill Through navigation in the Web Document Viewer. Use the IDrillThroughProcessorAsync interface.
- If an application uses JWT-based Authentication, use the IWebDocumentViewerExportResultUriGeneratorAsync interface to load exported documents to the storage asynchronously.
Implementation Details
Perequisites
To get started, create a sample project with our ASP.NET Core Reporting Project Template.
Report Provider
The CustomReportProviderAsync.cs
file contains a class that implements the DevExpress.XtraReports.Services.IReportProviderAsync interface and calls the report storage's ReportStorageWebExtension.GetDataAsync method.
Report Storage
The CustomReportStorageWebExtension.cs
file contains a class that implements a custom report storage with asynchronous methods. The CustomReportStorageWebExtension class inherits from the ReportStorageWebExtension and overrides all its public methods. Note that methods, which cannot be used in asynchronous mode, throw exceptions.
Report Model and Controllers
The Home controller uses the GetModelAsync methods to generate report models and send them to the End User Report Designer and Document Viewer.
You cannot use asynchronous API if the Bind method receives a report instance or a string (ReportUrl) as a parameter. Bind report controls to the ReportDesignerModel or WebDocumentViewerModel objects originated from controllers. The controller-based model allows you to use asynchronous API and avoid obscure problems. Such problems may occur when a subreport fails to load and throws an exception, or when a dynamic list of parameter values fails to retrieve its data.
ExportToPdf Action
The export action takes advantage of the IReportProviderAsync service that resolves report ID to a report and expedites the load of subreports without the need for the web report controls. The DI container injects the IReportProviderAsync service into the XtraReport instance. Then, the asynchronous CreateDocumentAsync method creates a document and the asynchronous ExportToPdfAsync method exports the document to PDF.
Enable Asynchronous Services
Request handlers in ASP.NET Core applications are asynchronous. To enable asynchronous services, call the UseAsyncEngine method at application startup:
C#services.ConfigureReportingServices(configurator => {
configurator.ConfigureReportDesigner(designerConfigurator => {
//..
});
configurator.ConfigureWebDocumentViewer(viewerConfigurator => {
//..
});
configurator.UseAsyncEngine();
});
Files to Review
- Startup.cs
- CustomReportProviderAsync.cs
- CustomPreviewCustomizationService.cs
- CustomReportStorageWebExtension.cs
- HomeController.cs
Documentation
More Examples
Does this example address your development requirements/objectives?
(you will be redirected to DevExpress.com to submit your response)
Example Code
C#using System;
using DevExpress.AspNetCore;
using DevExpress.AspNetCore.Reporting;
using DevExpress.XtraReports.Services;
using DevExpress.XtraReports.Web.Extensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ReportingAppAsyncServices.Services;
namespace ReportingAppAsyncServices
{
public class Startup {
public Startup(IConfiguration configuration, IWebHostEnvironment hostingEnvironment) {
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services) {
services.AddDevExpressControls();
services.AddSession();
services.AddScoped<ReportStorageWebExtension, CustomReportStorageWebExtension>();
services.AddScoped<IReportProviderAsync, CustomReportProviderAsync>();
services
.AddControllersWithViews()
.AddNewtonsoftJson();
services.ConfigureReportingServices(configurator => {
configurator.ConfigureReportDesigner(designerConfigurator => {
designerConfigurator.RegisterDataSourceWizardConfigFileConnectionStringsProvider();
});
configurator.ConfigureWebDocumentViewer(viewerConfigurator => {
viewerConfigurator.UseCachedReportSourceBuilder();
});
configurator.UseAsyncEngine();
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) {
var reportingLogger = loggerFactory.CreateLogger("DXReporting");
DevExpress.XtraReports.Web.ClientControls.LoggerService.Initialize((exception, message) => {
var logMessage = $"[{DateTime.Now}]: Exception occurred. Message: '{message}'. Exception Details:\r\n{exception}";
reportingLogger.LogError(logMessage);
});
DevExpress.XtraReports.Configuration.Settings.Default.UserDesignerOptions.DataBindingMode = DevExpress.XtraReports.UI.DataBindingMode.Expressions;
app.UseSession();
app.UseDevExpressControls();
System.Net.ServicePointManager.SecurityProtocol |= System.Net.SecurityProtocolType.Tls12;
if(env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
} else {
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
C#using System.IO;
using System.Threading.Tasks;
using DevExpress.XtraReports.Services;
using DevExpress.XtraReports.UI;
using DevExpress.XtraReports.Web.Extensions;
namespace ReportingAppAsyncServices.Services
{
public class CustomReportProviderAsync : IReportProviderAsync
{
readonly ReportStorageWebExtension reportStorageWebExtension;
public CustomReportProviderAsync(ReportStorageWebExtension reportStorageWebExtension)
{
this.reportStorageWebExtension = reportStorageWebExtension;
}
public async Task<XtraReport> GetReportAsync(string id, ReportProviderContext context)
{
var reportLayout = await reportStorageWebExtension.GetDataAsync(id);
if (reportLayout == null)
return null;
using (var ms = new MemoryStream(reportLayout))
{
var report = XtraReport.FromXmlStream(ms);
return report;
}
}
}
}
C#using DevExpress.XtraReports.UI;
using DevExpress.XtraReports.Web.ReportDesigner.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ReportingAppAsyncServices.Services
{
public class CustomPreviewCustomizationService: PreviewReportCustomizationService
{
public override Task CustomizeReportAsync(XtraReport report)
{
//TODO: Call CreateDocumentAsync or do any async data retrieving
return base.CustomizeReportAsync(report);
}
}
}
C#using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using DevExpress.XtraReports.UI;
using DevExpress.XtraReports.Web.ClientControls;
using Microsoft.AspNetCore.Hosting;
using ReportingAppAsyncServices.PredefinedReports;
namespace ReportingAppAsyncServices.Services
{
public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extensions.ReportStorageWebExtension
{
readonly string ReportDirectory;
const string FileExtension = ".repx";
public CustomReportStorageWebExtension(IWebHostEnvironment env)
{
ReportDirectory = Path.Combine(env.ContentRootPath, "Reports");
if (!Directory.Exists(ReportDirectory))
{
Directory.CreateDirectory(ReportDirectory);
}
}
private bool IsWithinReportsFolder(string url, string folder)
{
var rootDirectory = new DirectoryInfo(folder);
var fileInfo = new FileInfo(Path.Combine(folder, url));
return fileInfo.Directory.FullName.ToLower().StartsWith(rootDirectory.FullName.ToLower());
}
public override async Task<byte[]> GetDataAsync(string url)
{
// Returns report layout data stored in a Report Storage using the specified URL.
// This method is called only for valid URLs after the IsValidUrl method is called.
try
{
if (Directory.EnumerateFiles(ReportDirectory).Select(Path.GetFileNameWithoutExtension).Contains(url))
{
return await File.ReadAllBytesAsync(Path.Combine(ReportDirectory, url + FileExtension));
}
if (ReportsFactory.Reports.ContainsKey(url))
{
using (MemoryStream ms = new MemoryStream())
{
ReportsFactory.Reports[url]().SaveLayoutToXml(ms);
return ms.ToArray();
}
}
}
catch (Exception ex)
{
throw new DevExpress.XtraReports.Web.ClientControls.FaultException("Could not get report data.", ex);
}
throw new DevExpress.XtraReports.Web.ClientControls.FaultException(string.Format("Could not find report '{0}'.", url));
}
public override Task<Dictionary<string, string>> GetUrlsAsync()
{
// Returns a dictionary of the existing report URLs and display names.
// This method is called when running the Report Designer,
// before the Open Report and Save Report dialogs are shown and after a new report is saved to a storage.
return Task.FromResult(Directory.GetFiles(ReportDirectory, "*" + FileExtension)
.Select(Path.GetFileNameWithoutExtension)
.Union(ReportsFactory.Reports.Select(x => x.Key))
.ToDictionary<string, string>(x => x));
}
public override Task SetDataAsync(XtraReport report, string url)
{
// Stores the specified report to a Report Storage using the specified URL.
// This method is called only after the IsValidUrl and CanSetData methods are called.
if (!IsWithinReportsFolder(url, ReportDirectory))
throw new DevExpress.XtraReports.Web.ClientControls.FaultException("Invalid report name.");
report.SaveLayoutToXml(Path.Combine(ReportDirectory, url + FileExtension));
return Task.CompletedTask;
}
public override async Task<string> SetNewDataAsync(XtraReport report, string defaultUrl)
{
// Stores the specified report using a new URL.
// The IsValidUrl and CanSetData methods are never called before this method.
// You can validate and correct the specified URL directly in the SetNewData method implementation
// and return the resulting URL used to save a report in your storage.
await SetDataAsync(report, defaultUrl);
return defaultUrl;
}
public override bool CanSetData(string url)
{
// Determines whether or not it is possible to store a report by a given URL.
// For instance, make the CanSetData method return false for reports that should be read-only in your storage.
// This method is called only for valid URLs (i.e., if the IsValidUrl method returned true) before the SetData method is called.
return true;
}
public override bool IsValidUrl(string url)
{
// Determines whether or not the URL passed to the current Report Storage is valid.
// For instance, implement your own logic to prohibit URLs that contain white spaces or some other special characters.
// This method is called before the CanSetData and GetData methods.
return Path.GetFileName(url) == url;
}
public override void SetData(XtraReport report, string url)
{
throw new FaultException("SetData was called");
}
public override string SetNewData(XtraReport report, string defaultUrl)
{
throw new FaultException("SetNewData was called");
}
public override byte[] GetData(string url)
{
throw new FaultException("GetData was called");
}
public override Dictionary<string, string> GetUrls()
{
throw new FaultException("GetUrls was called");
}
}
}
C#using System.Collections.Generic;
using System.ComponentModel.Design;
using System.IO;
using System.Threading.Tasks;
using DevExpress.AspNetCore.Reporting.WebDocumentViewer;
using DevExpress.DataAccess.Sql;
using DevExpress.XtraReports.Services;
using DevExpress.XtraReports.Web.ReportDesigner.Services;
using DevExpress.XtraReports.Web.WebDocumentViewer;
using Microsoft.AspNetCore.Mvc;
namespace ReportingAppAsyncServices.Controllers {
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
public async Task<IActionResult> Designer([FromServices] IReportDesignerModelBuilder reportDesignerModelBuilder, [FromQuery] string reportName = "RootReport")
{
var dataSources = new Dictionary<string, object>
{
["Northwind"] = GetNorthwindSqlDataSource()
};
var designerModel = await reportDesignerModelBuilder
.DataSources(dataSources)
.Report(reportName)
.BuildModelAsync();
return View(designerModel);
}
public async Task<IActionResult> Viewer([FromServices] IWebDocumentViewerClientSideModelGenerator modelGenerator, [FromQuery] string reportName = "RootReport")
{
var viewerModel = await modelGenerator.GetModelAsync(reportName, WebDocumentViewerController.DefaultUri);
return View(viewerModel);
}
public async Task<IActionResult> ExportToPdf([FromServices] IReportProviderAsync reportProviderAsync, [FromQuery] string reportName = "RootReport")
{
var report = await reportProviderAsync.GetReportAsync(reportName, null);
var reportServiceContainer = (IServiceContainer)report;
reportServiceContainer.RemoveService(typeof(IReportProviderAsync));
reportServiceContainer.AddService(typeof(IReportProviderAsync), reportProviderAsync);
using (var stream = new MemoryStream()) {
await report.CreateDocumentAsync();
await report.ExportToPdfAsync(stream);
return File(stream.ToArray(), System.Net.Mime.MediaTypeNames.Application.Pdf);
}
}
SqlDataSource GetNorthwindSqlDataSource()
{
// Create a SQL data source with the specified connection string.
SqlDataSource ds = new SqlDataSource("NWindConnectionString");
// Create a SQL query to access the Products data table.
SelectQuery query = SelectQueryFluentBuilder.AddTable("Products").SelectAllColumnsFromTable().Build("Products");
ds.Queries.Add(query);
ds.RebuildResultSchema();
return ds;
}
}
}