January 1999

An Object-Oriented Reporting System

by Joshua Oskwarek

The Forum Corporation, an international training and consulting company, came to us with a need for a new reporting system. They were outgrowing the Clipper-based reporting system they had built several years ago--the old system lacked the flexibility to create the reports their clients required. We quickly realized that a traditional banded reporting system, like the one shown in Figure A, wouldn't work. Banded reporting systems depend upon reports that repeat the same types of information, record by record. While a few parts of Forum's reports were like this, most pages required complex graphs accompanied by freeform text, as shown in Figure B. In this article, we'll explain how we fulfilled this client's needs.

Figure A: Traditional banded reports assume that you want to repeat information for each record.

Figure B: Our client required reports that were much more freeform.

System goals

Because of the Forum Corporation's widely varied client needs, we knew we needed a reporting system that was highly flexible. We needed to meet these goals:

  1. The report system had to support an almost unlimited number of graphing objects.
  2. The reporting engine needed to be able to perform complex calculations on raw data.
  3. To save costs in creating new reports, the system's objects needed to be as re-usable as possible.
  4. Users needed to be able to create new reports as quickly as possible.
  5. The report design needed to be as simple as possible so that non-programmers could create new reports easily.
  6. The system needed to be fast--able to create a 30-page report in a few seconds.
  7. The system needed to import data from Forum's existing Clipper system.
The solution

In response to this need, we developed a powerful solution using Visual Basic, SQL Server, the VSView ActiveX control, and Visual Component's First Impression ActiveX charting control. We designed the reporting engine to be object-oriented--the user can place freeform graphical objects on a report page by setting properties on the object. We started with the object types listed in Table A.

Table A: Report object types

Object type Used for...
Text Fixed text appearing on the report
TextData Placing text on the report from data that's stored in a table
Graph Graphs
Line Placing lines on the report
RepeatLine Creating grids (similar to the Line object)

 

Tip: Get the controls

You can download the latest version of VideoSoft's VSView component at www.videosoft.com. To get the First Impression control, visit Visual Components' Web site at www.videosoft.com.

We coded these objects into VB class modules and stored the property settings for the report objects in a SQL Server database table. When the report runs, the property settings in the database are married with the VB class modules and each object is "painted" onto a printer object. While we could have used the built-in Visual Basic printer object, we found that the VSView ActiveX control gave us many more features, including print preview.

We encountered a potential difficulty when setting properties at runtime. Using DAO, we opened a recordset with fields containing the name of the object, its property name, and the new setting for the property. The trick was taking that information and setting the object's property with the values stored in the fields. Unfortunately, VB doesn't have an Eval() function--such a function would help greatly in setting the property value, by letting us write code like this:


Eval("Object. " & recordset!Property_Name _
  & " = " & recordset!Property_Value)

However, since this code isn't possible, we had to find another way to set the properties. We solved the problem by programming a SetProp method into each class module; Listing A shows the method's code. SetProp accepts two parameters: the property's name and its new value. Inside the class module, a Select Case sets the proper private variable that stores the value for the property. To set the property, the following code calls the SetProp method:


Object.SetProp recordset!Property_Name, _
  recordset!Property_Value

Listing A: The SetProp method


Public Sub setprop(property_name As String, _
  property_value As String)

Select Case UCase(property_name)
  Case "X1"
    mvarX1 = property_value
  Case "X2"
    mvarX2 = property_value
  Case "Y1"
    mvarY1 = property_value
  Case "Y2"
    mvarY2 = property_value
  Case "LINESTYLE"
    mvarlineStyle = property_value
  Case "LINEWIDTH"
    mvarlinewidth = property_value
  Case "LINECOLOR"
    mvarlineColor = property_value
  Case Else
    MsgBox "INVALID PROPERTY NAME FOR LINE CLASS: " 
& _
      property_name
End Select
End Sub

To print an object on the report page, each object has a printobject method. This method processes all property settings, generates the output, and then sends the output to the VSView printer control. The object's position on the page is controlled by the X1, X2, Y1, and Y2 properties; these properties belong to the class module. In addition, the VSView printer object is stored in another property of the class module: PrinterObject. Here's an example of the lineobject's printobject method:


Public Sub PrintObject()

mvarPrinterObject.PenColor = mvarlineColor
mvarPrinterObject.PenStyle = mvarlineStyle
mvarPrinterObject.PenWidth = mvarlinewidth

mvarPrinterObject.DrawLine mvarX1 * 1440, _
mvarY1 * 1440, mvarX2 * 1440, mvarY2 * 1440

End Sub
Performance tuning

We also had to tune our report system's performance. One potentially large slowdown lay in using class modules that query data--that is, text data and graph objects. Since these types of objects rely upon data, data retrieval needed to be fast. One way to increase data-retrieval speeds was to keep our queries as simple as possible. This meant eliminating all joins, and querying only one table.

To do so, we created one Report_Data table to store pre-processed data ready to be displayed on the reports. Table B lists the Report_Data table's basic fields.

Table B: Report_data table fields
Field Data
Report_Data_ID Identifies the set of result data
Report_ID Identifies which report this data is for
Report_Page Identifies the page number on which the data resides
Report_Object_Name A unique object name that matches the data with an object on the page
Number_Result The actual data for the object, if it's a numeric type
String_Result The actual data for the object, if it's a string type

With this one-table result scheme, a data pre-process was necessary. We did this using generic VB DLL functions that perform calculations upon the raw data and append records to the Report_Data table. A SQL Server table stores the DLLs necessary for each report.

We implemented another performance enhancement that greatly increased the report's speed. Each report is potentially made up of hundreds of objects throughout its pages. If each object queries the Report_Data SQL Server table, this could mean hundreds of separate remote calls to SQL Server--a very slow process.

To eliminate this problem, we cached the data locally. We created a Microsoft Access table whose structure was identical to the Report_Data table. When a new report runs, we extract all the records from the SQL Server Report_Data table that will be needed for the report, and append them to the local Access table. From that point on, all objects that require data query the local Access table instead of SQL Server. This is much faster and efficient.

Satisfying the client's needs

Let's review how the different aspects of our report system satisfy the client needs that we outlined earlier.

Supporting an almost unlimited number of graphing objects

We accomplished this goal by using class modules to define how graphical objects behave. Class objects can be programmed quickly to perform almost any task. We also found that Visual Components' First Impression ActiveX control gave us the most flexibility in creating custom graphs.

Perform complex calculations upon raw data

We also accomplished this goal by using class modules compiled into COM objects running on Microsoft Transaction Server. These components are flexible; and, once each object has been proven to perform as designed, the object can be re-used and trusted.

Making system objects as reusable as possible

To accomplish this goal, we used class modules compiled into DLLs. This approach makes re-use of objects the mainstay of the application.

Creating new reports as quickly as possible

Once report modules are created, the user can design new reports quickly by adding calls to existing class modules. Since class-module property settings are stored in SQL Server tables, a simple database application can allow new reports to be created easily.

Making the report design as simple as possible

By using the object-oriented approach, any complicated logic created for one report can be easily duplicated for others. New reports can re-use logic that has been previously written. This minimizes the amount of coding required by a programmer and allows non-programmers to construct reports using pre-existing objects.

Ensuring that the system is fast

By using the optimization techniques we've described, we achieved a very fast report engine. In addition, we made it possible for reports to be created via an Internet connection.

Importing data from the existing Clipper system

To meet this goal, we chose to use Intersolve's Clipper ODBC driver (www.intersolve.com). By doing so, we were able to implement a reliable pipeline for importing Clipper data into VB.

Conclusion

The Forum Corporation needed a reporting system that could give them a wide range of output based upon their data. Since these reports go directly to clients, they needed to be professional looking and very flexible in order to conform to their client's needs; users also needed the ability to create reports quickly. Our object-oriented approach saved costs by not requiring custom programming for every new report created, thus giving Forum a competitive edge.

Copyright © 1999, ZD Inc. All rights reserved. ZD Journals and the ZD Journals logo are trademarks of ZD Inc. Reproduction in whole or in part in any form or medium without express written permission of ZD Inc. is prohibited. All other product names and logos are trademarks or registered trademarks of their respective owners.