ReportGroup class
ReportGroup class is designed to provide the grouping functionality. It has the following
properties:
public ObservableCollection<GroupDescription> GroupDescriptions
{
get
{
return (ObservableCollection<GroupDescription>)this.GetValue(GroupDescriptionsProperty);
}
set
{
this.SetValue(GroupDescriptionsProperty, value);
}
}
public static DependencyProperty GroupDescriptionsProperty =
DependencyProperty.Register("GroupDescriptions", typeof(ObservableCollection<GroupDescription>), typeof(ReportGroup), new PropertyMetadata());
public DataTemplate GroupFooter
{
get
{
return (DataTemplate)this.GetValue(GroupFooterProperty);
}
set
{
this.SetValue(GroupFooterProperty, value);
}
}
public static DependencyProperty GroupFooterProperty =
DependencyProperty.Register("GroupFooter", typeof(DataTemplate), typeof(ReportGroup), new PropertyMetadata());
public DataTemplate GroupHeader
{
get
{
return (DataTemplate)this.GetValue(GroupHeaderProperty);
}
set
{
this.SetValue(GroupHeaderProperty, value);
}
}
public static DependencyProperty GroupHeaderProperty =
DependencyProperty.Register("GroupHeader", typeof(DataTemplate), typeof(ReportGroup), new PropertyMetadata());
public DataTemplate GroupPageFooter
{
get
{
return (DataTemplate)this.GetValue(GroupPageFooterProperty);
}
set
{
this.SetValue(GroupPageFooterProperty, value);
}
}
public static DependencyProperty GroupPageFooterProperty =
DependencyProperty.Register("GroupPageFooter", typeof(DataTemplate), typeof(ReportGroup), new PropertyMetadata());
public DataTemplate GroupPageHeader
{
get
{
return (DataTemplate)this.GetValue(GroupPageHeaderProperty);
}
set
{
this.SetValue(GroupPageHeaderProperty, value);
}
}
public static DependencyProperty GroupPageHeaderProperty =
DependencyProperty.Register("GroupPageHeader", typeof(DataTemplate), typeof(ReportGroup), 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(ReportGroup), 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(ReportGroup), new PropertyMetadata());
public Size PageHeaderSize
{
get
{
return (Size)this.GetValue(PageHeaderSizeProperty);
}
set
{
this.SetValue(PageHeaderSizeProperty, value);
}
}
public static DependencyProperty PageHeaderSizeProperty =
DependencyProperty.Register("PageHeaderSize", typeof(Size), typeof(ReportGroup), new PropertyMetadata());
public Size PageFooterSize
{
get
{
return (Size)this.GetValue(PageFooterSizeProperty);
}
set
{
this.SetValue(PageFooterSizeProperty, value);
}
}
public static DependencyProperty PageFooterSizeProperty =
DependencyProperty.Register("PageFooterSize", typeof(Size), typeof(ReportGroup), new PropertyMetadata());
internal int groupIndex;
internal int groupPage;
The Render method is the main part of the class than needs clarification. It is used
by the DocumentPaginatorSource class to generate group layout & content per
page. Its code is as follows:
internal FrameworkElement Render(object parentDataContext, CollectionView sourceView, ViewBase view, int itemsApproximation, ref int itemIndex, Rect size, out BindingList<object> list)
{
// The index of the grid row of the ListView
int listViewRowIndex = 0;
double pageHeight = size.Height;
// The layout of the group
Grid grid = new Grid();
grid.RowDefinitions.Add(new RowDefinition());
grid.RowDefinitions[listViewRowIndex].Height = new GridLength(1, GridUnitType.Star);
CollectionViewGroup group = sourceView.Groups[groupIndex] as CollectionViewGroup;
// Add header
if (groupPage == 0 && GroupHeader != null)
{
ContentControl groupHeaderControl = new ContentControl();
groupHeaderControl.ContentTemplate = GroupHeader;
groupHeaderControl.Height = HeaderSize.Height;
pageHeight -= HeaderSize.Height;
groupHeaderControl.Content = new HeaderFooterDataContext(parentDataContext, group.Items);
RowDefinition row = new RowDefinition();
row.Height = new GridLength(HeaderSize.Height, GridUnitType.Pixel);
grid.RowDefinitions.Insert(0, row);
grid.Children.Add(groupHeaderControl);
Grid.SetRow(groupHeaderControl, 0);
listViewRowIndex++;
}
// Page header
if (GroupPageHeader != null)
{
pageHeight -= this.PageHeaderSize.Height;
}
// Page footer
if (GroupPageFooter != null)
{
pageHeight -= this.PageFooterSize.Height;
}
list = new BindingList<object>();
ListView listview = UIUtility.createListView(group.Items, UIUtility.CreateDeepCopy<ViewBase>(view), itemsApproximation, ref itemIndex, new Rect(size.Left, size.Top, size.Width, pageHeight), out list);
HeaderFooterDataContext headerFooterContent = new HeaderFooterDataContext(parentDataContext, list);
headerFooterContent.IsFirstPage = groupPage == 0;
// group page header
if (GroupPageHeader != null)
{
ContentControl groupPageHeaderControl = new ContentControl();
groupPageHeaderControl.ContentTemplate = GroupPageHeader;
groupPageHeaderControl.Height = this.PageFooterSize.Height;
pageHeight -= this.PageHeaderSize.Height;
groupPageHeaderControl.Content = headerFooterContent;
RowDefinition row = new RowDefinition();
row.Height = new GridLength(PageHeaderSize.Height, GridUnitType.Pixel);
grid.RowDefinitions.Insert(listViewRowIndex, row);
grid.Children.Add(groupPageHeaderControl);
Grid.SetRow(groupPageHeaderControl, listViewRowIndex);
listViewRowIndex++;
}
// group page footer
if (GroupPageFooter != null)
{
ContentControl groupPageFooterControl = new ContentControl();
groupPageFooterControl.ContentTemplate = GroupPageFooter;
groupPageFooterControl.Height = this.PageFooterSize.Height;
pageHeight -= this.PageHeaderSize.Height;
groupPageFooterControl.Content = headerFooterContent;
RowDefinition row = new RowDefinition();
row.Height = new GridLength(PageFooterSize.Height, GridUnitType.Pixel);
grid.RowDefinitions.Insert(listViewRowIndex + 1, row);
grid.Children.Add(groupPageFooterControl);
Grid.SetRow(groupPageFooterControl, listViewRowIndex + 1);
}
// Group footer
if (itemIndex >= group.ItemCount - 1)
{
headerFooterContent.IsLastPage = true;
if (GroupFooter != null &&
listview.ActualHeight + FooterSize.Height <= pageHeight)
{
groupPage = 0;
groupIndex++;
itemIndex = 0;
ContentControl groupFooterControl = new ContentControl();
groupFooterControl.ContentTemplate = GroupHeader;
groupFooterControl.Height = FooterSize.Height;
pageHeight -= HeaderSize.Height;
groupFooterControl.Content = new HeaderFooterDataContext(parentDataContext, group.Items);
RowDefinition row = new RowDefinition();
row.Height = new GridLength(FooterSize.Height, GridUnitType.Pixel);
grid.RowDefinitions.Add(row);
grid.Children.Add(groupFooterControl);
Grid.SetRow(groupFooterControl, grid.RowDefinitions.Count - 1);
}
else
{
list.Remove(list[list.Count - 1]);
itemIndex--;
(listview.ItemsSource as CollectionView).Refresh();
groupPage++;
}
}
else
{
groupPage++;
}
grid.Children.Add(listview);
Grid.SetRow(listview, listViewRowIndex);
return grid;
}
First things first. The first thing that needs attention is the parameters of the
method.
object parentDataContext: Well, it is the data context of the main control. Why we need this? Sometimes, the report designer needs other data
sources in the headers or footers. Using this property, he/she can access the
data of the main data context. Using this technique we don’t need to define properties
for additional data and things like that.
CollectionView sourceView: It is the collectionView that contains the Groups.
ViewBase view: It is the template of the Report Body.
int itemsApproximation: ItemApproximation specifies the average number of items in the given page. Gradually
adding items to the ListView and calculating its size is time consuming. This
property helps the method to save time.
ref int itemIndex: The index of the first item that should be displayed in this page.
Rect size: The phisical size of the Group in this page.
out BindingList<object> list: The caller of this method needs the items in the ListView to pass it to the PageFooter
and PageHeader. So the method should return it as a list.
In the first section of the method, there are some initilization and the layout of
the group which is a Grid, has been created. listViewRowIndex specifies the Grid
Row of the body/ListView. Why it is needed? Due to existance of footers and headers, the row index of the body/ListView would be changed. Insertion of all footers and
headers depend on the location of the body/ListView. So we should keep it in
a variable.
In the above section, the Header of the group would be inserted, in case that it
is the first page of the group.
The page footer and the page header of the report need the data of the ListView.
And the ListView needs the available size of the group, in case of existance
of the group page footers and group page headers. So, in the first step, the
available space for the ListView will be determined, then the ListView will be
created and finally the Group Page Headers and Group Page Footers will be added
to the group.
The last part of the method needs furthur attention. It is about adding the group
footer. A group footer should be added to the report after finishing of each
group. But what would happen if the current group has been finished and on the
other hand, The ListView used the whole available space in the page. In such
case, the last item of the ListView will be removed from the List and the group
will be extended by one page and its Footer will be added to the last page.
Using the Code
Using the code is simple and straight-forward. It just needs defining the Group,
Footers, Headers and View content. Here is the code of the Demo project in the
attached code.
<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="Name" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
<con:ListConverter x:Key="listConverter"></con:ListConverter>
<con:NameConverter x:Key="nameConverter"></con:NameConverter>
<con:SumConverter x:Key="sumConverter"></con:SumConverter>
<con:CountConverter x:Key="countConverter"></con:CountConverter>
<con:BooleanToVisibilityConverter x:Key="booleanToVisibilityConverter"></con:BooleanToVisibilityConverter>
</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 PageSize="793.247244094488,1122.70866141732" HeaderSize="300,100" FooterSize="300,180" Grid.Row="1" Name="listview" ItemsSource='{Binding Source={StaticResource src}}'
BorderThickness="0">
<icp:PrintableListView.ReportBody>
<GridView>
<GridViewColumn Header="ID"
DisplayMemberBinding="{Binding Path=ID}"
Width="100" />
<GridViewColumn Header="Name"
DisplayMemberBinding="{Binding Path=Name}"
Width="240" />
<GridViewColumn Header="Price"
DisplayMemberBinding="{Binding Path=Price}"
Width="200" />
</GridView>
</icp:PrintableListView.ReportBody>
<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="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" HorizontalAlignment="Center" VerticalAlignment="Center" Text="Footer" Foreground="Black" FontSize="20"></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.PageFooterTemplate>
<icp:PrintableListView.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Margin" Value="0,0,0,5"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Expander IsExpanded="True" BorderBrush="#FFA4B97F"
BorderThickness="0,0,0,1">
<Expander.Header>
<DockPanel>
<TextBlock FontWeight="Bold" Text="{Binding Path=Name}"
Margin="5,0,0,0" Width="100"/>
</DockPanel>
</Expander.Header>
<Expander.Content>
<ItemsPresenter></ItemsPresenter>
</Expander.Content>
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</icp:PrintableListView.GroupStyle>
<icp:PrintableListView.Group>
<icp:ReportGroup FooterSize="30,30" HeaderSize="30,30" PageHeaderSize="30,30" PageFooterSize="30,30">
<icp:ReportGroup.GroupHeader>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="5,0,5,0">
<TextBlock VerticalAlignment="Center" FontWeight="Bold" FontSize="16" Margin="2" Text="{Binding PageItems, Converter={StaticResource nameConverter}}"></TextBlock>
<TextBlock VerticalAlignment="Center" Margin="2" Text="Count:"></TextBlock>
<TextBlock VerticalAlignment="Center" Margin="2" Text="{Binding PageItems, Converter={StaticResource countConverter}}"></TextBlock>
<TextBlock VerticalAlignment="Center" Margin="2" Text="Sum:"></TextBlock>
<TextBlock VerticalAlignment="Center" Margin="2" Text="{Binding PageItems, Converter={StaticResource sumConverter}}"></TextBlock>
</StackPanel>
</DataTemplate>
</icp:ReportGroup.GroupHeader>
<icp:ReportGroup.GroupFooter>
<DataTemplate>
<TextBlock Text="Group Footer"></TextBlock>
</DataTemplate>
</icp:ReportGroup.GroupFooter>
<icp:ReportGroup.GroupPageFooter>
<DataTemplate>
<StackPanel Visibility="{Binding IsLastPage, Converter={StaticResource booleanToVisibilityConverter}, ConverterParameter='false'}" Orientation="Horizontal">
<TextBlock FontWeight="Bold" Text="Name:" Margin="3"></TextBlock>
<TextBlock Text="{Binding PageItems, Converter={StaticResource nameConverter}}" Margin="3"></TextBlock>
</StackPanel>
</DataTemplate>
</icp:ReportGroup.GroupPageFooter>
<icp:ReportGroup.GroupPageHeader>
<DataTemplate>
<StackPanel Visibility="{Binding IsFirstPage, Converter={StaticResource booleanToVisibilityConverter}, ConverterParameter='false'}" Orientation="Horizontal">
<TextBlock FontWeight="Bold" Text="Name:" Margin="3"></TextBlock>
<TextBlock Text="{Binding PageItems, Converter={StaticResource nameConverter}}" Margin="3"></TextBlock>
</StackPanel>
</DataTemplate>
</icp:ReportGroup.GroupPageHeader>
<icp:ReportGroup.GroupDescriptions>
<PropertyGroupDescription PropertyName="Name" />
</icp:ReportGroup.GroupDescriptions>
</icp:ReportGroup>
</icp:PrintableListView.Group>
<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>
</Grid>
</Window>
Future Works
In this paper I extended the report engine to support grouping, but the report engine
still suffers from many deficiencies. First of all, its ReportGroup needs SubGroup
property, so that report designer could define countless groups. Secondly, the
Page Layouts and Group Layouts are hardcoded. Report designer should be able
to provide custom page and group layout. Finally, it needs a report viewer. In
the next version, I will target these issues.
You can downlaod the code here