Introduction
This is the first part of a series of articles about Inka, an open source reporting and printing engine for the .NET platform. This part is about getting started with Inka, and preparing and printing a basic report using your custom objects.
Background
Most reporting engines follow the database-oriented approach to printing. Typically, you print data that is fetched from a database, sorted, formatted, grouped, or shaped in some other way in order to become more usable. In the .NET world, you can use data coming from a dataset, but the basic idea is the same. You take a relational database (or its snapshot), and you move your data straight to the presentation layer. Although it might seem convenient from the dran-n-drop perspective, this approach has several unpleasant side effects:
- Instead of being just a persistence tool, the database becomes an integral part of the system.
- While your application design is (hopefully) object-oriented, your presentation is database-oriented, so you can't abstract away from the persistence implementation details.
- Any business logic that is involved in the presentation process should be either transformed into SQL or implemented in the presentation layer.
- Even when a report engine has an option to display business objects, the design of the system, being initially engineered as a database-centric engine, requires an extra effort to adapt your objects. For example, it might be required to have an extra «foreing key» property, or your classes have to implement a certain interface.
This might be an acceptable way of doing things for a Microsoft Access report, or even for a .NET hobbyist, but violating such core design principles is not an option for any serious project. As I couldn't find any open source project for printing, I decided to write my own.
Inka is different. Inka doesn't care where you get your data from: a database, a Web service, or created manually. What she cares about is your domain structure: objects, properties, collections, and relationships.
You can download Inka from the SourceForge site.
Getting Started
To print your data:
- Prepare the layout programmatically. You can create an
Inka.Report
object or (better) inherit from this class. - Assign one or several data sources to the sections.
- Create a new
Inka.WinForms.ReportPrinter
object using the prepared Report
object. - Call its
Print
method.
Let's print a HelloWorld string
.
Creating a Report
Each report should have one or more sections, each having one or more elements, including other sections. So, in order to display a string
, which corresponds to a LabelElement
, we have to create at least one section, add it to our report, and add a label to it:
Public Class HelloReport
Inherits Report
Dim mainSection As New Section
Public Sub New()
mainSection.AddElement(New LabelElement With {.Text = "Hello world"})
Me.Sections.Add(mainSection)
End Sub
End Class
Obviously, this report doesn't need any data sources.
Printing the Report
It would be tempting to just add a Print
method to the Report
class, or even inherit from System.Drawing.Printing.PrintDocument
, as it is recommended in most tutorials. However, it would introduce an unnecessary dependency, at the same time making it harder to test and extend. What I really wanted is to make the Report
class merely a structure that holds sections, services, and other vital data. In other words, it should be passive.
So, I added another class whose sole responsibility is printing reports. The code required to print a report is this:
Dim report As New HelloReport
Dim printer As New Inka.WinForms.ReportPrinter(report)
printer.Print()
You might consider adding a Print
method to the Report
class as an extension method.
Adding Some Data
Now, let's modify our requirements. Suppose the text should say «Hello name», where name should be set at runtime. The most straightforward way of achieving this is to set the DataSource
property of the container section to an appropriate object, and use a DataElement
to display the data. Let's review these steps in more detail.
The most obvious way would be to set the DataSource
property to the string
that contains the name we need. This approach works with other objects, but fails with string
s. Why? Remember that String
is IEnumerable
, so the layout engine will produce several sections. For example, if the name is «Bob», we'll have three sections containing «Hello B», «Hello o», «Hello b». So, we have two choices: either encapsulate the name in a custom object, or create a string array of one element.
The DataElement
is the base class for all elements displaying data bound text. Its DataObject
property is the object used in calculating the actually printed text. The Text
property serves as the formatting string. So, suppose we have an object whose Name
property is the name we need, then we should set the Text
property of our element to «Hello [Name]». Note that the property name should be put in square brackets. You can also use several fields in a single element, like «Hello [Name] [LastName]».
If, however, we decided to use a string array for the data source, we should have set the Text
to «Hello []». Here the square brackets without a field name indicate that we should use the object itself (more precisely, its ToString
method) rather than its property.
Putting it all together, we have:
Public Class HelloReport
Inherits Report
Dim mainSection As New Section
Public Sub New()
mainSection.AddElement(New DataElement With {.Text = "Hello [Name]"})
Me.Sections.Add(mainSection)
End Sub
Sub SetData(ByVal data As Object)
mainSection.DataSource = data
End Sub
End Class
Usage:
Dim report As New HelloReport
report.SetData(New With {.Name = "Bob"})
Dim printer As New Inka.WinForms.ReportPrinter(report)
printer.Print()
Finally, let's see a more realistic example, in which we have several «rows» of data. We'll be using a list of custom objects as our data source:
Dim dataSource() As Object = {New With {.Name = "Bob"}, _
New With {.Name = "Fyodor"}, _
New With {.Name = "Abdullah"}}
report.SetData(dataSource)
We don't make any modifications to the report source. However, when we print our report, we notice that the rows are superimposed. So, we set the Size
property for the section:
Dim mainSection As New Section With _
{.Size = New Utils.Rectangle(0, 20), _
.KeepTogether = Section.KeepTogetherType.Detail}
The other parameter influences the way the size of the section is calculated. Note that the width is not relevant for a section. In future releases, the height will be calculated automatically (with a possibility of manual adjustment).
The sample code included in the download contains the report displaying our custom objects, and the code required to preview it. I decided to save you some paper, so the actual printing code is commented out.
Conclusion
We have seen the most basic operations one expects from a reporting engine: displaying static text and data rows. In the next part, I'll show you how to implement different kinds of grouping, add aggregate functions, and solve paging issues.