After some time I'm back to continue my WPF reporting post series. I have started with the overall architecture post , then I dived into a data preparation part. Today I will switch back to WPF and describe a report definition and a report rendering part.
Why have I decided to use flow documents for report creation? . The reason is obvious, they do support automatic pagination. The only problem is that you have to use the controls that are suitable for flow documents. In reality it means you are confined to the System.Windows.Documents namespace. Of cource you can host Grid , StackPanel, ListBox or other familiar ItemsControls inside FlowDocument using the BlockUIContainer , but you loose pagination support. These standard layouou controls do not support pagination and a ListBox will be rendered out of page boundaries.
My solution will use mainly standard Table, Paragraph and Run controls. Table as a layout control, Paragrpah and Run for displaying data. A small isuue is they do not support standard databinding and no "Itemtemplate" (like in Listview) is available.
Using Databinding for pure data display with simple formatting is something I can live without . Maybe it is even undesired to use powerfull WPF databinding for a simple task like "printf" ;-). The result will use less memory and will be faster.
The templating would be usefull. There are repeating parts like headers and footers and of course the item detail part. I need to handle the templating-like behaviour during report generation , so my report defininition contains properties for template definitions as a string with Xaml content.
Here is the report definition class with inner parts for page layout and inner groups:
public class ReportDefinition
{
string headerTemplate;
public string HeaderTemplate
{
get { return headerTemplate; }
set { headerTemplate = value; }
}
PageDefinition page;
public PageDefinition Page
{
get {
if (page == null)
page = new PageDefinition();
return page; }
set { page = value; }
}
string footerTemplate;
public string FooterTemplate
{
get { return footerTemplate; }
set { footerTemplate = value; }
}
string itemTemplate;
public string ItemTemplate
{
get { return itemTemplate; }
set { itemTemplate = value; }
}
string tableDefinition;
public string TableDefinition
{
get { return tableDefinition; }
set { tableDefinition = value; }
}
List<GroupDefinition> groups;
public List<GroupDefinition> Groups
{
get
{
if (groups == null)
groups = new List<GroupDefinition>();
return groups;
}
}
}
public class GroupDefinition
{
string headerTemplate;
public string HeaderTemplate
{
get { return headerTemplate; }
set { headerTemplate = value; }
}
string footerTemplate;
public string FooterTemplate
{
get { return footerTemplate; }
set { footerTemplate = value; }
}
bool newPageOnGroupBreak;
public bool NewPageOnGroupBreak
{
get { return newPageOnGroupBreak; }
set { newPageOnGroupBreak = value; }
}
bool resetPageNumberOnGroupBreak;
public bool ResetPageNumberOnGroupBreak
{
get { return resetPageNumberOnGroupBreak; }
set { resetPageNumberOnGroupBreak = value; }
}
}
public class PageDefinition
{
Size margin;
public Size Margin
{
get { return margin; }
set { margin = value; }
}
string headerTemplate;
public string HeaderTemplate
{
get { return headerTemplate; }
set { headerTemplate = value; }
}
double headerHeight;
public double HeaderHeight
{
get {
if (headerHeight < 0)
return 0;
return headerHeight; }
set { headerHeight = value; }
}
double footerHeight;
public double FooterHeight
{
get {
if (footerHeight<0)
return 0;
return footerHeight; }
set { footerHeight = value; }
}
string footerTemplate;
public string FooterTemplate
{
get { return footerTemplate; }
set { footerTemplate = value; }
}
}
Templates are loaded during a report generation using XamlLoader.Load. I didn't find better solution for replicating the content for each TableRow in a Table.
The report engine class is responsible for rendering the report content. During report generation the following occurs
-
Empty FlowDocument and TableRowGroup
are created.
The rows for ReportHeader are added to the TableRowGroup.
For each group the group headers, footers and data are added to the TableRowGroup.
The rows for ReportFooter are added to the TableRowGroup.
A Table is created and the TableRowGroup is added to the RowGroups collection. !!! This is important to do at the end , becasu otherwise the perfromance will decrase for bigger reports.
The Table is added to the FlowDocument.
Custom report paginator is creatd for managing the page header and footer.
XpsSerializationManager creates XpsDocument..
public class ReportEngine
{
static List<FormattedRun> displayItems;
static ParserContext xamlContext;
/// <summary>
/// Helps in namespace mapping during Xaml loading for each template
/// </summary>
public static ParserContext XamlContext
{
get
{
if (xamlContext == null))
{
xamlContext =
new ParserContext();
xamlContext.XmlnsDictionary.Add(
"",
"http://schemas.microsoft.com/winfx/2006/xaml/presentation");
xamlContext.XmlnsDictionary.Add(
"c",
"http://reportcontrols");
}
return xamlContext;
}
}
/// <summary>
/// Creates report part according to specific template , and "binds" the data
/// </summary>
internal static T createReportPart<T>(
string template,
object data)
where T:TextElement
{
T templatedItem;
MemoryStream ms =
new MemoryStream(System.Text.Encoding.UTF8.GetBytes(template));
templatedItem = (T)XamlReader.Load(ms, XamlContext);
if (data !=
null)
{
DocumentWalker dw =
new DocumentWalker();
dw.VisualVisited +=
new DocumentVisitedEventHandler(dw_VisualVisited);
displayItems =
new List<FormattedRun>();
if (
typeof(T) ==
typeof(Paragraph))
dw.TraverseParagraph(templatedItem
as Paragraph);
else if (
typeof(T) ==
typeof(Section))
{
Section sec = templatedItem
as Section;
foreach (Block b
in sec.Blocks)
{
Paragraph p =b
as Paragraph;
if (p!=
null)
dw.TraverseParagraph(p);
}
}
else if (
typeof(T) ==
typeof(TableRow))
{
TableRow tr = templatedItem
as TableRow;
foreach (TableCell tc
in tr.Cells)
dw.TraverseBlockCollection(tc.Blocks);
}
else if (
typeof(T) ==
typeof(TableRowGroup))
{
TableRowGroup trg = templatedItem
as TableRowGroup;
foreach (TableRow tr
in trg.Rows)
{
foreach (TableCell tc
in tr.Cells)
dw.TraverseBlockCollection(tc.Blocks);
}
}
foreach (FormattedRun fRun
in displayItems)
{
if (data
is DataRow)
{
DataRow row=data
as DataRow;
if (row.Table.Columns.Contains(fRun.PropertyName))
fRun.Data = row[fRun.PropertyName];
}
else if (data
is GroupData)
{
GroupData gd = data
as GroupData;
fRun.Data = gd.GetComputedValue(fRun.PropertyName);
}
}
}
return templatedItem;
}
static void dw_VisualVisited(
object sender,
object visitedObject,
bool start)
{
FormattedRun fRun = visitedObject
as FormattedRun;
if (fRun !=
null)
displayItems.Add(fRun);
}
void copyFromRowGroup(
string template, TableRowGroup trg,GroupData r)
{
TableRowGroup gf = createReportPart<TableRowGroup>(template, r);
TableRow[] rows =
new TableRow[gf.Rows.Count];
gf.Rows.CopyTo(rows, 0);
gf.Rows.Clear();
for (
int j = 0; j < rows.Length; j++)
{
TableRow tr = rows[j];
trg.Rows.Add(tr);
}
}
void addGroup(ReportDefinition rd, TableRowGroup trg, GroupData group,DataTable rData)
{
copyFromRowGroup(rd.Groups[group.Level].HeaderTemplate, trg, group);
if (group.HasNestedGroups)
{
foreach (GroupData g
in group.NestedDataGroups)
addGroup(rd,trg,g,rData);
}
else
{
int endRow=group.StartRow+group.Count;
for (
int i=group.StartRow;i<endRow;i++)
{
DataRow r = rData.Rows[i];
trg.Rows.Add(createReportPart<TableRow>(rd.ItemTemplate, r));
}
}
copyFromRowGroup(rd.Groups[group.Level].FooterTemplate,trg,group);
}
public XpsDocument CreateReport(ReportDefinition rd, ReportData rData)
{
int pageWidth = 700;
//just for testing !! get it from your printer
FlowDocument fd =
new FlowDocument();
fd.ColumnWidth = pageWidth - 100;
fd.Blocks.Add(createReportPart<Section>(rd.HeaderTemplate,rData.ReportGroup));
TableRowGroup trg =
new TableRowGroup();
for (
int i = 0; i < rData.Groups.Count;i++ )
{
addGroup(rd,trg, rData.Groups[i],rData.Rows);
}
MemoryStream tableStream =
new MemoryStream(System.Text.Encoding.UTF8.GetBytes(rd.TableDefinition));
Table table = (Table)XamlReader.Load(tableStream,XamlContext);
table.RowGroups.Add(trg);
fd.Blocks.Add(table);
fd.Blocks.Add(createReportPart<Section>(rd.FooterTemplate, rData.ReportGroup));
return ReportPaginator.CreateXpsDocument(fd,rd.Page);
}
}
For report definition I will create a descendant class from standard Run , that supports simple formatting:
/// <summary>
/// Implements support class for formating data to various text outputs. E.g. dates number formats, etc.
/// </summary>
public class FormattedRun : Run
{
object data;
public object Data
{
get { return data; }
set { data = value; formatText(); } }
string format;
public string Format
{
get { return format; }
set { format = value; }
}
string propertyName;
public string PropertyName
{
get { return propertyName; }
set { propertyName = value; }
}
}
void formatText()
{
if (data != null )
{
if (format != null) {
if (data.GetType() == typeof(DateTime))
{
DateTime dt = Convert.ToDateTime(data);
this.Text = dt.ToString(format);
return;
}
else if (data.GetType() == typeof(Decimal))
{
Decimal dt = Convert.ToDecimal(data);
this.Text = dt.ToString(format);
return;
}
}
else
this.Text = data.ToString();
}
else
Text = "";
}
}
Assume you have your report definition in one flowdocument like this 
That is a flowdocument containing ale the necessary templates for report generation. The Xaml definition is:
<FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<Section Name="pageHeader">
<Paragraph>Page Header</Paragraph>
</Section>
<Section Name="reportHeader">
<Paragraph>
ReporttHeader definition
</Paragraph>
</Section>
<Table CellSpacing="5">
<Table.Columns>
<TableColumn Width="100"/>
<TableColumn Width="100"/>
<TableColumn Width="150"/>
</Table.Columns>
<TableRowGroup Name="Group_1_Header">
<TableRow>
<TableCell ColumnSpan="3">
<Paragraph FontWeight="Bold">Product:
<c:FormattedRun PropertyName="Project"/>
</Paragraph>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Paragraph >Created</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Right">Amount</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Right">Tax</Paragraph>
</TableCell>
</TableRow>
<c:RowLine ColSpan="3"/>
</TableRowGroup>
<TableRowGroup Name="ItemDetail">
<TableRow>
<TableCell >
<Paragraph>
<c:FormattedRun PropertyName="Created" Format="dd.MM.yyyy"/>
</Paragraph></TableCell>
<TableCell >
<Paragraph TextAlignment="Right">
<c:FormattedRun PropertyName="Amount"/>
</Paragraph>
</TableCell>
<TableCell >
<Paragraph TextAlignment="Right">
<c:FormattedRun PropertyName="Tax"/>
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>
<TableRowGroup Name="Group_1_Footer">
<c:RowLine ColSpan="3"/>
<TableRow>
<TableCell ColumnSpan="3" TextAlignment="Right">
<Paragraph TextAlignment="Right">
Total:<c:FormattedRun PropertyName="Tax"/>
</Paragraph>
</TableCell>
</TableRow>
<TableRow>
<TableCell ColumnSpan="3"></TableCell>
</TableRow>
</TableRowGroup>
</Table>
<Section Name="reportFooter">
<Paragraph>Report Footer</Paragraph>
</Section>
<Section Name="pageFooter">
<Paragraph>Page Footer</Paragraph>
<Paragraph TextAlignment="Right">
Page @PageNumber from @PageCount
</Paragraph>
</Section>
</FlowDocument>
Each named section represents a template that gets replicated for each corresponding part during report generation.
In this sample for example the report header template is : "<Paragraph> ReporttHeader definition </Paragraph>"
As a result of rendering we get
this report
.
In my next post I will focus on the DocumentWalker and ReportPaginator classes.