WPF Report Engine, Part 4

In the previous papers of this series, I implemented an open source WPF report engine that gets benefits from the current WPF controls. In this paper, I show the power of the implemented report engine in action.

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.

By Siyamand Ayubi   Popularity  (10116 Views)