WPF Report Engine, Part 1

Unlike Windows Forms, WPF has the capability to print everything. Many people try to use this feature to create custom report engines. In this series, I will show the implementation process of a simple report engine that is robust and one can use it in practical application. Unlike other approaches that are based on FlowDocuments, my proposed approach uses the ListView control. It gives the approach the power to use all of the WPF features in the reports.

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.

By Siyamand Ayubi   Popularity  (13298 Views)