Monday, 15 June 2009

Dynamically generated Visual Studio Reports

Although I was able to get reports generated using the ReportViewer I soon found that I needed the layout of these reports to be dynamically generated whether they were for a single record showing each required field on its own line or a group of associated records in a table format (but again showing only the required fields).

XSL/XSLT

I soon across Dan Shipe's blog C# Shooter with both C# and XSLT code to help the dynamic generation of reports, however this was geared towards web generated reports (WebForms) and I needed it for Windows Application (WinForms).
Then I came across an answer of StackFlow by MusiGenesis (which referred to Dan's blog) and this proved to be what I needed. The code was provided me with the basis of what I needed (but still using Dan's XSL file).
From this I was able to start dynamically generating reports.

Passing simple Parameters to XSL

Although the Dataset was being Transformed with the XSL file I needed other data to be added.
I found somewhere about the XsltArgumentList class which would allow for the adding of parameters which would then be passed through the Transform process

xform.Transform(xmlDomSchema, xslArgs, writer);

Passing Array Parameters to XSL

After many attempts I figured out that the passing of an array was unlikely through the normal method and fortunately found Oleg Tkachenko's answer on the Bytes.com website.
Here Oleg detailed the requirement 'to build XmlDocument or XPathDocument, then ask it to create XPathNavigator, select nodelist of values and pass it.'
I soon implemented his code and after many attempts I began to understand what was happening and soon tweaked it so I could it a Dictionary list (thus giving me the key name of the field and the value of the column width - which would be summed and then ratioed and appended with 'cm').

public partial class frmReport_General_groups : Form
{
private DataSet ds;
private XsltArgumentList xslArgs;
private const decimal REPORT_WIDTH = 17.75m;

public frmReport_General_groups(DataSet ds, int customerID, string customerName,
string reportName, Dictionary<string,int> columnWidth)
{
InitializeComponent();
this.ds = ds;
reportName += "s";
this.Text += reportName + " report : " + customerName +
" (" + customerID.ToString().PadLeft(6, '0') + ")";
xslArgs = new XsltArgumentList();
xslArgs.AddParam("reportWidth", "", REPORT_WIDTH.ToString()+"cm");
xslArgs.AddParam("reportName", "", reportName);
xslArgs.AddParam("customerName", "", customerName);
xslArgs.AddParam("customerID", "", customerID);

//create XPathNavigator, select nodelist of values and pass it.
StringWriter sw = new StringWriter();
XmlTextWriter w = new XmlTextWriter(sw);
w.WriteStartElement("root");
int totalWidth = 0;
foreach (KeyValuePair<string,> pair in columnWidth)
{
totalWidth += pair.Value;
}
foreach (KeyValuePair<string,int> pair in columnWidth)
{
w.WriteElementString(pair.Key,
(REPORT_WIDTH * ((decimal)pair.Value / (decimal)totalWidth)).ToString("n2") +
"cm");
}
w.WriteEndElement();
w.Close();
//XmlNodeWriter should be used instead of temporary string
XPathDocument doc = new XPathDocument(new StringReader(sw.ToString()));
XPathNavigator navColumns = doc.CreateNavigator();
xslArgs.AddParam("navColumns", "", navColumns.Select("/root/*"));
}

private void frmReport_General_groups_Load(object sender, EventArgs e)
{
string AppPath = System.IO.Path.GetDirectoryName(
System.Reflection.Assembly.GetExecutingAssembly().Location);

Stream rdlc = RdlcEngine.BuildRDLCStream(
ds, "general_groups", AppPath + "\\general_groups.xslt", xslArgs);

reportViewer1.LocalReport.LoadReportDefinition(rdlc);
reportViewer1.LocalReport.DataSources.Clear();
reportViewer1.LocalReport.DataSources.Add(
new ReportDataSource(ds.DataSetName, ds.Tables[0]));
reportViewer1.RefreshReport();
}
}



public static Stream BuildRDLCStream(
DataSet data, string name, string reportXslPath, XsltArgumentList xslArgs)
{
using (MemoryStream schemaStream = new MemoryStream())
{
// save the schema to a stream
data.WriteXmlSchema(schemaStream);
schemaStream.Seek(0, SeekOrigin.Begin);

// load it into a Document and set the Name variable
XmlDocument xmlDomSchema = new XmlDocument();
xmlDomSchema.Load(schemaStream);
xmlDomSchema.DocumentElement.SetAttribute("Name", data.DataSetName);

// load the report's XSL file (that's the magic)
XslCompiledTransform xform = new XslCompiledTransform();
xform.Load(reportXslPath);

// do the transform
MemoryStream rdlcStream = new MemoryStream();
XmlWriter writer = XmlWriter.Create(rdlcStream);
if(xslArgs !=null) xform.Transform(xmlDomSchema, xslArgs, writer);
else xform.Transform(xmlDomSchema, writer);
writer.Close();
rdlcStream.Seek(0, SeekOrigin.Begin);

// send back the RDLC
return rdlcStream;
}
}

I found that I needed to remove the TableColumn template call

<xsl:apply-templates select="xs:element" mode="TableColumn">
</xsl:apply-templates>

and instead put in it's place (the variable $navColumns being a parameter that had been passed)

<xsl:for-each select="$navColumns">
<TableColumn>
<Width>
<xsl:value-of select=".">
</Width>
</TableColumn>
</xsl:for-each>

Alias field names

The names being used for the Header titles were that of the field/column names which don't have spaces and thus look very poorly formatted. Finding the SQL doesn't like aliased names with spaces I decided to format spaces (based on a Field Alias column I already had that was loaded into the class record along with all the other field details) to underscores and hyphens to astericks and then when the XSL file was 'used' it would translate these back into spaces and hyphens accordingly thus giving me a workaround solution.


Richard
Visionscape