Part 1
Part 2
Part 3
Part 4
Introduction
Most developers hate the process of generating printed reports. Although, there are
well known report engines like Active Reports, Crystal Reports and rdlc Reports,
but each report engine has its own issues. The main issue is the fact that developers
have to work with different environments to generate reports. In many situations,
the developer has to implement two interfaces; one for viewing & editing
data in the application, one for printing.
As you may know, WPF provides the ability to print almost everything. It tempts many
people to produce their own report engines. Most of these open source report
engines have been based on FlowDocuments. FlowDocument provides basic functionality
for pagination and dynamic content and it temps many people to customize it to
use as a platform for generating reports. For example, http://wpfreports.codeplex.com/
, http://www.switchonthecode.com/tutorials/wpf-printing-part-2-pagination and http://janrep.blog.codeplant.net/WPF-Multipage-Reports--Part-I.aspx are the best examples that tried to set up printing features using FlowDocuments.
Actually, I don’t like the idea of using FlowDocuments as a foundation of report
engines. FlowDocuments do not support most of the UI controls and the reports
would be restricted to use the classes of System.Windows.Documents namespace.
Most of them don’t support binding. (Apparently, Run class will support binding
in the Framework 4.0). Although one can insert UIElement controls like StackPanel and TextBlock into the
FlowDocument using BlockUIContainer and InlineUIContainer, but FlowDocument does
not support paginations on them.
The main idea behind my approach is using the current UI controls to produce reports.
Using this approach, the developers don’t have to work with different environment
to produce reports. Most of the data intensive applications display data in the
form of Lists. The ListView in WPF has the basic functionality for grouping,
sorting and custom layouts. In this paper, I will add the following features
to the ListView Control:
· Print Command.
· Custom View for the print friendly version of the ListView.
· Custom page headers and page footers for the printed version of the ListView.
· The ability to add custom aggregations in the footers of the pages.
Print Friendly ListView
In order to have the mentioned features, we add the following properties to the standard
ListView.
· PrintView: We need a special View for printing the ListView, simply because, the layouts of
the ListView in the application are different from the layout of it in the printed
version.
· PageHeader: PageHeader represents the content of the headers of the printed pages. The best datatype
for it is DataTemplage.
· PageFooter: PageFooter represents the content of the footers of the printed pages. The best
datatype for it is DataTemplage.
· PageSize: PageSize determines the size of printed pages. The PageSize property is the size
of the page in pixels, where a pixel is 1/96th of an inch. So if we want to print
to an 8.5 x 11 in. paper, we just have to multiple the dimensions by 96 and we
will get 816 x 1056 pixels.
· HeaderSize: HeaderSize specifies the height of the header.
· FooterSize: FooterSize specifies the height of the footer.
· PrintCommand: PrintCommand is an API that allows the application to print the ListView.
public class PrintableListView : ListView
{
#region Properties
public ViewBase PrintView
{
get
{
return (ViewBase)this.GetValue(PrintViewProperty);
}
set
{
this.SetValue(PrintViewProperty, value);
}
}
public static DependencyProperty PrintViewProperty =
DependencyProperty.Register("PrintView", typeof(ViewBase), typeof(PrintableListView), new PropertyMetadata());
public DataTemplate PageHeaderTemplate
{
get
{
return (DataTemplate)this.GetValue(PageHeaderTemplateProperty);
}
set
{
this.SetValue(PageHeaderTemplateProperty, value);
}
}
public static DependencyProperty PageHeaderTemplateProperty =
DependencyProperty.Register("PageHeaderTemplate", typeof(DataTemplate), typeof(PrintableListView), new PropertyMetadata());
public DataTemplate PageFooterTemplate
{
get
{
return (DataTemplate)this.GetValue(PageFooterTemplateProperty);
}
set
{
this.SetValue(PageFooterTemplateProperty, value);
}
}
public static DependencyProperty PageFooterTemplateProperty =
DependencyProperty.Register("PageFooterTemplate", typeof(DataTemplate), typeof(PrintableListView), new PropertyMetadata());
public Size PageSize
{
get
{
return (Size)this.GetValue(PageSizeProperty);
}
set
{
this.SetValue(PageSizeProperty, value);
}
}
public static DependencyProperty PageSizeProperty =
DependencyProperty.Register("PageSize", typeof(Size), typeof(PrintableListView), new PropertyMetadata());
public Size HeaderSize
{
get
{
return (Size)this.GetValue(HeaderSizeProperty);
}
set
{
this.SetValue(HeaderSizeProperty, value);
}
}
public static DependencyProperty HeaderSizeProperty =
DependencyProperty.Register("HeaderSize", typeof(Size), typeof(PrintableListView), new PropertyMetadata());
public Size FooterSize
{
get
{
return (Size)this.GetValue(FooterSizeProperty);
}
set
{
this.SetValue(FooterSizeProperty, value);
}
}
public static DependencyProperty FooterSizeProperty =
DependencyProperty.Register("FooterSize", typeof(Size), typeof(PrintableListView), new PropertyMetadata());
public ICommand PrintCommand
{
get
{
return printCommand;
}
set
{
printCommand = value;
}
}
private ICommand printCommand;
#endregion
#region Constructor
public PrintableListView()
: base()
{
PrintCommand = new DelegateCommand<object>(print);
}
#endregion
#region Private Members
private void print(object parameter)
{
PrintDialog printDialog = new PrintDialog();
if (printDialog.ShowDialog() == true)
{
if (PageSize == null)
{
PageSize = new Size((int)printDialog.PrintableAreaWidth, (int)printDialog.PrintableAreaHeight);
}
DocumentPaginatorExtention documentPaginatorExtention = new DocumentPaginatorExtention(this, new Thickness(5), PageSize);
printDialog.PrintDocument(documentPaginatorExtention, "My Data");
}
}
#endregion
}
WPF provides the PrintDialog class that can be used to print any controls. Its main
methods are PrintVisual and PrintDocument. PrintVisual can print any class that
inherits from Visual class and PrintDocument prints any class that implements
IDocumentPaginatorSource interface. At first glance, PrintVisual can solve our
problem, since a ListView is a visual. But actually it is not. PrintVisual does
not take care of number of pages, page headers and page footers. We need content
pagination mechanism. Some WPF classes have an intrinsic ability to split their
content into pages.
The FlowDocument, FixedDocument, and FixedDocumentSequence classes all advertise
this capability by implementing the IDocumentPaginatorSource interface. It means
that a PrintDialog can print the content of these classes without any problem.
But unfortunately, ListView does not implement IDocumentPaginatorSource. It means
when one tries to print a ListView, its overflowed content have not being printed.
The solution is a custom DocumentPaginator that calculates the page numbers of
the original ListView, and then creates one instance of ListView per page. Using
this technique, a ListView can be printed in multiple pages.
Custom DocumentPaginator
Our custom DocumentPaginator should implement the abstract members of the base DocumentPaginator
class. It includes the following properties and methods.
public abstract bool IsPageCountValid { get; }
public abstract int PageCount { get; }
public abstract Size PageSize { get; set; }
public abstract IDocumentPaginatorSource Source { get; }
public abstract DocumentPage GetPage(int pageNumber);
Among these properties, PageCount and GetPage need some explanations. PageCount specifies
the total number of printed pages. The simplest way to calculate its value is
creating a temporary listview and calculates its size. Having the size of it,
we can calculate the number of pages directly by dividing it by the page height.
The GetPage method returns the content of the given page number. Each page contains
a header section, a footer section and a body section. I used a Grid panel to
layout them. The Grid of each page has three rows. First row is dedicated to
header; Last row is dedicated to footer and the second Row is dedicated to the
ListView itself. Each page has its one ListView control. In the createPageListView
method, the ListView has been created for the given page number. In the createPageListView,
a new CollectionViewSource has been created for each page. It contains the items
that should be displayed in the given page. At first, the process of filling
it looks simple. The items are added to the CollectionViewSource until the ListView
of the page reaches the desired size.
// Create Itemssource
CollectionViewSource collectionViewSource = new CollectionViewSource();
list = new List<object>();
collectionViewSource.Source = list;
listview.ItemsSource = collectionViewSource.View;
// Recorrect the items inside listview
while (listview.ActualHeight < pageHeight && !source.IsCurrentAfterLast)
{
object item = source.CurrentItem;
list.Add(item);
source.MoveCurrentToNext();
collectionViewSource.InvalidateProperty(CollectionViewSource.SourceProperty);
collectionViewSource.View.Refresh();
listview.Measure(new Size());
stackPanel.Arrange(new Rect(PageMargin.Left, PageMargin.Top, PageSize.Width - PageMargin.Left - PageMargin.Right,
PageSize.Height - PageMargin.Top - PageMargin.Bottom));
}
But unfortunately, the above code is too slow. It calculates the size of the ListView
in all iterations. In order to improve its performance, I used a tricky approach.
At first phase, the number of items in the given page has been estimated. Then
it assigns the estimated items to the ListView. In the final phase, the List
has been refined to contain the precise number of items. The total code of the
method is as follows:
protected virtual ListView createPageListView(int pageNumber, out System.Collections.IList list)
{
ListView listview = new ListView();
// Setting view
ViewBase view = UIUtility.CreateDeepCopy<ViewBase>(printableListView.PrintView);
listview.View = view;
listview.UpdateLayout();
// Create Itemssource
CollectionViewSource collectionViewSource = new CollectionViewSource();
list = new List<object>();
collectionViewSource.Source = list;
listview.ItemsSource = collectionViewSource.View;
StackPanel stackPanel = new StackPanel();
stackPanel.Children.Add(listview);
// Add the estimated items to the listview
int currentPosition = source.CurrentPosition;
for (int i = 0; i < source.Count / this.PageCount; i++)
{
object item = source.CurrentItem;
list.Add(item);
source.MoveCurrentToNext();
}
// Calculate the size of listview
listview.Measure(new Size());
stackPanel.Arrange(new Rect(PageMargin.Left, PageMargin.Top, PageSize.Width - PageMargin.Left - PageMargin.Right,
PageSize.Height - PageMargin.Top - PageMargin.Bottom));
// Recorrect the items inside listview
while (listview.ActualHeight < pageHeight && !source.IsCurrentAfterLast)
{
object item = source.CurrentItem;
list.Add(item);
source.MoveCurrentToNext();
collectionViewSource.InvalidateProperty(CollectionViewSource.SourceProperty);
collectionViewSource.View.Refresh();
listview.Measure(new Size());
stackPanel.Arrange(new Rect(PageMargin.Left, PageMargin.Top, PageSize.Width - PageMargin.Left - PageMargin.Right,
PageSize.Height - PageMargin.Top - PageMargin.Bottom));
}
while (listview.ActualHeight > pageHeight && !source.IsCurrentBeforeFirst)
{
list.Remove(list[list.Count - 1]);
source.MoveCurrentToPrevious();
collectionViewSource.InvalidateProperty(CollectionViewSource.SourceProperty);
collectionViewSource.View.Refresh();
listview.Measure(new Size());
stackPanel.Arrange(new Rect(PageMargin.Left, PageMargin.Top, PageSize.Width - PageMargin.Left - PageMargin.Right,
PageSize.Height - PageMargin.Top - PageMargin.Bottom));
}
stackPanel.Children.Clear();
return listview;
}
As you may notice, I add the created ListView to a StackPanel to calculate its size.
Using this trick, the ListView has to extend its size to the required value.
The print code of the control is as follows:
private void print(object parameter)
{
PrintDialog printDialog = new PrintDialog();
if (printDialog.ShowDialog() == true)
{
System.Windows.Size pageSize = new Size((int)printDialog.PrintableAreaWidth, (int)printDialog.PrintableAreaHeight);
DocumentPaginatorExtention documentPaginatorExtention = new DocumentPaginatorExtention(this, new Thickness(5), pageSize);
printDialog.PrintDocument(documentPaginatorExtention, "My Data");
}
}
Now the ListView has the basic functionality. But it is not sufficient. One should
be able to add some custom aggregations to page footers. A simple and flexible
solution is creating footers and headers as ContentControls. The user of the
control is responsible for creating custom data templates for the control. The
responsibility of the control is providing all of the data that they need. From
my point of view, footers and headers should have access to the Items of the
page, page number and the DataContext of the original ListView. All of these
fields can be put to a custom datatype like the following class.
public class HeaderFooterDataContext
{
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 int PageNumber
{
get
{
return pageNumber;
}
set
{
pageNumber = value;
}
}
int pageNumber;
public override string ToString()
{
return pageNumber.ToString();
}
public HeaderFooterDataContext(object parentDataContext, int pageNumber, IList items)
{
this.PageNumber = pageNumber;
this.ParentDataContext = parentDataContext;
this.PageItems = items;
}
}
In the GetPage method, an instance of the HeaderFooterDataContext has been created
for each page and has been assigned to the headers and footers.
Using the Control
Using the control is really simple. The only thing that should be done is creating
an instance of the control and providing custom header, footer and PrintView
for it. In the following sample, I create a custom footer that aggregate the
values of the page based on the Name property of the items.
<Window x:Class="ICP.Controls.PrintableListView.Demo.MainWindow "
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:icp="clr-namespace:ICP.Controls.PrintableListView;assembly=ICP.Controls.PrintableListView"
xmlns:con="clr-namespace:ICP.Controls.PrintableListView.Demo"
Title="Window1" Height="600" Width="800">
<Window.Resources>
<CollectionViewSource x:Key='src'
Source="{Binding MyData}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Catalog" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
<con:ListConverter x:Key="listConverter"></con:ListConverter>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Button Grid.Row="0" Width="100" Content="Print" Command="{Binding ElementName=listview, Path=PrintCommand}"></Button>
<icp:PrintableListView HeaderSize="300,100" FooterSize="300,150" Grid.Row="1" Name="listview" ItemsSource='{Binding Source={StaticResource src}}'
BorderThickness="0">
<icp:PrintableListView.View>
<GridView>
<GridViewColumn Header="ID"
DisplayMemberBinding="{Binding Path=ID}"
Width="100" />
<GridViewColumn Header="Name"
DisplayMemberBinding="{Binding Path=Name}"
Width="140" />
<GridViewColumn Header="Price"
DisplayMemberBinding="{Binding Path=Price}"
Width="80" />
</GridView>
</icp:PrintableListView.View>
<icp:PrintableListView.PrintView>
<GridView>
<GridViewColumn Header="ID"
DisplayMemberBinding="{Binding Path=ID}"
Width="100" />
<GridViewColumn Header="Name"
DisplayMemberBinding="{Binding Path=Name}"
Width="140" />
<GridViewColumn Header="Price"
DisplayMemberBinding="{Binding Path=Price}"
Width="80" />
<GridViewColumn Header="Author"
DisplayMemberBinding="{Binding Path=Author}"
Width="80" />
</GridView>
</icp:PrintableListView.PrintView>
<icp:PrintableListView.PageHeaderTemplate>
<DataTemplate>
<Grid Margin="5" >
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" HorizontalAlignment="Center" VerticalAlignment="Center" Text="Header" Foreground="Black" FontSize="30"></TextBlock>
<StackPanel Orientation="Horizontal" Grid.Row="1">
<TextBlock FontSize="20" Text="Page Number:"></TextBlock>
<TextBlock FontSize="20" Text="{Binding PageNumber}"></TextBlock>
</StackPanel>
</Grid>
</DataTemplate>
</icp:PrintableListView.PageHeaderTemplate>
<icp:PrintableListView.PageFooterTemplate>
<DataTemplate>
<Grid Margin="5" >
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<ListBox Grid.Row="0" ItemsSource="{Binding PageItems, Converter={StaticResource listConverter}}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="5,0,5,0">
<TextBlock VerticalAlignment="Center" FontWeight="Bold" FontSize="16" Margin="2" Text="{Binding Name}"></TextBlock>
<TextBlock VerticalAlignment="Center" Margin="2" Text="Count:"></TextBlock>
<TextBlock VerticalAlignment="Center" Margin="2" Text="{Binding Count}"></TextBlock>
<TextBlock VerticalAlignment="Center" Margin="2" Text="Sum:"></TextBlock>
<TextBlock VerticalAlignment="Center" Margin="2" Text="{Binding Sum}"></TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</DataTemplate>
</icp:PrintableListView.PageFooterTemplate>
</icp:PrintableListView>
</Grid>
</Window>
Conclusion
In this article, I describe the process of creating a simple WPF report engine that
is based on the ListView control. Unlike other open source approaches, my solution
can use all of the WPF features in the reports. In the next series, I will illustrate how we can add custom grouping, print preview features to our
open source report engine.
Download the code here. We suggest you to download the latest version in the next paper.
A well thought out article. I will certainly give this more time, when I have a printing requirement of this nature.
I would not have hardcoded the Page size, but I also note that this is Part 1, so perhaps we will see a work around in Part 2.
If I was to throw in a suggestion on what could be improved I would not have architectured the PrintableListView.PrintView
Instead I would have included a PageBodyTemplate. I feel this would have given us better control over the document Body, as the first thing we would like to do is better format the grid so it is centered on the page.
This was a very refreshing to read.
__Allan
Thank you for your post and your suggestions. Currently, the page size gets its value in pixels where a pixel is 1/96th of an inch. It is better if the pagesize gets its value in inches, you are right. I will take into account your suggestion of architecture in the third version of it. Currently, I am working on adding group functionality to the report which supports footers, headers and etc.
I saw your implementation. I admit the fact that your implementation is simpler, but you hardcoded the number of items per page of the report!! It could not be a general solution.
I think I solved this issue in the latest paper. Please try using the code in the paper 4.
I think you should set a fixed width to the textblock and play with TextWrapping attached property. It is an issue that is related to the textblock rather than the report engine