Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Report Builder

0.00/5 (No votes)
10 Jun 2004 6  
Article on a simple report builder.

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

Introduction

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:

  1. 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.
  2. 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.

How it Works

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
{// class body}

PrintCommand class does two things:

  1. Declare two abstract functions GetHeight and Draw needed by the PrintElement class to measure and draw itself.
  2. 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
      {// class body}
    • 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.

    How to Use it

    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:

    1. [PgNum] Represents the current page number.
    2. [PgCol] Represents the column of the current page in the two dimensional page array.
    3. [PgRow] Represents the row of the current page in the two dimensional page array.
    4. [PgNumArr] Represents the two-dimensional position of the current page in the two dimensional page array.
    5. [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.

    Conclusions

    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.

    References

    Revision History

    • Original article.
    • The spectrum of the DataSource property was greatly enlarged to directly accept some common controls.

    License

    This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

    A list of licenses authors might use can be found here