Summary
This is a simple report builder component with the possibility of designing the report both at design and run-time and with a wide variety of candidates for the data source.
Contents
Although printing in .NET is easier than ever before, it's still not accessible to everyone. The goal of this component is to provide an easy, direct, and customizable solution for printing the data that you normally process in a database driven application.
ReportBuilder
doesn't target a specific control, like DataGrid
or ComboBox
, instead it's focusing on the data source that these controls use. Its architecture is based on DataView
, and it receives as data source anything from which it can extract one: DataTable
, DataView
, DataGrid
, ComboBox
, ListBox
and IReportSource
.
The demo project presents two different ways of using this component:
- Automatically create reports for controls that have an automatic print support. To check this, click on the toolbar button labeled "Print Assistant". Moving the mouse over different controls you will notice that the mouse cursor changes. If it changes to
Cursors.Hand
(I was too lazy to make a more suggestive cursor) it means that the control has an automatic print support. Click on it and see what happens next.
- Create custom reports. These reports are designed by the programmer at design-time. Click on the button labeled "Print Grid" to see a custom report of the
DataGrid
on the form. Notice that for the first column, you can't change neither the Width
nor the BackColor
. This changes from column to column.
This component wasn't seriously tested. Use it on your own risk, and please send a feedback with the found bugs.
When you open the solution for the first time, you should compile it before doing anything else.
In order to understand how this component works, you should be familiar with the printing process in .NET.
ReportBuilder
is a wrapper class that contains and exposes the main fields of two specialized classes: PrintEngine
and PaintEngine
. PrintEngine
inherits from PrintDocument
and executes tasks related with the printing process, while PaintEngine
is dedicated to the task of drawing the data from the DataSource on bitmaps. They communicate to each other through two interfaces: IPrintableClient
and IPaginationClient
.
Technically, the report is a collection of pages. A page has five constituent parts (or blocks): the Report Header, the Page Header, the Body, the Page Footer, and the Report Footer. The footers and headers are collections of print commands (Text, Picture, Date, etc.). A PrintCommand
is very suitable for printing text, lines, and other simple things, and it has always the width of the page, and a limited height (in this case, 200 pixels). It is both simple and effective, but is not a good solution for representing complicated structures which can expand on more than one page in both directions. The layout for the Body (which is very similar with the DataGrid
's layout) is provided by a PaintEngine
.
PrintEngine
A report can have more pages and their architecture might not be the same (only the first page has a Report Header and only the last one has a Report Footer, etc.). That is why PrintEngine
needs a PaginationEngine
to provide the layout for every page in the report.
Once initialized, the PaginationEngine
creates a collection of pages with complete details of how every page should be constructed. After that, the PrintEngine
iterates through the page collection of the PaginationEngine
requiring from all constituent blocks of each page (headers, footers, and the body) to give a bitmap with their graphical representation (picture) and then simply paints them on the page, one under the other. For this, every block must be able to say its height and provide a bitmap with the correct size.
For the headers and footers, the technique is the same, and they are all of PrintElement
type. For the body, the bitmap is provided by a PaintEngine
.
PaintEngine
Since printing the body of the report was too complicated to be done with print commands, a special class was totally dedicated to this task: PaintEngine
. Having as input a data source and a table style, this class is able to make a virtual image of the data very similar with a layout of a DataGrid
. But in most of the cases, this would be too big to fit on a page. So, it has a third input parameter, a rectangle, which represents the desired part from the big picture. The output, as you probably figured it out, is a bitmap. The cells area (GetGridBitmap
) is delivered separately from the columns header area (GetColumnsHeaderBitmap
). These two functions are required by the IPrintableClient
interface. This interface inherits from another one: IPaginationClient
. The last interface ensures that the implementer (PaintEngine
in this case) is able to return its virtual rectangle (imagine that you stretch a DataGrid
till it needs no more scroll bars, and all data is visible; this is the virtual rectangle) (ClientVirtualRectangle
), and that it is able to return the biggest rectangle with contiguous data (no chopped columns or rows) that is included in the required rectangle (GetPageRectangle
).
PaginationEngine
Having as input an IPaginationClient
, headers and footers of the report, and the page rectangle, this class creates a collection of Page
objects. A Page
object is meant to map a real page from the report. It contains all the information needed by the PrintEngine
to construct the pages of the report: the row and the column of the page in the page array (the report may expand on both directions), two boolean fields to indicate if the page contains any report header or footer, and the rectangle for the body of the page.
PrintElement
PrintElement
(Report Header, Report Footer, etc.) class is a collection of PrintCommand
(TextCommand
, PictureCommand
, BlankLineCommand
, etc.) which can be graphically edited at design and run-time using a special collection editor PrintElementCollEdit
. A PrintElement
calculates its height by summing the height of its constituent print commands, and creates its bitmap by iterating through the commands and asking them to paint by themselves one after the other, on a drawing surface provided by it (a bitmap).
PrintCommand
Print commands are the building units of headers and footers. By default, there are six simple print commands already created: BlankLineCommand
, ColumnsHeaderCommand
, DateCommand
, HRuleCommand
, PictureCommand
, TextCommand
. Although these might be enough for most cases, you want to add your own print command.
How to create your own print command
HRuleCommand
will serve as an example. You can find its implementation in CustomControls Project, Printing.cs file.
1. Any print command must inherit from
PrintCommand
abstract class.
public class HRuleCommand:PrintCommand
{
PrintCommand
class does two things:
- Declare two abstract functions
GetHeight
and Draw
needed by the PrintElement
class to measure and draw itself.
- Inherit from
DynamicTypeDescriptor
, and override GetLocalizedName
and GetLocalizedDescription
members in order to provide localization support for the command properties at run-time. PropertyCommands
and CategoryCommands
collections will not appear in PropertyGrid
at design time, and they will always be empty since I considered that there is no need to make use of property control for print commands. See Unleash PropertyGrid with Dynamic Properties and Globalization for more information on DynamicTypeDescriptor
.
- Override and implement the two abstract functions
GetHeight
and Draw
. public override int GetHeight(Graphics g, PrintEngine pe, int maxWidth)
{
return Width;
}
public override int Draw(Graphics g,PrintEngine pe,
Point startPoint, int maxWidth)
{
int yOffset=Width/2;
using(Pen pen= new Pen(Color,Width))
{
pen.DashStyle=DashStyle;
g.DrawLine(pen,new Point(startPoint.X ,startPoint.Y + yOffset),
new Point(startPoint.X + maxWidth, startPoint.Y + yOffset));
}
return Width;
}
At this point, you are able to use your command in code, but you won't be able to benefit from the advantages of using the special editor at design or run time. For that, there are some more steps to follow. If you are not familiarized with the techniques used to edit collections with CollectionEditor
, then take a look at this article: How to Edit and Persist Collections with CollectionEditor.
- Create a
TypeConverter
for your class. internal class HRule_Converter:ExpandableObjectConverter
{
public override bool CanConvertTo(ITypeDescriptorContext context,
Type destType)
{
if (destType == typeof(InstanceDescriptor))
{
return true;
}
return base.CanConvertTo(context, destType);
}
public override object ConvertTo(ITypeDescriptorContext context,
CultureInfo info,object value,Type destType )
{
if (destType == typeof(InstanceDescriptor))
{
return new InstanceDescriptor(typeof(
HRuleCommand).GetConstructor(new Type[]
{typeof(int), typeof(Color), typeof(DashStyle)}),
new object[]
{((HRuleCommand)value).Width,((HRuleCommand)value).Color,
((HRuleCommand)value).DashStyle},true);}
return base.ConvertTo(context,info,value,destType);
}
}
- Associate the newly created
TypeConverter
to your PrintCommand
class. [TypeConverter(typeof(HRule_Converter))]
public class HRuleCommand:PrintCommand
{
- Integrate your
PrintCommand
in the PrintElement
's editor.
ReportBuilder
uses a special collection editor PrintElementCollEditForm
to edit the print commands of the headers and footers at design and run time. It is implemented in CustomControls Project, Printing.cs file.
In order to be available from the drop down list of the Add button of the editor, you have to add the type of your class to the available types array:
protected override Type[] CreateNewItemTypes(IList coll)
{
return new Type[]{typeof(HRuleCommand),typeof(BlankLineCommand),
typeof(TextCommand), typeof(DateCommand),typeof(PictureCommand),
typeof(ColumnsHeaderCommand)};
}
In SetProperties
method, set the desired look.
protected override void SetProperties(TItem titem,
object reffObject)
{
base.SetProperties (titem, reffObject);
if(reffObject is HRuleCommand )
{
titem.Text="Horizontal Rule";
titem.ForeColor=Color.Gray;
titem.ImageIndex=2;
titem.SelectedImageIndex=2;
}
}
As you can see, it is possible to associate an image index for the command. The editor has an ImageList
property. In the constructor, I assigned a static ImageList
found in the ImageRes
class.
public PrintElementCollEditForm()
{
this.ImageList=
CustomControls.BaseClasses.ImageRes.ImageList;
}
This image list is taking its images from a ImageListStreamer
("ImageStream
"), located in ImageRes.resx resource file. To change the images for this image list, on a form, add an ImageList
, add the images that you want, go to the resource file of the form, find the place where it has serialized the "ImageSteam
" property of your ImageList
, and copy the block between <value>
and </value>
tags. After that, go to the ImageRes.resx file and replace the block between <value>
and </value>
tags of the "ImageSteam
" element with the block that you copied from your ImageList
.
Automatic Print Support
When starting a big project, it is always a good idea to leave behind some open doors. One thing that could significantly influence the flexibility of the project's structure is creating a private library for it and subclassing all the .NET standard controls that you will use in your project.
public class PToolBar:ToolBar{}
public class PButton:Button{}
Never use directly a standard control, use only the controls from your library.
- This is based on the assumption that sooner or later, you will need to change some controls, to add a new property, event etc., or simply to modify its behavior. (In the middle of the project, your boss want to have themes support, or want a better looking menu.) You modify the control in only one place, and the changes will automatically propagate all throughout the project.
- It can be very helpful to signal at design time simple problems that otherwise will propagate to run-time and will be much harder to detect. Think of a missing
ValueMember
of a ComboBox
, a DataGrid
without a TableStyle
, missing format string, etc. How to catch these errors anyway? You have here an example in PComboBoxDesigner which draws an outline around the PComboBox
at design time if this has an empty ValueMember
.
Following the above idea, the ReportBuilder project has a private library with derived controls implemented in ProjectLibrary.cs file.
Think of big data-based applications with dozens of forms, each form with many data controls as DataGrid
, ComboBox
, ListBox
, etc. The client suddenly wants some printing capabilities for some data controls, especially the DataGrid
. Normally ,you will have to modify all the existent forms, and for every data control to add and customize a ReportBuilder
component. Having a private library will give you the possibility to create an automatic print support for your project. In the ReportBuilder project, it is implemented like this: on the form, there is a Toolbar
which among other standard buttons has one dedicated to printing, labeled "Print Assistant". Clicking on this button will start a tracking mechanism that checks every control the mouse hovers for being "printable" (implements IPrintable
interface). If it is, the mouse cursor changes to Cursors.Hand
, and if clicked, disables tracking and displays the PrintSettingsDialog
. Otherwise changes to Cursors.No
. If the user still clicks, the cursor is reset and the tracking is stopped. This can be divided in two: create an interface (IPrintable
) and implement it in all controls for which we want printing support, and create the tracking mechanism on the Form
.
IPrintable
interface has three members:
- A function
GetSource
that returns a DataView
object, required by the ReportBuilder
for the DataSource
.
- A function
GetTableStyle
that return a CustomControls.ApplicationBlocks.TableStyle
object, required by the ReportBuilder
for the TableStyle
.
- A
boolean
property IsValid
indicating if the data source and the table style are valid, required by the tracking mechanism.
The implementation of this interface by the PDataGrid
, PListBox
and PComboBox
controls is straightforward and does not need more explanations.
The tracking mechanism is implemented in totality by the PForm
class. This implements the IMessageFilter
interface, and listens for the WM_MOUSEMOVE
and WM_LBUTTONDOWN
messages. Has a protected
StartTracking
member which can be called from the derived classes. It is not mandatory to use the Toolbar
button to start tracking, just call this procedure.
Custom Printing
Basically, custom printing means to assign a ReportBuilder
component for each data source you want to print. This implies more work, but gives you much more possibilities to customize the output. You have here an example that uses a ReportBuilder
component (rp
) and a Button
(btn_PrintGrid
).
As said before, this component doesn't print the control but its data source. ReportBuilder
was created to print DataView
objects. Since with this type of printing you will normally use the TableStyleEditor
to create a TableStyle
, you will need the DataSource
property to be set first. To set a DataSource
is very simple. Just click the drop down button of the DataSource
property and a list with the available sources on the form will be displayed. In order to appear on that list, an object should be one of the fallowing: DataSet
, DataTable
, DataView
, DataGrid
, ComboBox
, ListBox
or implement the IReportSource
interface. Be aware that when you change the DataSource
property, the ColumnStyles
collection of the TableStyle
might be erased if the new value targets a different table.
After the DataSource
is set, you can easily create a TableStyle
for the body of the report. For this, click on the ellipsis button of the TableStyle
property and the TableStyleEditor
will popup. This is very similar with the DataGridTableStyle
's collection editor, so it should be easy to understand how it works. You can add the columns that you want, but be aware that the user is able to choose for printing only from the columns you provide. Even if a column is added to the ColumnStyles
collection, it is visible only if the Visible
property is set to true
and has a valid MappingName
. The user can toggle a column's visibility at run-time by setting the Visible
property.
You may notice that both TableStyle
and ColumnStyles
has two strange properties under the PropertyControl
category: PropertyCommands
and CategoryCommands
. These collections are used for properties control, and allow the programmer to set at design-time the visibility and read only state at run-time for the properties of the parent class. For example, the programmer decides that the first column should be always visible with a fixed width and with a certain BackColor
. When you create a TableStyle
or ColumnStyles
, the two collections are empty, and they are filled only after you compile the project. See Unleash PropertyGrid with Dynamic Properties and Globalization for more information on this topic.
Most of the time, you will want to save as much ink as possible and to keep the report simple using only black and white. For this, the TableStyleEditor
has a drop down button with two common formatting options.
You can further customize your report by designing the headers and footers. You have a header and a footer for the report, and for the page as well. Designing these is very simple, because you have a special collection editor to assist you: PrintElementCollEdit
. You can actually see the element's (header or footer) look while you are building it. If on the bottom of the element's picture, a dotted red line and scissors appeared, this means the current window in which the element is displayed is too small to contain it. If this appears on the report while you are viewing or printing it, it means that the element is higher than the space allocated for it (which is maximum 400 pixels height for the ReportHeader
and 200 pixels height for the others, and has always the same width as the page), and you have to redesign the element.
Globalization
Globalization is a serious issue for every big application. Unfortunately, it is not always simple to implement this. To help you integrate this component in an globalized application, ProjectBuilder
has its own implementation for the globalization feature. For it, the globalization problem appears only for the two editors: PrintElementCollEdit
and TableStyleEditor
. They can be fully localized (even the properties names and descriptions from PropertyGrid
). The implementation of globalization is based on a specialized class: Dictionary
found in the CustomControls.Globalization
namespace. It has a simple mechanism that looks for the localized resource in a resx file called Dictionary.resx in the executable's directory. It chooses the localization language automatically based on the System.Globalization.CultureInfo.CurrentCulture
value. But you can force a certain language (in this case Romanian) like this:
System.Threading.Thread.CurrentThread.CurrentCulture=
new System.Globalization.CultureInfo("ro-RO");
The names of the resources in the Dictionary.resx file must be of a certain format. To create a valid resource name, to the neutral text value, you have to append the two letter ISO name of the culture and "_". For example, to localize the text of the OK button in Romanian, you have to create a resource with this name: RO_OK
. Of course, the value of the resource is at your choice. The same for the display value of a property in the PropertyGrid
(for the BackColor
property, create a resource with this name: RO_BackColor
). With the description of a property, it is slightly different. Because of the previous format, you have to add "_Descr" at the end of the resource name: RO_BackColor_Descr
. To eliminate all doubts and to see how it works, take a look at the Dictionary.resx file for some example.
Page Numbering
For this, you have to introduce certain tokens in a TextCommand
. Here are all the available tokens:
- [PgNum] Represents the current page number.
- [PgCol] Represents the column of the current page in the two dimensional page array.
- [PgRow] Represents the row of the current page in the two dimensional page array.
- [PgNumArr] Represents the two-dimensional position of the current page in the two dimensional page array.
- [TotalPgs] Represents the total number of pages.
Something like "Page number [PgNum]" will have this output on the second page: "Page number 2", and this: "Page number 3" on the third page.
This is far from being ready, and there are many parts that should be improved. This is not a final product, it is just something to get you started. I've written this article hoping that you will find it helpful, and that you will send some constructive feedback.
- Original article.
- The spectrum of the
DataSource
property was greatly enlarged to directly accept some common controls.