Creating a report
This tutorial will show you how to create a report template and generate a PDF for your data.
The ReportGenerator component allows you to create pdf files by using HTML as an intermediary step. The HTML is created by transforming XML data via an XSLT template. You provide your structured reporting data by creating a tree of objects which will be translated to the XML data for you. This tutorial will not teach you how to write XSLT templates.
The API is open for report sources other than XSLT or report target types other than PDF, but currently only XSLT to PDF is supported.
Prerequisites
Before using this feature, it is recommended to follow the instructions of the Getting started tutorial to create a WPF-Application based on the SAF.
Create a template
- Prerequisites: an existing SAF application.
- Create a new .NET library project named ReportingBase. This project will contain template parts common to all your report templates.
- Add a package reference to Sartorius.SAF.ReportGenerator.XSLT.
- Add a logo.png containing a logo for your reports. Change the build action to 'Embedded Resource'. You can add any type of web compatible image format (PNG, JPEG, GIF).
- Add an XSLT file named Base.xslt. Change the build action to 'Embedded Resource'. This XSLT serves as a template library containing common template parts (e.g. CSS).
The example below shows you how you can integrate image files into your templates. The logo.png created above will automatically be inserted Base64 encoded instead of the
placeholder {logo.png}. You can embed any file by enclosing it's file name (no path) in braces.
<?xml version="1.0" encoding="utf-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"> <xsl:output method="html" indent="yes" /> <xsl:template name="style"> <style type="text/css"> @page { width: 210 mm; height: 297 mm; } html, table, body, td { font-family: Arial; font-size: 9pt; line-height: 11.5pt } .logo { position: absolute; top: 1cm; right: 0; width: 6.3cm; height: 2cm; background: url(data:image/png;base64,{logo.png}) no-repeat; background-size: contain; } </style> <!-- add templates common to your reports --> </xsl:template> </xsl:stylesheet>
- Create a new class named ReportResourceConfiguration and implement the IReportResourceConfiguration interface.
This class will configure assemblies that contain resources additional to the assembly your report is located in (e.g. the ReportingBase assembly).
using System.Collections.Generic; using System.Reflection; using Sartorius.SAF.ReportGenerator; namespace Sartorius.SAF.Documentation.Examples.ReportGenerator { internal class ReportResourceConfiguration : IReportResourceConfiguration { public IEnumerable<Assembly> FallbackAssemblies { get { // Add any assembly that contain resources relevant to reporting. // E.g. your base reporting assembly containing base xslt files or global resource assemblies. yield return Assembly.GetExecutingAssembly(); yield return typeof(GlobalResources).Assembly; } } } }
- Create a new .NET library project named FirstReport, add a package reference to Sartorius.SAF.ReportGenerator.XSLT and add a class named 'ReportTemplate' deriving from ReportTemplateBase.
Each report must reside in it's own assembly, when using the default resource locator. Add a project reference to the ReportingBase project.
using Sartorius.SAF.ReportGenerator; namespace Sartorius.SAF.Documentation.Examples.ReportGenerator { public class ReportTemplate : ReportTemplateBase { } }
- Add a resource file to your project and add resources to it. This resources can be accessed in the XSLT by using an XSLT extension provided by the reporting engine.
- Add an XSLT file named Report.xslt. Change the build action to 'Embedded Resource'. This file will define the actual content of your report.
You can import the base.xslt or any other embedded resource you create here. Note that the path to imported files are irrelevant for the reporting engine. To use intellisense in Visual Studio
it is recommended to use the correct relative path to the file. The default resource locator of the reporting engine will only look for the name of the file and search the embedded resources for it.
<?xml version="1.0" encoding="utf-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl" xmlns:x="https://extensions.sartorius.com/localization"> <!-- Import your base xslt containing common report parts. --> <xsl:import href="../ReportGenerator/Base.xslt" /> <!-- The reporting engine will provide the cultures that are currently used --> <xsl:param name="Cultures" /> <xsl:output method="html" indent="yes" /> <xsl:template match="/*"> <html> <head> <!-- we recommend to add CSS from a central template file.--> <xsl:call-template name="style" /> </head> <body> <!-- You can access resources using the Resource method --> <!-- You can change the culture used by using --> <h1> <xsl:value-of select="x:Resource('ReportTitle')"/> </h1> <!-- You can check if there should be any translations by counting the requested cultures --> <xsl:if test="count($Cultures) > 1"> <h2> <!-- You can use translations by using the Translate method. When not specifying a culture the second culture provided by the call to CreateReportAsync is used. --> <xsl:value-of select="x:Translate('ReportTitle')"/> </h2> </xsl:if> <xsl:if test="count($Cultures) > 2"> <h3> <!-- You can also specify an index to specify which culture from the call to CreateReportAsync should be used. --> <xsl:value-of select="x:Translate('ReportTitle', 2)"/> </h3> </xsl:if> </body> </html> </xsl:template> </xsl:stylesheet>
- Add another XSLT file named Header.xslt. Change the build action to 'Embedded Resource'. This file defines the page header for your report. The naming of this file is important. You can also import other XSLT files you have added as embedded resource here. If all your reports have the same header you should move the file to your ReportBase assembly.
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
xmlns:x="https://extensions.sartorius.com/localization">
<!-- Import your base xslt containing common report parts. -->
<xsl:import href="../ReportGenerator/Base.xslt" />
<xsl:output method="html" indent="yes" />
<xsl:template match="/*">
<html>
<head>
<!-- we recommend to add CSS from a central template file.-->
<xsl:call-template name="style" />
</head>
<body>
<div class="logo"></div>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
- Add another XSLT file named Footer.xslt. Change the build action to 'Embedded Resource'. This file defines the page footer for your report.
The naming of this file is important.
You can also import other XSLT files you have added as embedded resource here.
If all your reports have the same footer you should move the file to your ReportBase assembly.
<?xml version="1.0" encoding="utf-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl" xmlns:x="https://extensions.sartorius.com/localization"> <!-- Import your base xslt containing common report parts. --> <xsl:import href="../ReportGenerator/Base.xslt" /> <xsl:output method="html" indent="yes" /> <xsl:template match="/*"> <html> <head> <!-- we recommend to add CSS from a central template file.--> <xsl:call-template name="style" /> </head> <body> <xsl:value-of select="metadata/user/name" /> <div class="page"> <!-- &p; is the current page number --> <!-- &P; is the total page count --> Page <xsl:text disable-output-escaping="yes"><![CDATA[&p;]]></xsl:text> of <xsl:text disable-output-escaping="yes"><![CDATA[&P;]]></xsl:text> </div> </body> </html> </xsl:template> </xsl:stylesheet>
Creating reports from test data
You can optionally create test reports during the compile time of your project.
- Add a folder named TestData to your project.
- Add a cs file to this folder and add some structured test data to it.
using System; using Sartorius.SAF.ReportGenerator; namespace Sartorius.SAF.Documentation.Examples.ReportGenerator { public sealed class TestReportContent : ReportContent { public TestReportContent() : base(GetData(), GetMetaData()) { } private static ReportContentNode GetData() { return new ReportContentNode("data") { {"some", "data"}, {"another", "data"} }; } private static ReportContentNode GetMetaData() { return new ReportContentNode("metadata") { GetUserData(), {"some", "metadata"}, {"reportDate", DateTime.Today.ToShortDateString()} }; } private static ReportContentNode GetUserData() { return new ReportContentNode("user") { {"firstName", "Hans"}, {"lastName", "Maulwurf"}, {"name", Environment.UserName} }; } } }
- Change the build action to ReportTestData
- Configure the ReportingBase.dll as dependency for report generation.
<ItemGroup> <ReportDependency Include="$(OutDir)ReportingBase.dll" /> </ItemGroup>
- When the report project is build, you will find a folder named reports in your output directory containing the report pdf file and intermediary xml and html files.
- You can prevent building these test reports by setting a MSBuild property named GenerateReports to false. E.g. -p:GenerateReports=false in
your build pipeline. Or as Property in your project file:
<PropertyGroup> <GenerateReports>false</GenerateReports> </PropertyGroup>
Use the template
- Import your template(s) anywhere in your code using MEF.
[ImportingConstructor] public ReportCreator([ImportMany] IEnumerable<IReportTemplate> reportTemplates) { this.reportTemplates = reportTemplates.ToArray(); }
- Create a new report instance from your template and set your report specific data by building a structured data tree using the
ReportContentNodes and create the report stream as illustrated in the following example.
var template = this.reportTemplates.Single(it => it.Id == id); var instance = template.CreateReportInstance(); // you can build up a data tree instance.Content = new ReportContent( new ReportContentNode("metaData") { {"User", Environment.UserName} }, new ReportContentNode("data") { {"DataOne", "Some Data"}, {"DataTwo", "Some more data"} } ); using (var stream = File.OpenWrite(path)) { using (var output = await instance.CreateReportAsync()) { await stream.CopyToAsync(stream); } }