Part 1
Part 2
Part 3
Part 4
The Story So Far
Generating printable reports is one of the main outputs of any software system and
generally, it is one of the boring things to do for developers. Keeping in mind
the difficulty of the task, there are many report engines such as Crystal Reports,
RDLC reports and Active Reports. In the past, creating an infrastructure to target
the printing was not an easy task as one has to draw figures and texts in a PrintDocument using the Graphics objects. WPF changes the things greatly by providing the capability of printing
Visual objects. It persuades many people to setup an open source report engine.
From my point of view, using existing UI features is crucial for a report engine,
since it prevents us from implementing many costly features, but most of the
open source engines are based on FlowDocuments and they don’t use the current
WPF controls. In the previous papers in this series, I set up a report engine
that uses WPF visuals. In this paper, I will show the power of the report engine
by providing a few practical examples. All of the next samples use the AdventureWorkLT
database provided by Microsoft.
A Simple Report that Show a List of Items.
The first thing that should be done is adding the DLL of the report engine to the
project. After that, you should reference the ICP.Controls.Reporting namespace
in the window that you want to host the report.
<Window x:Class="ICP.Controls.Reporting.SimpleListDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:icp="clr-namespace:ICP.Controls.Reporting;assembly=ICP.Controls.Reporting"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="600" Width="800" Loaded="Window_Loaded">
Add a ReportViewer to the window as follows.
<icp:ReportViewer Grid.Row="0">
</icp:ReportViewer>
Next, create a Report for the Report property of the ReportViewer. ItemsSource, PageSize
and MainSection are the main properties of the Report. ItemsSource specifies
the data source that should be printed and the PageSize specifies the size of
the printed pages in pixel. Please note that each pixel is 1/96 of an inch.
<icp:ReportViewer Grid.Row="0">
<icp:ReportViewer.Report>
<icp:Report PageSize="793.247244094488,1122.70866141732"
Name="listview" ItemsSource="{Binding Customers}">
</icp:Report.MainSection>
</icp:Report>
</icp:ReportViewer.Report>
</icp:ReportViewer>
The next step is assigning a Section to the MainSection of the report. The report
engine has the following build in sections: ReportSection, GroupSection, ListViewSection
and ItemsControlSection.
ReportSection is a section that owns a child section and provides Footer, Header, PageFooter and
PageHeader for its child section.
ReportGroup is a section that inherits from the ReportSection and provides grouping functionality.
ListViewSection is a section that displays data as a ListView.
ItemsControlSection is a section that displays data as an ItemsControl.
In this example, I want to generate a simple report for the List of customers of
the AdventureWork database. I expect the report to have header and footer, so
I use the ReportSection for the MainSection and put a ListViewSection inside
the ReportSection.
<icp:Report PageSize="793.247244094488,1122.70866141732" Name="listview" ItemsSource="{Binding Customers}">
<icp:Report.MainSection>
<icp:ReportSection HeaderSize="200,30"
FooterSize="200,30">
<icp:ReportSection.Header>
<DataTemplate>
<TextBlock HorizontalAlignment="Center"
FontSize="24" Text="Adventure
Customers"></TextBlock>
</DataTemplate>
</icp:ReportSection.Header>
<icp:ReportSection.Footer>
<DataTemplate>
<StackPanel Orientation="Horizontal"
Background="LightGray">
<TextBlock
HorizontalAlignment="Center"
FontSize="24" Text="Adventure
Customers"></TextBlock>
</StackPanel>
</DataTemplate>
</icp:ReportSection.Footer>
</icp:ReportSection>
</icp:Report.MainSection>
</icp:Report>
The PageHeader and the PageFooter are DataTemplates. Please note that the HeaderSize
and FooterSize should be specified too. Again the sizes are in pixel and each
pixel is 1/96 of an inch. The next step is adding the body of the report.
<icp:ReportSection.Body>
<icp:ListViewSection>
<icp:ListViewSection.Style>
<Style>
<Setter Property="ListView.FontSize" Value="16"></Setter>
</Style>
</icp:ListViewSection.Style>
<icp:ListViewSection.Body>
<GridView>
<GridViewColumn Header="FirstName"
DisplayMemberBinding="{Binding Path=FirstName}"
Width="140" />
<GridViewColumn Header="Last Name"
DisplayMemberBinding="{Binding Path=LastName}"
Width="140" />
<GridViewColumn Header="Phone"
DisplayMemberBinding="{Binding Path=Phone}"
Width="140" />
<GridViewColumn Header="SalesPerson"
DisplayMemberBinding="{Binding Path=SalesPerson}"
Width="140" />
</GridView>
</icp:ListViewSection.Body>
</icp:ListViewSection>
</icp:ReportSection.Body>
As you may notice, a ListViewSection gets a ViewBase for its body and the ListViewSection
represents data using that ViewBase. The report has been finished now. The reader
can find the code of above sample in the ICP.Controls.Reporting.ListViewDemo
project in the attached download. Here is a snapshot of the generated report.

If you run the sample, you may notice that you can change the width of the columns
in the generated report. It is a functionality that most of the report engines
do not support!
As you may notice, the generated pages of the above sample do not have Margin. Setting
margins is important for the printed page due to nature of printers: there is
a danger that the edge parts of the report would not be printed. Well, actually
setting margins is very easy. The Section base classes in the report engine have
the Margin property and setting it is not difficult at all!
<icp:Report PageSize="793.247244094488,1122.70866141732" Name="listview" ItemsSource="{Binding Products}">
<icp:Report.MainSection>
<icp:ReportSection Margin="10" PageHeaderSize="200,30" PageFooterSize="200,30">
The printed page looks like this now.

If you noticed, there are margins around the report now.
Generate a Report that Prints Each Item in One Page.
Sometimes the data items have many columns and the user doesn’t want to see them
in a List and he/she prefers to print each item per page. Generating such reports
is very easy using ItemsControlSection. ItemsControlSection uses ItemsControl
class to render the data items. Report designers can assign custom ItemTemplate
to the ItemsControlSection using its body property. In this example, I
<icp:ReportViewer Grid.Row="0">
<icp:ReportViewer.Report>
<icp:Report PageSize="793.247244094488,1122.70866141732" Name="listview" ItemsSource="{Binding Customers}">
<icp:Report.MainSection>
<icp:ReportSection Margin="10" PageHeaderSize="200,30" PageFooterSize="200,30">
<icp:ReportSection.PageHeader>
<DataTemplate>
<Grid Background="LightGray">
<TextBlock HorizontalAlignment="Center" FontSize="24" Text="Adventure Customers"></TextBlock>
</Grid>
</DataTemplate>
</icp:ReportSection.PageHeader>
<icp:ReportSection.PageFooter>
<DataTemplate>
<Grid Background="LightGray">
<TextBlock HorizontalAlignment="Center" FontSize="24" Text="Adventure Customers"></TextBlock>
<TextBlock HorizontalAlignment="Right" FontSize="20" Text="{Binding PageNumber}"></TextBlock>
</Grid>
</DataTemplate>
</icp:ReportSection.PageFooter>
<icp:ReportSection.Body>
<icp:ItemsControlSection>
<icp:ItemsControlSection.ItemTemplate>
<DataTemplate>
<Border Width="600" Height="800" BorderThickness="1" BorderBrush="Black">
<Grid Background="LightCyan">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Label FontSize="14" Foreground="DarkBlue" Grid.Column="0" Grid.Row="0" HorizontalAlignment="Right">Customer Name:</Label>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Grid.Column="1" Grid.Row="0">
<Label FontSize="12" Content="{Binding Title}" Margin="3"></Label>
<Label FontSize="12" Content="{Binding FirstName}" Margin="3"></Label>
<Label FontSize="12" Content="{Binding MiddleName}" Margin="3"></Label>
<Label FontSize="12" Content="{Binding LastName}" Margin="3"></Label>
</StackPanel>
<Label FontSize="14" Foreground="DarkBlue" Grid.Column="0" Grid.Row="1" HorizontalAlignment="Right">Company:</Label>
<Label FontSize="12" Grid.Column="1" Grid.Row="1" HorizontalAlignment="Left" Content="{Binding CompanyName}"></Label>
<Label FontSize="14" Foreground="DarkBlue" Grid.Column="0" Grid.Row="2" HorizontalAlignment="Right">Email:</Label>
<Label FontSize="12" Grid.Column="1" Grid.Row="2" HorizontalAlignment="Left" Content="{Binding EmailAddress}"></Label>
<Label FontSize="14" Foreground="DarkBlue" Grid.Column="0" Grid.Row="3" HorizontalAlignment="Right">Phone:</Label>
<Label FontSize="12" Grid.Column="1" Grid.Row="3" HorizontalAlignment="Left" Content="{Binding Phone}"></Label>
<Label FontSize="14" Foreground="DarkBlue" Grid.Column="0" Grid.Row="4" HorizontalAlignment="Right">SalesPerson:</Label>
<Label FontSize="12" Grid.Column="1" Grid.Row="4" HorizontalAlignment="Left" Content="{Binding SalesPerson}"></Label>
<Label Grid.Column="0" Grid.Row="5" VerticalAlignment="Top" HorizontalAlignment="Right" Content="Notes:"></Label>
<RichTextBox BorderThickness="0" Grid.Column="1" Grid.Row="5" VerticalAlignment="Top" HorizontalAlignment="Left" Height="400" Width="400"></RichTextBox>
</Grid>
</Border>
</DataTemplate>
</icp:ItemsControlSection.ItemTemplate>
</icp:ItemsControlSection>
</icp:ReportSection.Body>
</icp:ReportSection>
</icp:Report.MainSection>
</icp:Report>
</icp:ReportViewer.Report>
</icp:ReportViewer>
First things first. In the above report, there are no Headers and Footers, but instead,
I put PageFooter and PageHeader. Defining them is similar to the Headers and
Footers, the only difference is, PageFooters and PageHeaders will be repeated
per page.
The other thing that needs attention in the above example is a Page Number in the
page footer. The second TextBlock of the footer DataTemplate has been binded
to the PageNumber. The question here is ”where is the PageNumber come from?”
Well, actually, in order to allow headers and footers to have access to the content
of the page, the report engine sets an instance of the ReportDataContext class
to the DataContext of the headers and footers. The ReportDataContext has the
following properties:
public class ReportDataContext
{
public object ParentDataContext
{
get
{
return parentDataContext;
}
set
{
parentDataContext = value;
}
}
private object parentDataContext;
public IList PageItems
{
get
{
return pageItems;
}
set
{
pageItems = value;
}
}
private IList pageItems;
public bool IsFirstPage
{
get
{
return isFirstPage;
}
set
{
isFirstPage = value;
}
}
private bool isFirstPage = false;
public bool IsLastPage
{
get
{
return isLastPage;
}
set
{
isLastPage = value;
}
}
private bool isLastPage = false;
public int PageNumber
{
get
{
return pageNumber;
}
set
{
pageNumber = value;
}
}
int pageNumber;
}
The above class grants the footers and headers to access the necessary data. For
example, a report developer can put data aggregators in the footers using the
PageItems property. In the next sample, there is an aggregator that uses the
above class as DataContext. Now, let back to the current report. In the Body
property of the ItemsControlSection, there is a DataTemplate that its size fits
the page size of the report. It means that the report engine will print each
item per page. The other amazing thing here is the ability to change the data
inside a report at runtime. In the above ItemTemplate, there is a TextBox for
the Notes. Users can write texts inside it in the runtime!
Here is a snapshot of the generated report.

Grouping
One of the main requirements of generating reports is the ability to group data inside
the reports. Report designers should be able to put some aggregations in the
footers or headers of the groups too. Our report engine has a nice support for
creating multi-group reports. In the following report, there are two groups;
the main group is based on the Main product category and the sub group is based
on the sub category products. Here is the data model of the report.

According to the data model, each Product has a ProductCategory and each ProductCategory
has a parent category. We want a report that partitions products in two levels;
first, by the parent categories and second by the sub categories. From report
layout point of view, the report needs the following Sections.
ReportSection with header and footer.
ReportGroup with PageHeader and PageFooter for the Main Category.
ReportGroup with Header and Footer for the Sub Category.
ListViewSection for rendering products.
Here is the XAML code of the report.
<icp:ReportViewer Grid.Row="0">
<icp:ReportViewer.Report>
<icp:Report PageSize="793.247244094488,1122.70866141732" Name="listview" ItemsSource="{Binding Products}">
<icp:Report.MainSection>
<icp:ReportSection Margin="10" PageHeaderSize="200,30" PageFooterSize="200,30">
<icp:ReportSection.PageHeader>
<DataTemplate>
<Grid Background="LightGray">
<TextBlock HorizontalAlignment="Center" FontSize="24" Text="Adventure Products"></TextBlock>
</Grid>
</DataTemplate>
</icp:ReportSection.PageHeader>
<icp:ReportSection.PageFooter>
<DataTemplate>
<Grid Background="LightGray">
<TextBlock HorizontalAlignment="Center" FontSize="24" Text="Adventure Products"></TextBlock>
<TextBlock HorizontalAlignment="Right" FontSize="20" Text="{Binding PageNumber}"></TextBlock>
</Grid>
</DataTemplate>
</icp:ReportSection.PageFooter>
<icp:ReportSection.Body>
<icp:ReportGroup PageHeaderSize="200,40" PageFooterSize="200,40">
<icp:ReportGroup.GroupDescriptions>
<PropertyGroupDescription PropertyName="MainCategory"></PropertyGroupDescription>
</icp:ReportGroup.GroupDescriptions>
<icp:ReportGroup.PageHeader>
<DataTemplate>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal" Background="LightYellow">
<Label FontSize="16" FontWeight="Bold" Foreground="DarkBlue" Content="Main Category:" Margin="3"></Label>
<Label FontSize="16" Content="{Binding PageItems, Converter={StaticResource productCategoryConverter}, ConverterParameter=Main}" Margin="3" ></Label>
</StackPanel>
</DataTemplate>
</icp:ReportGroup.PageHeader>
<icp:ReportGroup.PageFooter>
<DataTemplate>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal" Background="LightYellow">
<Label FontSize="16" FontWeight="Bold" Foreground="DarkBlue" Content="Main Category:" Margin="3"></Label>
<Label FontSize="16" Content="{Binding PageItems, Converter={StaticResource productCategoryConverter}, ConverterParameter=Main}" Margin="3" ></Label>
</StackPanel>
</DataTemplate>
</icp:ReportGroup.PageFooter>
<icp:ReportGroup.Body>
<icp:ReportGroup HeaderSize="200,40" FooterSize="200,40">
<icp:ReportGroup.GroupDescriptions>
<PropertyGroupDescription PropertyName="ProductCategoryID"></PropertyGroupDescription>
</icp:ReportGroup.GroupDescriptions>
<icp:ReportGroup.Header>
<DataTemplate>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<Label FontSize="16" FontWeight="Bold" Foreground="DarkBlue" Content="Sub Category:" Margin="3"></Label>
<Label FontSize="16" Content="{Binding PageItems, Converter={StaticResource productCategoryConverter}}" Margin="3" ></Label>
</StackPanel>
</DataTemplate>
</icp:ReportGroup.Header>
<icp:ReportGroup.Footer>
<DataTemplate>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<Label FontSize="16" FontWeight="Bold" Foreground="DarkBlue" Content="Number of Products:" Margin="3"></Label>
<Label FontSize="16" Content="{Binding PageItems, Converter={StaticResource countConverter}}" Margin="3"></Label>
</StackPanel>
</DataTemplate>
</icp:ReportGroup.Footer>
<icp:ReportGroup.Body>
<icp:ListViewSection>
<icp:ListViewSection.Style>
<Style>
<Setter Property="ListView.FontSize" Value="16"></Setter>
</Style>
</icp:ListViewSection.Style>
<icp:ListViewSection.Body>
<GridView>
<GridViewColumn Header="Name"
DisplayMemberBinding="{Binding Path=Name}"
Width="140" />
<GridViewColumn Header="Product Number"
DisplayMemberBinding="{Binding Path=ProductNumber}"
Width="140" />
<GridViewColumn Header="Color"
DisplayMemberBinding="{Binding Path=Color}"
Width="140" />
<GridViewColumn Header="Weight"
DisplayMemberBinding="{Binding Path=Weight}"
Width="140" />
</GridView>
</icp:ListViewSection.Body>
</icp:ListViewSection>
</icp:ReportGroup.Body>
</icp:ReportGroup>
</icp:ReportGroup.Body>
</icp:ReportGroup>
</icp:ReportSection.Body>
</icp:ReportSection>
</icp:Report.MainSection>
</icp:Report>
</icp:ReportViewer.Report>
</icp:ReportViewer>
Here is the snapshot of the generated report.

Aggregators
As you can see, there is one aggregation in the footer of the sub category group
which displays the count of products. There is no hard-coded aggregation in the
report engine, but instead, report designer should writer his/her converters
for aggregations. In the footers or headers, report designer has access to the
items of the group and he/she can put some aggregations using Converters. Although, it needs a little further works, but certainly it gives the report designers
more flexibility. Here is the code of the CountConverter I wrote for the above
report.
public class CountConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null || !(value is IList))
{
return Binding.DoNothing;
}
IList list = value as IList;
return list.Cast<object>().Count();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return Binding.DoNothing;
}
}
And here is the footer that uses the above converter.
<icp:ReportGroup.Footer>
<DataTemplate>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<Label FontSize="16" FontWeight="Bold" Foreground="DarkBlue" Content="Number of Products:" Margin="3"></Label>
<Label FontSize="16" Content="{Binding PageItems, Converter={StaticResource countConverter}}" Margin="3"></Label>
</StackPanel>
</DataTemplate>
</icp:ReportGroup.Footer>
Conclusion
In this paper, I showed some practical examples of using our report engine. Despite
its simplicity, the report engine has the flexibility to generate different kinds
of reports. Interested reader can download the code of the samples here. The downloaded package has a Data folder that contains the database. The database
needs SQL Server 2005 EXPRESS Edition. In case of using another database, I put
the SQL scripts of the database into the Data folder too. It is worth to mention
that you need to change the ConnectionStrings too.
And at the end, I appreciate any suggestion about improving the report engine.
It is possible to write totals and subtotals, but I am too busy now, I will write a sample for you in the weekend.
Thank you for your hint.
I solved the issue but I didn't update the source code in the server. I will do it in the weekend plus some other bug fixing.
Thank you for your hint.
I solved the issue but I didn't update the source code in the server. I will do it in the weekend plus some other bug fixing.
First, I would like to say thanks for taking the time to keep up with this project and this thread. Is there a link to the updated code allowing for celltemplates? Also, Is there a way to either include two ListViewSections in one report, or a way show constant, hard coded data, a listview, and then more constant data? I have data that I would like to show in a report, above and below the listview, but I would prefer it not be in a header/footer.
Hi, thank you for tempting me push this code forward. The source code is updated now. You can use CellTemplates inside ListView or ItemsControl. About using two ListViews, you should write a special ReportSection that inherits from the ReportSection. and inside that you can use two ListViewSections. From theory it is possible.