Showing posts with label Reports. Show all posts
Showing posts with label Reports. Show all posts

Sunday, February 8, 2004

Report Printing - Class Descriptions

Introduction

Printing a document programmatically is quite involved. Using the ReportPrinting library presented here, youll be able to print reports of many sections, with very little code.

Figure 1 - Part of a sample report

The reports are comprised of text sections (such as the title "Birthdays", and the other paragraphs), grids of data from a database (more specifically, from a DataView object) and many other primitives. Since the initial version of this document, I've already exteneded the framework to handle images (from .Net Image class), boxes (similar to the CSS box implementation) and more are coming every week.

Two important classes are introduced in this article.

  • ReportDocument - a class that inherits from PrintDocument and greatly assists in printing tabular reports consisting of data from a DataTable.
  • PrintControl - a control that simplifies the process of guiding the user through the printing process.

The section on the ReportDocument includes many helper classes to define sections, columns, and text styles. The PrintControl section includes a brief description of some of the dialogs used in the printing process.

Report Document Classes

There are several classes introduced into the ReportPrinting namespace. They work together for the printing of the above report (in addition to all the .NET Framework base classes that are used). Here is a quasi-UML diagram that shows the relationship between these classes. An open triangle is generalization (i.e. it points to the super-class in the inheritance chain). The black diamonds are composite (i.e. shows that one class instantiates members of another class). The dashed-lines are dependency (i.e. it uses the class).

(note 18-Sep-03, ReportSectionText is now SectionText, ReportSectionData is now SectionTable)

Figure 2 - UML diagram of classes

ReportDocument

ReportDocument extends from PrintDocument and is customized for printing reports from one or more tables of data. A ReportDocument object is the top-level container for all the sections that make up the report. (This consists of a header, body, and footer.)

The ReportDocument's main job is printing, which occurs when the Print() method is called of the base class. The Print() method iterates through all the ReportSections making up the document, printing each one.

The strategy design pattern is employed for formatting the report. An object implementing IReportMaker may be associated with the ReportDocument. This IReportMaker object is application specific and knows how to create a report based on application state and user settings. This object would be responsible for creating sections, associating DataViews, and applying any required styles through use of the TextStyle class. It will generally use the ReportBuilder class to assist with the complexity of building a report.

ReportSection

ReportSection is an abstract class that represents a printable section of a report. There are several subclasses of ReportSection, including ReportSectionText (which represents a string of text) and ReportSectionData (which represents a printable DataView). There are also container sections (which derive from SectionContainer class, which in turn derives from ReportSection). These containers hold child ReportSection objects (also known as subsections) to be printed. Lets take a quick look at how this might work with an example.

In the sample report shown at the top of this article, there is a paragraph of text followed by a table of data. (There are actually two paragraphs of text, one of which is a heading. Plus there is a page header, but well ignore all that for now.) We would create a ReportSectionText object to print the paragraph of text and a ReportSectionData object to print the table of data. To add both of these ReportSections to the ReportDocument, we must create a container. We would create a LinearSections container to hold these two sections. This container is then made the body of the ReportDocument. When the document is printed, the section container will first print the ReportSectionText, and then below that, it will print the ReportSectionData. Simply simply printing each section below the preceding one will result in the finished report. But there are many other ways to set up these classes.

SectionContainer

This abstract class defines a container of sections. There are two types provided with the framework: LinearSections and LayeredSections.

LinearSections

The LinearSections class is a subclass of SectionContainer, which is a subclass of ReportSection. Therefore, the LinearSections can be thought of as "a printable section of a report." However, it is also a container of one or more sections.

As its name implies, it lays sections out linearly -- that is, in a row or in a column. A property named Direction specifies if this container will layout sections going down the page (typical) or across the page (not as typical).

(see

Layouts for more information about how this class works.)

LayeredSections

The LayeredSections class is also a subclass of SectionContainer, which is a subclass of ReportSection. Therefore, the LayeredSections can be thought of as "a printable section of a report." It is also a container of one or more sections.

The child sections of a LayeredSections object are all painted on top of one another (creating layers). The first section added to a LayeredSections object is the bottom layer.  Subsequent ReportSection objects added to the LayeredSections object will be shown on top of each other.

(see Layouts for more information about how this class works.)

SectionText

The SectionText prints a string to the page. Two public properties are used to setup this section. Text is used to specify the string to print. TextStyle, described later, sets the font, color, alignment and other properties for the how the text is printed.

It is interesting to note that the string specified for this section can be just one word, or many paragraphs of text.

SectionTable

The SectionTable prints a table of data.  It uses a DataView object (from the .Net System.Data namespace) as the source of data.  It then uses a series of ReportDataColumns to provide the fomatting details.  These ReportDataColumns are similar to the DataGridColumnStyle class. Table wide formatting includes setting Header, Row, and Alternating row TextStyle's, along with setting the width and margins.

ReportDataColumn

The ReportDataColumn provides the necessary information for formatting data for a column of a report. For every column to be presented within a section of data, a new ReportDataColumn object is instantiated and added to the ReportSection. At a minimum, each column describes a source field from the DataSource (that is, a column name from the DataView) and a maximum width on the page.

The ReportDataColumn can be setup with its own unique TextStyle for both header and normal rows. Therefore, each column's data can be formatted differently (e.g. an important column could be bold and red). The TextStyle is also used to set the horizontal alignment (justification).

TextStyle

The TextStyle class allows styles and fonts to be added to text selectively, allowing default styles to be used when not explicitly set. All styles (except for the static TextStyle.Normal) have another style as their "default" style. Until a property is set (like bold, underline, size, font family, etc), a TextStyle object always uses the corresponding value from its default (or parent) style.

For example, a new style can be defined using Normal as its default, but setting bold.

TextStyle paragraphStyle = new TextStyle(TextStyle.Normal);
paragraphStyle.Bold = true;

It will have all the same properties as TextStyle.Normal, except it will be bold. A later change to Normal (such as below) will have the effect of increasing the size of both styles (Normal and paragraphStyle).

TextStyle.Normal.Size += 1.0f

ReportBuilder

ReportBuilder assists with the building of a report. This class is the main interface between your code and the ReportPrinting library. In many cases, you will never explicitly create any of the above objects.  Instead, the ReportBuilder will create them for you.

To instantiate a ReportBuilder, you must provide the ReportDocument to be built. Then you can call its various Add methods to sequentially add pieces to a report document.

Example:
The following example shows the creation of a report using the ReportBuilder. The following methods would be part of a class that implements IReportMaker (this is from example1 in the sample project).

private DataView GetDataView()
{
    DataTable dt = new DataTable("People");
    dt.Columns.Add("FirstName", typeof(string));
    dt.Columns.Add("LastName", typeof(string));
    dt.Columns.Add("Birthdate", typeof(DateTime));

    dt.Rows.Add(new Object[] {"Theodore", "Roosevelt", new DateTime(1858, 11, 27)});
    dt.Rows.Add(new Object[] {"Winston ", "Churchill", new DateTime(1874, 11, 30)});
    dt.Rows.Add(new Object[] {"Pablo", "Picasso", new DateTime(1881, 10, 25)});
    dt.Rows.Add(new Object[] {"Charlie", "Chaplin", new DateTime(1889, 4, 16)});
    dt.Rows.Add(new Object[] {"Steven", "Spielberg", new DateTime(1946, 12, 18)});
    dt.Rows.Add(new Object[] {"Bart", "Simpson", new DateTime(1987, 4, 19)});
    return dt.DefaultView;
    }

public void MakeDocument(ReportDocument reportDocument)
{
    // Clear the document
    reportDocument.ClearSections();

    // create a data table and a default view from it.
    DataView dataView = this.GetDataView();

    // create a builder to help with putting the table together.
    ReportBuilder builder = new ReportBuilder(reportDocument);
    
    // Add a simple page header and footer that is the same on all pages.
    builder.AddPageHeader("Birthdays Report", String.Empty, "page %p");
    builder.AddPageFooter(String.Empty, DateTime.Now.ToLongDateString(), String.Empty);

    builder.StartLinearLayout(Direction.Vertical);

    // Add text sections
    builder.AddTextSection("Birthdays", TextStyle.Heading1);
    builder.AddTextSection("The following are various birthdays of people who "
        + "are considered important in history.");

    // Add a data section, then add columns
    builder.AddDataSection(dataView, true);
    builder.AddColumn ("LastName", "Last Name", 1.5f, false, false);
    builder.AddColumn ("FirstName", "First Name", 1.5f, false, false);
    builder.AddColumn ("Birthdate", "Birthdate", 3.0f, false, false);
    // Set the format expression to this string.
    builder.CurrentColumn.FormatExpression = "{0:D}";

    builder.FinishLinearLayout();
        
}

IReportMaker

IReportMaker is an interface used to implement the strategy design pattern. An object that implements IReportMaker can be added to a ReportDocument. When the document is about to be printed, it automatically calls the single method MakeDocument(). The above example shows an implementation of that method to print a one-page report.

For example, you could have an application that can print either detailed reports or a shorter overview. The logic to make each of these reports would be located in separate classes.  Each class would implementing the IReportMaker interface. Your print dialog could have a "Print What" combo box to allow the user to select the type of report, and use the selection in the combo box to associate the correct implementation of IReportMaker with the ReportDocument.

Print Dialogs

The print dialog guides the user through the printing process. Most applications have some options that affect what is printed and how it is printed. Most windows applications customize the standard PrintDialog, adding an additional section at the bottom for various options. There are articles on extending the standard PrintDialog using MFC, but Ive yet to find anything for .NET. If someone creates a .NET control that looks like a standard PrintDialog and could easily be added to new Forms to create a customized PrintDialog or knows of some other way to extend the functionality of the .NET PrintDialog, please let me know.

PrintControl

To make printing easy for my applications, I created this very basic control that can be dropped onto any form. It gives the user options to setup, preview, submit (ok) or cancel. Providing a preview button and a page setup button on a print dialog are not standard in the windows interface, but I wish they were. So this control provides that functionality to your print dialog. Note, you can still provide access via a File menu (File | Print Preview, File | Page Setup).

Figure 3 - PrintControl user control

The control uses the following dialogs associated with printing:

  • PrintPreviewDialog
  • PageSetupDialog
  • PrintDialog

To use the print control, place it on a form. Set the Document property to a valid PrintDocument. (it doesnt have to just be the ReportDocument described earlier). Thats it!

You can customize a few things with the following properties:

  • ShowStatusDialog - The progress of the print job is shown in a status dialog. Default is true.
  • PrintInBackground - Indicates that printing should be done in the background. Default is true.
  • Printing - This event is raised prior to printing. It allows user code to setup for printing. (This is useful for dumping data from the GUI to a helper class, for instance).

Print Dialog with PrintControl

A sample form with a PrintControl is shown below. This dialog allows a user to select tables to print from the Northwind sample database.

Figure 4 - A dialog to prompt user for print settings and give them a chance to preview and setup the page.

Revision History

1-Sep-03 : Original article posted.

18-Sep-03 : Names of some classes changed.

Report Printing - Layouts

Summary

Linear layout

Example of LinearSections

The LinearSections class is used to print a variety of sections, one below (or next to) another. In its simplest form, the linear layout looks like the figure to the right.

Each section may consume a different amount of real-estate. Some may be as wide as the page, others may not. The LinearSections container (shown in color) is a rectangle as wide as the widest section and as tall as all sub-sections combined.

This is the simplest class to use for structuring a report. Simply start a LinearLayout with the ReportBuilder class and then add ReportSections for text and data as needed.

Layered layout

The layered layout is for combing sections together into one region. This is often used for placing varying ReportSectionText objects together in one PageHeader or PageFooter. It could also be used to add a watermark to the body of a ReportDocument.

Example of LayeredSections

Column layout

As the name suggests, the column layout is used for creating columns on a page.

The column layout contains a vertical LinearSections container nested within a horizontal LinearRepeatableSections container. The vertical container has a maximum width of the column width. It renders as much of its content as possible within a column, then returns to the horizontal container. The horizontal container advances by the width of the column (plus any margin required in the middle) and repeats the call to the vertical container. This gives the parent container the name LinearRepeatableSections since it will repeat calls to a child section within a single page.

In the example shown, the LinearRepeatableSections has a special divider section assigned to it of a vertical line. This divider is printed between calls to the vertical section.

Box section

The box isn't stricly a container in the class hierarchy sense, since it doesn't inherit from SectionContainer. However, it does contain one section which can be assigned to it. And the ReportBuilder class, when it supports the SectionBox, will treat it as a container by assigning a layered or linear container to the box.

The box follows many of the properties from the W3C box model. I recommend visiting this page to learn more. http://www.w3.org/TR/REC-CSS2/box.html

(graphic from w3.org)

The margins are set the same as for any other report section. The margins are always clear. The borders are set by specifying a System.Drawing.Pen object to use for each border. The pen object specifies color and width. The padding is specified in inches, again for each side independently.

If a background brush is set (using the System.Drawing.Brushes), it will paint the entire area inside the border (including the padding).

The width and height can each be set independently, or not at all. If the width is not specified, then the width will size to that of the contents. If the width is explictly set using the Width property, that width includes content, padding, border, and margins. The width can also be set as a percentage of the parent (using the WidthPercent property). The same is true for the Height and HeightPercent properties.

An offset can be set which moves the box in relation to the parent and normal flow. This should normally be used in LayeredLayout, as the results in LinearLayout haven't been adequately tested.

Printing Reports - Tutorial

Introduction

Printing a document programmatically is quite involved. Using the ReportPrinting library presented here, youll be able to print reports with multiple sections, with very little code.

Figure 1 - Part of a sample report

This report is comprised of plain text sections (such as the title "Birthdays", and the other paragraphs) and grids of data from a database (more specifically, from a DataView object).

Step-by-Step

This page will take you through the process of using ReportPrinting library, step by step.

Create a DataView

The first step is to create a DateTable and DateView that serves as the source of data. The following code will create a table of some famous birthdays:

public static DataView GetDataView()
{
    DataTable dt = new DataTable("People");
    dt.Columns.Add("FirstName", typeof(string));
    dt.Columns.Add("LastName", typeof(string));
    dt.Columns.Add("Birthdate", typeof(DateTime));

    dt.Rows.Add(new Object[] {"Theodore", "Roosevelt",  new DateTime(1858, 11, 27)});
    dt.Rows.Add(new Object[] {"Winston",  "Churchill",  new DateTime(1874, 11, 30)});
    dt.Rows.Add(new Object[] {"Pablo",    "Picasso",    new DateTime(1881, 10, 25)});
    dt.Rows.Add(new Object[] {"Charlie",  "Chaplin",    new DateTime(1889,  4, 16)});
    dt.Rows.Add(new Object[] {"Steven",   "Spielberg",  new DateTime(1946, 12, 18)});
    dt.Rows.Add(new Object[] {"Bart",     "Simpson",    new DateTime(1987,  4, 19)});
    dt.Rows.Add(new Object[] {"Louis",    "Armstrong",  new DateTime(1901,  8,  4)});
    dt.Rows.Add(new Object[] {"Igor",     "Stravinski", new DateTime(1882,  6, 17)});
    dt.Rows.Add(new Object[] {"Bill",     "Gates",      new DateTime(1955, 10, 28)});
    dt.Rows.Add(new Object[] {"Albert",   "Einstein",   new DateTime(1879,  3, 14)});
    dt.Rows.Add(new Object[] {"Marilyn",  "Monroe",     new DateTime(1927,  6,  1)});
    dt.Rows.Add(new Object[] {"Mother",   "Teresa",     new DateTime(1910,  8, 27)});

    DataView dv = dt.DefaultView;
    return dv;
}

This function will return a DataView for a table of a dozen famous individuals and their birthdays.

Create a ReportMaker

The ReportPrinting.IReportMaker interface is used to create objects that setup a ReportDocument. The ReportMaker for this example is called SampleReportMaker1. It has one method to implement:

public void MakeDocument(ReportDocument reportDocument)
{

Let's take a look at the implementation of this method step-by-step. First, it is a good idea to reset the TextStyle class. This class provides global styles for text blocks. Since the scope of this class is application wide, it should be reset to a known state.

    TextStyle.ResetStyles();

As mentioned, the TextStyle class has several static, global styles that can be applied to different text blocks. These styles can each be customized. We'll change some fonts and colors just to show what's possible.

    // Setup the global TextStyles
    TextStyle.Heading1.FontFamily = new FontFamily("Comic Sans MS");
    TextStyle.Heading1.Brush = Brushes.DarkBlue;
    TextStyle.Heading1.SizeDelta = 5.0f;  
    TextStyle.TableHeader.Brush = Brushes.White;
    TextStyle.TableHeader.BackgroundBrush = Brushes.DarkBlue;
    TextStyle.TableRow.BackgroundBrush = Brushes.AntiqueWhite;
    TextStyle.Normal.Size = 12.0f;
    // Add some white-space to the page.  By adding a 1/10 inch margin
    // to the bottom of every line, quite a bit of white space will be added
    TextStyle.Normal.MarginBottom = 0.1f;

Using our class defined earlier, we'll get a dataview and set the default sort based on the values setup in a GUI.  (Note, this is a hack. Better encapsulation should be used to isolate the dialog, defined later, from this class.)

    // create a data table and a default view from it.
    DataView dv = GetDataView();
    // set the sort on the data view
    if (myPrintDialog.cmbOrderBy.SelectedItem != null)
    {
        string str = myPrintDialog.cmbOrderBy.SelectedItem.ToString();
        if (myPrintDialog.chkReverse.Checked)
        {
            str += " DESC";
        }
        dv.Sort = str;
    }

The next step is creating an instance of the ReportPrinting.ReportBuilder class. This object is used to simplify the somewhat complicated task of piecing together text, data, and the container sections that they go into.

    // create a builder to help with putting the table together.
    ReportBuilder builder = new ReportBuilder(reportDocument);

Creating a header and footer are quite easy with the buider class' five overloaded functions. This one creates a simple header with text on the left side (Brithdays Report) and on the right side (page #). The footer has the date centered.

    // Add a simple page header and footer that is the same on all pages.
    builder.AddPageHeader("Birthdays Report", String.Empty, "page %p");
    builder.AddPageFooter(String.Empty, DateTime.Now.ToLongDateString(), String.Empty);

Now, the real fun begins. We start a vertical, linear layout because every section from here should be added below the preceding section (see Layouts for more information).

    builder.StartLinearLayout(Direction.Vertical);

Now add two text sections. The first section added will be a heading (as defined by TextStyle.Heading1). The second section is just normal text (as defined by the TextStyle.Normal).

    // Add text sections 
    builder.AddText("Birthdays", TextStyle.Heading1); 
    builder.AddText("The following are various birthdays of people who " + 
        "are considered important in history."); 

Next, we add a section of a data table. The first line adds a data section with a visible header row. Then three column descriptors are added. These are added in the order that the columns are displayed. That is, LastName will be the first column, followed by FirstName, followed by Birthdate.

The first parameter passed to AddColumn is the name of the column in the underlying DataTable. The second parameter is the string as printed in the header row. The last three parameters describe the widths used. A max-width can be specified in inches. Optionally, the width can be auto-sized based on the header row and/or the data rows. In this case, with false being passed, no auto-sizing is being done.

    //
    Add a data section, then add columns builder.AddTable 
    (dv, true);
    builder.AddColumn ("LastName", "Last Name", 1.5f, false, false);
    builder.AddColumn ("FirstName", "First Name", 1.5f, false, false);
    builder.AddColumn ("Birthdate", "Birthdate", 3.0f, false, false);

We set a format expression for the last column added (the date column). These format expressions are identical to those used by String.Format

    // Set the format expression to this string. 
    builder.CurrentColumn.FormatExpression = "{0:D}"; 
    

And the very last thing is to finish the LinearLayout that was started earlier.

    builder.FinishLinearLayout(); 
}

Create a form for printing

There are only a handful of controls on the following form: a label, a combox box, a check box, and a usercontrol from ReportPrinting namespace called PrintControl.  This control has the four buttons you see at the bottom of the form.

Figure 2 - SampleDialog

This form also has an instance of ReportPrinting.ReportDocument class. This is a subclass of System.Drawing.Printing.PrintDocument. If you create the above form in a designer, here is the constructor required to create a new ReportDocument object.

private ReportDocument reportDocument;
public ReportPrinting.PrintControl PrintControls;
public System.Windows.Forms.ComboBox cmbOrderBy;
public System.Windows.Forms.CheckBox chkReverse;

public SamplePrintDialog1()
{
	InitializeComponent();

	this.reportDocument = new ReportDocument();
	this.PrintControls.Document = reportDocument;

	SampleReportMaker1 reportMaker = new SampleReportMaker1(this);

	this.reportDocument.ReportMaker = reportMaker;

	this.cmbOrderBy.Items.Clear();
	this.cmbOrderBy.Items.Add("FirstName");
	this.cmbOrderBy.Items.Add("LastName");
	this.cmbOrderBy.Items.Add("Birthdate");
}
			

First an instance is created with new ReportDocument(). This instance is assigned to the PrintControls.Document property. A SampleReportMaker1 object (defined above) is then created and assigned to the ReportDocument's ReportMaker property. The final bit of the constructor simply sets up the combobox.

Conclusion

That's all that is involved in printing a document.  Of course, you can use the standard PrintDialog, PrintPreview, and PageSettings dialogs on your own (without using the PrintControls usercontrol).

This entire sample can be found in the download of the ReportPrinting library, along with many more.

Printing Reports in .NET - Overview

The ReportPrinting library provides a programmatic way (i.e. no visual designer) to print reports. Features include:

  • Printing DataTables using classes similar to the DataGridColumnStyle class for formatting (also includes header and summary rows).
  • Printing paragraphs of text using style classes.
  • Customizable page headers and footers.
  • Boxes, Lines, Graphics...
  • Thorough class documentation and completely open source (written 100% in managed C#) with examples in C# and VB.NET

Releases

Download from SourceForge

I suggest compiling with the 1.1 compiler if you are using Visual Studio .Net 2003. There have been several reports of problems with text not printing correctly, or overlapping other text. This has never shown on my test machines, so I'm led to think there's some incompatibility on other machines that is fixed in .Net 1.1 (although reading through the Microsoft list of changes between versions, I don't see anything that logically makes sense).

Documentation

Background links for printing