New to Telerik ReportingStart a free 30-day trial

Implementing Custom ReportSource Resolver and Custom ReportDocument Resolver

The article explains how you may fully control the report definition at runtime and set its data dynamically through custom code.

Most of Telerik Report Viewers utilize Telerik Reporting REST Service. They rely entirely on the latter to prepare and send the requested report documents.

The Reporting engine is exposed by the REST Service, and all the work related to report processing and rendering is done by the engine. The purpose of this article is to elaborate on how the report source information sent by the viewer is resolved to an object that can be processed by the Reporting engine, and where and how the developer may customize this process.

We recommend using the built-in tools for report customization that Telerik Reporting offers. These are:

The above tools allow you to use a variety of Global Objects inside the report definition to modify its appearance at run time. For example, based on the incoming data, Report Parameter values, and the logged-in user. In most reporting scenarios, these tools should let you achieve your goals without custom code.

One of the major benefits of this approach is the much easier upgrade and maintenance of the Reporting part of your application. Our Upgrade Wizard takes care of all the code auto-generated by our report designers. The approach discussed in this article requires custom code that must be maintained manually.

Let's start with some theory before showing two basic examples.

How does the REST Service know which report definition to use and what parameter values to apply?

The Report Viewer sends a client-side reportSource to the REST Service. It is an object with two properties described next:

  • The string property report

    This is the report identifier. Server-side, it will be used by the REST Service to identify the correct report definition to be used as a report template.

  • The parameters dictionary with string keys and object values

    The keys are the Report Parameter names, and the values are the corresponding values that need to be applied. The Reporting engine matches the parameter names specified in the report definition with the keys in the parameters dictionary when applying the values.

The Reporting engine requires a server-side ReportSource to generate a report document. How does it get it from the client-side reportSource sent to the REST Service?

The answer is in the ReportSource resolver that implements the IReportSourceResolver interface. Its Resolve() method is invoked with reportSource.report passed as the first argument and returns the server-side ReportSource to be used by the Reporting engine.

The rest of the arguments of the Resolve() method contain important information that may be used effectively. Let's clarify the meaning and the intended usage of these additional arguments:

  • The OperationOrigin parameter operationOrigin shows the purpose of the current call to the resolver.

    The viewer makes several requests to the REST Service to let the user preview and select the desired parameter values for the report rendering, and then to display the requested report document. The first request that requires report resolving is Get Report Parameters. Its purpose is to check what parameters the report definition has and display the visible ones with their preset or default values. During this call to the resolver, operationOrigin is set to ResolveReportParameters.

    The second call to the resolver is in response to the Resolve Report Instance request from the viewer. When successful, it results in the instantiation of the report definition. The operationOrigin is set to CreateReportInstance.

    The third call is associated with operationOrigin equal to GenerateReportDocument and is in response to the Resolve Document request from the viewer. This is when the rendering begins if the requested document is not found in the REST Service storage. Generally, each report document gets saved in the storage. If the client requests the same report with the same parameter values, the same interactivity state, and in the same rendering format, the report would be taken from the storage rather than being rendered again.

    The GetPageLayout value of operationOrigin is related to the Page Settings dialog of the viewers and is for internal use.

  • The currentParameterValues dictionary contains the parameter values that would be applied when rendering the report. This is a dictionary with the values of the report parameters. You may access the values set on the client with all operationOrigin calls. When opening the report for the first time with operationOrigin equal to OperationOrigin.ResolveReportParameters, currentParameterValues returns only the parameter values set by the client/viewer. At this point, the report has not been resolved yet, and the default parameter values have not been read from the report definition. Let's elaborate on handling the Report Parameter values. Generally, they may be set in three different places:

    1. In the report definition, when you prepare or modify it. These are the default values.
    2. On the client side, in the reportSource of the viewer.
    3. On the server side, in the ReportSource returned by the REST Service.

    There are two approaches for modifying the parameter values in the ReportSource resolver:

    • You may edit the currentParameterValues dictionary directly during OperationOrigin.ResolveReportParameters. The changes will override all the settings, including the values passed from the viewer. If you add parameters to the dictionary that do not exist in the report definition during this call, they will be ignored later. The same is valid for the non-existing parameters added by the viewer. The reason is that when the report definition is identified/resolved, the Reporting engine matches the parameters passed with the ReportSource with the parameters in the report definition by name. Those that don't match the report definition parameters get ignored. The dictionary should not be changed during the rest of the calls, as this may lead to inconsistency between what is displayed in the viewer and the actual parameter values used during report processing, or even a failure in report generation.

    • Here is the priority of applying parameter values if you do not modify the currentParameterValues dictionary directly.

      If you pass any parameter values from the viewer, the Reporting engine will use them with the highest priority. For the values that are not provided, it will utilize the default parameter values from the report definition. In this case, any parameter values set in the ReportSource returned by the resolver will be ignored. For example, if the report has two parameters, Category and Id, and you set only Id from the viewer, the Category value would be taken from the report definition. If the default Category value is not set in the definition, the viewer will display an exception message even if you set this value in the ReportSource returned by the resolver.

      If you don't pass any parameter values from the viewer, the Reporting engine will use the values assigned to the server-side ReportSource.Parameters collection set in the resolver during the OperationOrigin.ResolveReportParameters pass. If you provide them later, they will be ignored, and the values from the report definition will be used instead.

After this short introduction to the ReportSource resolver concept, we may demonstrate it in action with some sample implementations.

How to assign a data source dynamically to a report and one of its data items

A common scenario that requires a custom ReportSource resolver is when you need to assign a data source dynamically to the report or other data items. In this case, it is necessary to instantiate the report definition, set the data source on the report (and/or other data items), and return the modified report definition instance wrapped in an InstanceReportSource.

Here is a sample code that demonstrates the approach:

C#
using System.Collections.Generic;
using System.IO;
using Telerik.Reporting;
using Telerik.Reporting.Services;
namespace MyReportSourceResolverDemo
{
    public class MyReportSourceResolver : IReportSourceResolver
    {
        public string ReportsPath { get; set; }
        public MyReportSourceResolver(string reportsPath)
        {
            this.ReportsPath = reportsPath;
        }
        public ReportSource Resolve(string reportId, OperationOrigin operationOrigin, IDictionary<string, object> currentParameterValues)
        {
            string reportPath = Path.Combine(this.ReportsPath, reportId);
            var reportPackager = new ReportPackager();
            Report report = null;
            using (var sourceStream = System.IO.File.OpenRead(reportPath))
            {
                report = (Report)reportPackager.UnpackageDocument(sourceStream);
            }
            if (operationOrigin == OperationOrigin.GenerateReportDocument)
            {
                // Set the data source for the report
                report.DataSource = MyService.GetMyData();
                // Set the data source for another data item
                var table1 = report.Items.Find("table1", true)[0] as Table;
                table1.DataSource = MyService.GetMyTableData();
            }
            
            return new InstanceReportSource
            {
                ReportDocument = report
            };
        }
    }
}

For the particular scenario, we pass the reports folder in the constructor of the custom ReportSource resolver so that it may find the report definitions. We assume that reportId is the report name, for example, MyReportName.trdp, that should be looked for in this folder. We unpackage the report definition and, on the proper request, add the data source to the report and to its table.

The server-side ReportSource that we return is an InstanceReportSource with the modified report object. What deserves to be acknowledged here is that data source assignment, and therefore the data fetching, happens only when needed.

What happens next?

The server-side ReportSource, along with the parameter values, holds a reference to the report definition.

In the UriReportSource, this is the physical path to the TRDP/TRDX report or TRBP report book and is held in the Uri property.

For the TypeReportSource, this is the assembly-qualified name of the CLR report definition, for example, the C# report class inheriting from our base class Report. It is held in the TypeName property.

The InstanceReportSource holds the report definition instance reference in its ReportDocument property.

The XmlReportSource holds the XML of the report definition in its Xml property. This is the same XML that describes the report and can also be found in the TRDX report definition and inside the TRDP archive in the definition.xml file.

From this reference to the report definition, the Reporting engine needs to extract an instance of the corresponding report. For example, from the Uri to the TRDP file, it needs to produce a Telerik.Reporting.Report object. For this reason, it utilizes the IReportDocumentResolver interface. There is a default implementation for each of the above ReportSource types.

For example, the document resolver for UriReportSource utilizes ReportPackager to instantiate the TRDP reports, and ReportXmlSerializer to deserialize the TRDX reports.

In the REST Service, you may specify your own IReportDocumentResolver implementation. It should be set to the ReportDocumentResolver property of the ReportServiceConfiguration. Why did we introduce it?

The main reason was the way the SubReports got resolved by the service.

Imagine a main report and a subreport referenced inside it. When you use a Custom ReportSource Resolver in the REST Service, the main report will be resolved by it. What about the subreport? Here comes the second example.

How to assign a data source dynamically to a report and a subreport referenced in it

Let's consider a common scenario, where the main report definition XML would be fetched from a database. It contains a SubReport item with XML definition also held in the database. In addition, we need to set the data source at run time for both the main report and its subreport, and for a table in the subreport.

How would the Reporting engine resolve the subreport in this case? Usually, the subreport would be referenced in a SubReport item as a server-side ReportSource, for example, a TRDP report with a relative path. This represents the most common scenario when the reports were designed with the Standalone or Web Report Designer. In this case, the Reporting engine will try to resolve the subreport's UriReportSource in the context of the application.

Importantly, the REST Service ReportSource resolver will not be involved, as the SubReport processing happens as part of the main report processing. This means that the Reporting engine will look in a particular folder of the application, as it will use the default IReportDocumentResolver for the UriReportSource. This is fine in some cases. However, in the general scenario, you would like to keep all your reports in the database rather than keeping the main ones in the database and the subreports in a local folder.

For this common scenario, a custom implementation of IReportDocumentResolver lets you resolve all the report documents in the same manner—for example, from the database. That said, the resolving of any server-side ReportSource.ReportDocument would pass through this custom IReportDocumentResolver rather than through the default one for the corresponding ReportSource. In the context of the example, this would let you fetch all of the report definitions as XML from the database and instantiate them through the ReportXmlSerializer.

Here is a sample implementation of the ReportSourceResolver and the ReportDocumentResolver for the discussed scenario:

C#
using System.Collections.Generic;
using System.IO;
using Telerik.Reporting;
using Telerik.Reporting.Services;
using Telerik.Reporting.XmlSerialization;
namespace MyReportSourceResolverDemo
{
    public class MyReportSourceResolverWithDocumentResolver : IReportSourceResolver
    {
        public ReportSource Resolve(string reportId, OperationOrigin operationOrigin, IDictionary<string, object> currentParameterValues)
        {
            ReportXmlSerializer xmlSerializer = new ReportXmlSerializer();
            Report reportInstance = (Report)xmlSerializer.Deserialize(new StringReader(MyService.GetMainReportXml(reportId)));
            if (operationOrigin == OperationOrigin.GenerateReportDocument)
            {
                // Set the data source for the main report
                reportInstance.DataSource = MyService.GetMyData();
            }
            var instanceReportSource = new InstanceReportSource
            {
                ReportDocument = reportInstance
            };
            return instanceReportSource;
        }
    }
    public class MyReportDocumentResolver : IReportDocumentResolver
    {
        public IReportDocument Resolve(ReportSource reportSource)
        {
            // The main report is wrapped in an InstanceReportSource by MyReportSourceResolver
            if (reportSource is InstanceReportSource)
            {
                return (reportSource as InstanceReportSource).ReportDocument;
            }
            //The subreport is resolved in the context of the main report's SubReport
            else if (reportSource is UriReportSource)
            {
                ReportXmlSerializer xmlSerializer = new ReportXmlSerializer();
                Report report = (Report)xmlSerializer.Deserialize(new StringReader(MyService.GetSubReportXml((reportSource as UriReportSource).Uri)));
                // Set the data source for the subreport
                report.DataSource = MyService.GetMySubreportData();
                // Set the data source for another data item in the subreport
                var table1 = report.Items.Find("table1", true)[0] as Table;
                table1.DataSource = MyService.GetMyTableData();
                return report;
            }
            return null;
        }
    }
}

We set the data source for the main report in the IReportSourceResolver implementation. The reason is that we can do this only once when generating the report document. Remember that the Resolve method of IReportSourceResolver gets called several times during the communication between the viewer and the service. In each of them, IReportDocumentResolver is also called when resolving the main report. Hence, if we set its data source in the IReportDocumentResolver.Resolve method, the data source would be set every time, which is overhead.

The resolving of the SubReport.ReportSource, on the other hand, happens once while processing the main report, and requires a single call to its IReportDocumentResolver. For that reason, we may set the subreport data source in the latter without worrying that this may decrease the performance. Note that since we assumed the SubReport.ReportSource is a UriReportSource in the main report definition, we need to fetch its XML based on the Uri property, deserialize it, set the data sources if necessary, and return the final modified report instance.