Hi,
I want end-users to design their own reports to run in an application where the database connection is dynamic. This was sort-of covered in a previous post but now that I understand things better I wanted to ask the right questions. The options I have got are:
1. Use sql connector in the designer with a 'shared' connection string (where the alias is the same name as stored in web.config). This works great as the user can do their own joins using the query builder, slap the reports into the website and they work. BUT there is no connection string in my web.config, it is stored elsewhere.
2. Extend the standalone designer so the users can use an Object Datasource to use my custom dll BUT then all the joins need to be pre-defined in the dll code. (I have already got this working, but joining all the tables is a real problem + confusing for the uesr)
So what I want to ask - is there any way I can use case 1 by switching the connection string at run time (I have all the info available in memory, it's just not static in web.config)?
12 Answers, 1 is accepted
By default, SqlDataSource components read their connection strings from the application's configuration file via ConfigurationManager.
If you want to make changes, you can get a Telerik.Reporting.Report instance and update all DataSource properties of data items in it. Please consider the example in Changing the connection string dynamically according to runtime data.
In the case of the HTML5 Viewer and Reporting REST service, such code can be executed in a custom resolver for the Reporting REST service. An example is available in this forum post.
Let us know if you have further questions.
Regards,
Stef
Telerik

Thanks Stef,
That sounds like it's just what I need. Quite a lot to digest though, I'll report back once I've run some tests.

Ok, It seems I have this working after combining/reworking some of the concepts at the links provided by Stef.
- ReportConnectionStringManager class comes from here as-is
- The standard reports controller now uses a custom resolver instead of the default one
- The custom resolver returns a report instance with the db connection switched
@Stef: please confirm that my approach is correct and all aspects of the db connection will be updated (ie. all report items/parameters etc.)
using
System;
using
System.IO;
using
System.Web;
using
Telerik.Reporting;
using
Telerik.Reporting.Cache.File;
using
Telerik.Reporting.Services;
using
Telerik.Reporting.Services.Engine;
using
Telerik.Reporting.Services.WebApi;
using
Telerik.Reporting.XmlSerialization;
namespace
Scratch.Controllers
{
//The class name determines the service URL.
//ReportsController class name defines /api/report/ service URL.
public
class
ReportsController : ReportsControllerBase
{
static
ReportServiceConfiguration configurationInstance;
static
ReportsController()
{
//This uses a custom resolver
configurationInstance =
new
ReportServiceConfiguration
{
HostAppId =
"Html5App"
,
Storage =
new
FileStorage(),
ReportResolver =
new
MyResolver(),
// ReportSharingTimeout = 0,
// ClientSessionTimeout = 15,
};
}
public
ReportsController()
{
//Initialize the service configuration
this
.ReportServiceConfiguration = configurationInstance;
}
}
public
class
MyResolver : IReportResolver
{
//this method will be called on each request for report (refresh, navigate to report, sub report, parameters updates)
//the method will be called 3 times on initial load
public
Telerik.Reporting.ReportSource Resolve(
string
report)
{
ReportSource reportInstance =
null
;
//retrieve an instance of the report
var connectionString =
"Data Source=XXXXXXXX;Initial Catalog=XXXXXXXXXXX;User ID=XXXXXXXX;Password=XXXXXXXXXXX;"
;
ReportConnectionStringManager csm =
new
ReportConnectionStringManager(connectionString);
var appPath = HttpContext.Current.Server.MapPath(
"~/"
);
var reportsPath = Path.Combine(appPath,
"Reports"
);
var uri = Path.Combine(reportsPath, report);
var sourceReportSource =
new
UriReportSource() { Uri = uri };
reportInstance = csm.UpdateReportSource(sourceReportSource);
return
reportInstance;
}
}
public
class
ReportConnectionStringManager
{
readonly
string
connectionString;
public
ReportConnectionStringManager(
string
connectionString)
{
this
.connectionString = connectionString;
}
public
ReportSource UpdateReportSource(ReportSource sourceReportSource)
{
if
(sourceReportSource
is
UriReportSource)
{
var uriReportSource = (UriReportSource)sourceReportSource;
var reportInstance = DeserializeReport(uriReportSource);
ValidateReportSource(uriReportSource.Uri);
this
.SetConnectionString(reportInstance);
return
CreateInstanceReportSource(reportInstance, uriReportSource);
}
if
(sourceReportSource
is
XmlReportSource)
{
var xml = (XmlReportSource)sourceReportSource;
ValidateReportSource(xml.Xml);
var reportInstance =
this
.DeserializeReport(xml);
this
.SetConnectionString(reportInstance);
return
CreateInstanceReportSource(reportInstance, xml);
}
if
(sourceReportSource
is
InstanceReportSource)
{
var instanceReportSource = (InstanceReportSource)sourceReportSource;
this
.SetConnectionString((ReportItemBase)instanceReportSource.ReportDocument);
return
instanceReportSource;
}
if
(sourceReportSource
is
TypeReportSource)
{
var typeReportSource = (TypeReportSource)sourceReportSource;
var typeName = typeReportSource.TypeName;
ValidateReportSource(typeName);
var reportType = Type.GetType(typeName);
var reportInstance = (Report)Activator.CreateInstance(reportType);
this
.SetConnectionString((ReportItemBase)reportInstance);
return
CreateInstanceReportSource(reportInstance, typeReportSource);
}
throw
new
NotImplementedException(
"Handler for the used ReportSource type is not implemented."
);
}
ReportSource CreateInstanceReportSource(IReportDocument report, ReportSource originalReportSource)
{
var instanceReportSource =
new
InstanceReportSource { ReportDocument = report };
instanceReportSource.Parameters.AddRange(originalReportSource.Parameters);
return
instanceReportSource;
}
void
ValidateReportSource(
string
value)
{
if
(value.Trim().StartsWith(
"="
))
{
throw
new
InvalidOperationException(
"Expressions for ReportSource are not supported when changing the connection string dynamically"
);
}
}
Report DeserializeReport(UriReportSource uriReportSource)
{
var settings =
new
System.Xml.XmlReaderSettings();
settings.IgnoreWhitespace =
true
;
using
(var xmlReader = System.Xml.XmlReader.Create(uriReportSource.Uri, settings))
{
var xmlSerializer =
new
Telerik.Reporting.XmlSerialization.ReportXmlSerializer();
var report = (Telerik.Reporting.Report)xmlSerializer.Deserialize(xmlReader);
return
report;
}
}
Report DeserializeReport(XmlReportSource xmlReportSource)
{
var settings =
new
System.Xml.XmlReaderSettings();
settings.IgnoreWhitespace =
true
;
var textReader =
new
System.IO.StringReader(xmlReportSource.Xml);
using
(var xmlReader = System.Xml.XmlReader.Create(textReader, settings))
{
var xmlSerializer =
new
Telerik.Reporting.XmlSerialization.ReportXmlSerializer();
var report = (Telerik.Reporting.Report)xmlSerializer.Deserialize(xmlReader);
return
report;
}
}
void
SetConnectionString(ReportItemBase reportItemBase)
{
if
(reportItemBase.Items.Count < 1)
return
;
if
(reportItemBase
is
Report)
{
var report = (Report)reportItemBase;
if
(report.DataSource
is
SqlDataSource)
{
var sqlDataSource = (SqlDataSource)report.DataSource;
sqlDataSource.ConnectionString = connectionString;
}
foreach
(var parameter
in
report.ReportParameters)
{
if
(parameter.AvailableValues.DataSource
is
SqlDataSource)
{
var sqlDataSource = (SqlDataSource)parameter.AvailableValues.DataSource;
sqlDataSource.ConnectionString = connectionString;
}
}
}
foreach
(var item
in
reportItemBase.Items)
{
//recursively set the connection string to the items from the Items collection
SetConnectionString(item);
//set the drillthrough report connection strings
var drillThroughAction = item.Action
as
NavigateToReportAction;
if
(
null
!= drillThroughAction)
{
var updatedReportInstance =
this
.UpdateReportSource(drillThroughAction.ReportSource);
drillThroughAction.ReportSource = updatedReportInstance;
}
if
(item
is
SubReport)
{
var subReport = (SubReport)item;
subReport.ReportSource =
this
.UpdateReportSource(subReport.ReportSource);
continue
;
}
//Covers all data items(Crosstab, Table, List, Graph, Map and Chart)
if
(item
is
DataItem)
{
var dataItem = (DataItem)item;
if
(dataItem.DataSource
is
SqlDataSource)
{
var sqlDataSource = (SqlDataSource)dataItem.DataSource;
sqlDataSource.ConnectionString = connectionString;
continue
;
}
}
}
}
}
}
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="ReportViewerForm1.aspx.cs" Inherits="Scratch.ReportViewerForm1" %>
<%@ Register TagPrefix="telerik" Assembly="Telerik.ReportViewer.Html5.WebForms" Namespace="Telerik.ReportViewer.Html5.WebForms" %>
<!DOCTYPE html>
<
html
xmlns
=
"http://www.w3.org/1999/xhtml"
>
<
head
runat
=
"server"
>
<
title
>Telerik HTML5 Web Forms Report Viewer Form</
title
>
<
script
src
=
"http://code.jquery.com/jquery-1.9.1.min.js"
></
script
>
<
style
>
#reportViewer1 {
position: absolute;
left: 5px;
right: 5px;
top: 5px;
bottom: 5px;
overflow: hidden;
font-family: Verdana, Arial;
}
</
style
>
</
head
>
<
body
>
<
form
runat
=
"server"
>
<
telerik:ReportViewer
ID
=
"reportViewer1"
Width
=
"1300px"
Height
=
"900px"
runat
=
"server"
>
<
ReportSource
IdentifierType
=
"UriReportSource"
Identifier
=
"Report1.trdx"
>
</
ReportSource
>
</
telerik:ReportViewer
>
</
form
>
</
body
>
</
html
>
In general, the code snippet illustrates the suggested approach. The issue you may have are:
- Where you are deserializing a report by using a UriReportSource.Uri, make sure that the path is routed correctly as it is done for the first UriReportSource;
- If reports use NavigateToReportAction, the NavigateToReportAction.ReportSource property will be using an instance of a report. This is not supported for actions and the navigation will not be available in the HTML5 Viewer.
You can replace the NavigateToReportAction with a NavigatetoUrlAction, where the URL can point to the same page but with different query string. The information from the query string can be used to update the viewer on the page.
Regards,
Stef
Telerik

TRDP files are introduced in R2 2016. They can be handled in a similar way as TRDX files - Report Packaging. Thus the code from Changing the connection string dynamically according to runtime data should be extended to cover TRDP files recognition and unpackaging.
Regards,
Stef
Telerik

Hi,
Can you pls explain what you mean by "should be extended to cover TRDP files recognition and unpackaging."? I don't see any reference to trdp or trdx in the ReportConnectionStringManager class.
Please check the UpdateReportSource method and the case handling UriReportSource. In this method you need to check the Uri for the file's extension - TRDX or TRDP, and to provide proper handling (deserializing or unpackaging). There are two methods for deserializing TRDX files or XML - DeserializeReport, but you will need to add your own for unpackaging.
Regards,
Stef
Telerik by Progress

Thanks Stef,
Quick question, I use UriReportSource for both tdrp and tdrx, it seems to work fine but is this correct?
ie. I am only editing the DeserializeReport(UriReportSource uriReportSource) overload to cater for tdrx and tdrp
It is possible to include unpackaging in the DeserializeReport method.
Regards,
Stef
Telerik by Progress

Ok thanks. Here is the code I have working in my custom resolver to resolve trdp + trdx, if anyone needs it:
Report DeserializeReport(UriReportSource uriReportSource)
{
Report report;
if
(Path.GetExtension(uriReportSource.Uri).ToLower() ==
".trdx"
)
{
var settings =
new
System.Xml.XmlReaderSettings();
settings.IgnoreWhitespace =
true
;
using
(var xmlReader = System.Xml.XmlReader.Create(uriReportSource.Uri, settings))
{
var xmlSerializer =
new
Telerik.Reporting.XmlSerialization.ReportXmlSerializer();
report = (Telerik.Reporting.Report)xmlSerializer.Deserialize(xmlReader);
}
}
else
{
//tdrp
var reportPackager =
new
ReportPackager();
using
(var sourceStream = System.IO.File.OpenRead(uriReportSource.Uri))
{
report = reportPackager.Unpackage(sourceStream);
}
}
return
report;
}
