SSRS Ad-Hoc Reporting: Dynamic RDLC with LINQ to XML
Dynamically generate SSRS reports with this approach! Learn how to use LINQ to XML and RDLC to create flexible, application-integrated reports beyond standard SSRS scenarios.
I ran into a couple of different issues when setting up advanced reporting scenarios with SQL Server Reporting Services. While SSRS runs fairly well in server-report situations, it lacks flexibility in application integration scenarios. Having to hard-code datasets for each report and hard-code fill/get methods seemed inelegant. I wanted a more robust solution — so I came up with an approach that lets me take preset report segments and load them dynamically into a report definition before passing it to the renderer.
The Approach
You start by setting up a blank RDLC with a header and footer. This keeps formatting consistent throughout the report. You can use global variables or parameters in the header and footer — most of the time these will be standard for the application and templated for the company.
Loading the RDLC
Load the RDLC from a file stream into an XDocument:
private Stream Load_FileStream()
{
Stream MasterReport = new FileStream(
Server.MapPath("~/Report/Master.rdlc"),
FileMode.Open,
FileAccess.Read);
return MasterReport;
}
Finding the Body's ReportItems Node
Using LINQ you can search the nodes for the ReportItems node in the body — this is where the guts of the report live:
private Stream PrepReport()
{
XDocument ReportDoc = XDocument.Load(XmlReader.Create(Load_FileStream()));
IEnumerable<XNode> hold = ReportDoc.DescendantNodes().InDocumentOrder();
XElement BodyItems = (XElement)hold.Where(n =>
n.GetType() != typeof(XText) &&
((XElement)n).Parent != null &&
((XElement)n).Name.LocalName.ToUpper() == "REPORTITEMS" &&
((XElement)n).Parent.Name.LocalName.ToUpper() == "BODY").SingleOrDefault();
BodyItems.Add(NewNodes(ReportDoc.Root.Name.Namespace));
return CreateMemoryStream(ReportDoc);
}
Inserting a Sub-Report Node
Use LINQ to XML to insert a new node package to add tables, sub-reports, or graphs. In this example I add a sub-report with a parameter:
private XElement NewNodes(XNamespace @namespace)
{
var holder = new XElement(@namespace + "Subreport",
new object[] {
new XAttribute("Name", "Subreport1"),
new XElement(@namespace + "Parameters",
new XElement(@namespace + "Parameter",
new object[] {
new XElement(@namespace + "Value", "=Parameter!Period.Value"),
new XAttribute("Name", "Period")
})),
new XElement(@namespace + "ReportName", "NameHere"),
new XElement(@namespace + "Top", "0.25in"),
new XElement(@namespace + "Width", "7.25in"),
new XElement(@namespace + "Left", "0.125in"),
new XElement(@namespace + "Height", "0.25in")
});
return holder;
}
The width value changes for paper orientation, and the top changes based on how many reports you insert. The height is set to a quarter inch because sub-reports auto-expand to their content.
Packaging to a Memory Stream
private Stream CreateMemoryStream(XDocument doc)
{
MemoryStream memStream = new MemoryStream();
XmlWriter writer = XmlWriter.Create(memStream);
if (writer != null)
{
doc.Save(writer);
writer.Close();
}
memStream.Position = 0;
return memStream;
}
Make sure you reset the position — if you don't, the read will start at the end and give an invalid RDLC error. Once packaged, you wire it to the ReportViewer control the same way you would with a static report:
private void GenerateReport()
{
ReportViewer1.LocalReport.LoadReportDefinition(PrepReport());
ReportViewer1.LocalReport.DisplayName = "Set what you want the export file name to be.";
ReportViewer1.LocalReport.LoadSubreportDefinition("SubReport",
new FileStream(Server.MapPath("~/Report/Subreport.rdlc"),
FileMode.Open, FileAccess.Read));
ReportViewer1.ReportError += ReportError;
ReportViewer1.LocalReport.SubreportProcessing += LocalReport_SubreportProcessing;
ReportViewer1.LocalReport.Refresh();
ReportViewer1.ZoomMode = ZoomMode.PageWidth;
ReportViewer1.Focus();
}
private void LocalReport_SubreportProcessing(object sender,
Microsoft.Reporting.WebForms.SubreportProcessingEventArgs e)
{
// Provide the data sources for your sub-reports here
e.DataSources.Add(new ReportDataSource("Something", "MethodCallHere"));
}
private void ReportError(object sender,
Microsoft.Reporting.WebForms.ReportErrorEventArgs e)
{
// Error handling and report logging here
}
This provides the basis for an ad-hoc report integration. Happy coding, and remember to code like you have to support it.
Full Code Sample
The complete _Default page class putting all of the above together:
public class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
private Stream Load_FileStream()
{
var MasterReport = new FileStream(
Server.MapPath("~/Report/Master.rdlc"),
FileMode.Open, FileAccess.Read);
return MasterReport;
}
private Stream PrepReport()
{
XDocument ReportDoc = XDocument.Load(XmlReader.Create(Load_FileStream()));
IEnumerable<XNode> hold = ReportDoc.DescendantNodes().InDocumentOrder();
var BodyItems = (XElement)hold.Where(
n =>
n.GetType() != typeof(XText) &&
((XElement)n).Parent != null &&
((XElement)n).Name.LocalName.ToUpper() == "REPORTITEMS" &&
((XElement)n).Parent.Name.LocalName.ToUpper() == "BODY").SingleOrDefault();
BodyItems.Add(NewNodes(ReportDoc.Root.Name.Namespace));
return CreateMemoryStream(ReportDoc);
}
private XElement NewNodes(XNamespace @namespace)
{
var holder = new XElement(@namespace + "Subreport",
new object[]
{
new XAttribute("Name", "Subreport1"),
new XElement(@namespace + "Parameters",
new XElement(@namespace + "Parameter",
new object[]
{
new XElement(@namespace + "Value", "=Parameter!Period.Value"),
new XAttribute("Name", "Period")
})),
new XElement(@namespace + "ReportName", "NameHere"),
new XElement(@namespace + "Top", "0.25in"),
new XElement(@namespace + "Width", "7.25in"),
new XElement(@namespace + "Left", "0.125in"),
new XElement(@namespace + "Height", "0.25in")
});
return holder;
}
private Stream CreateMemoryStream(XDocument doc)
{
var memStream = new MemoryStream();
XmlWriter writer = XmlWriter.Create(memStream);
if (writer != null)
{
doc.Save(writer);
writer.Close();
}
memStream.Position = 0;
return memStream;
}
private void GenerateReport()
{
ReportViewer1.LocalReport.LoadReportDefinition(PrepReport());
ReportViewer1.LocalReport.DisplayName = "Set what you want the export file name to be.";
ReportViewer1.LocalReport.LoadSubreportDefinition("SubReport",
new FileStream(
Server.MapPath("~/Report/Subreport.rdlc"),
FileMode.Open, FileAccess.Read));
ReportViewer1.ReportError += ReportError;
ReportViewer1.LocalReport.SubreportProcessing += LocalReport_SubreportProcessing;
ReportViewer1.LocalReport.Refresh();
ReportViewer1.ZoomMode = ZoomMode.PageWidth;
ReportViewer1.Focus();
}
private void LocalReport_SubreportProcessing(object sender,
Microsoft.Reporting.WebForms.SubreportProcessingEventArgs e)
{
// Provide the data sources for your sub-reports here
e.DataSources.Add(new ReportDataSource("Something", "MethodCallHere"));
}
private void ReportError(object sender,
Microsoft.Reporting.WebForms.ReportErrorEventArgs e)
{
// Error handling and report logging here
}
}