ReportViewer Control Binds to Multiple Generic Collections

 

Introduction 

Microsoft has gone to great lengths to make it easy, less code, with more drag and drop, to bind Datasets to DataGridViews and ReportViewer controls. What if you use strongly typed collections versus Datasets though? And what if you want one ReportViewer Control to preview and print all your application’s reports versus using one ReportViewer control for each report of the 200 your Accounting system will use? This article specifically target Winforms aplication, but the crucial ReportDataSource class exists in the System.Webforms namespace so this idea should work very similar in web applications though I haven't had time to test this yet.

ReportViewer Source    

How to instantiate a Collection full of data

Visual Studio knows how datasets are populated, but may not know as much about your favorite ORMapper Collection type. If you create a new Data Source, but choose Data Source Type Object versus Database, at runtime the ReportViewer Control knows how to create a default constructor instance of that object which might be a collection especially since in reports we tend to often report many rows of data. Your collection may have a complex method for instantiation of filtered and sorted collection of some type of object. If you subclass that collection with a collection whose default constructor knows how to fill the contents you want and then bind to that instance, the ReportViewer displays it fine, but you may find yourself subclassing a lot of types that way.

Another way is to tell the ReportViewer the proper type of collection so that the proper set of data fields can be dragged onto the report form and then just before runtime, switch that empty instance of the collection out with a properly filtered and sorted collection with code like this:

    Me.BindingSource1.DataSource = myDatasource

    Me.ReportViewer1.RefreshReport()


This assumes your ReportViewer1 Control has a System.Windows.Forms.BindingSource Control named BindingSource1 associated with it where myDatasource was instantiated like this in LLBLGen Pro the popular ORMapper.

Dim myDatasource as New CustomerCollection
Dim typeFilter as ipredicate = CustomerFields.Type = 1
Dim nameSort as isortexpression = New SortExpression
nameSort.add(sortclausefactory.create(CustomerFieldIndex.Name,Ascending))

Your collection’s instantiation would differ.

When I first tried this, no fields of interest showed up in my new Data Source for dragging onto the Report surface. This is when I learned the value of using Generic collections with code like this:


(this collection had every property that my report required in one file so no relation needed to be supported, parent properties at top for header and child properties at bottom which will go into a table of rows in the report)

 

Code Listing #1

Imports system.collections.generic

 

Public Class PoItems : Inherits List(Of PoItem)
‘ from the above line the Data Source gets drag/drop with the fields below

End Class

Public Class PoItem

  ' Parent properties

  Dim _poID As Integer

  Dim _vendor As String

  Dim _vAddr1 As String

  Dim _vAddr2 As String

  Dim _vcsz As String

  Dim _shipVia As String

  Dim _shipName As String

  Dim _shipAddr1 As String

  Dim _shipAddr2 As String

  Dim _scsz As String

  Dim _orderDate As DateTime

  Dim _received As Boolean

 

  ' Child properties

  Dim _prodNo As String

  Dim _description As String

  Dim _quantity As Integer

  Dim _rate As Decimal

  Dim _retail As Decimal

 

  Public Property POID() As Integer

    Get

      Return _poID

    End Get

    Set(ByVal value As Integer)

      _poID = value

    End Set

  End Property

 

  ‘ a few lines of code are removed here though they are important for binding

  .

  .

  .

 

  Public Property Retail() As Decimal

    Get

      Return _retail

    End Get

    Set(ByVal value As Decimal)

      _retail = value

    End Set

  End Property

 

End Class

 

How can we get one form with a ReportViewer Control to display 200 reports?

There are four things that must be coordinated to allow a report to occur:
1) A ReportViewer control
2) A Report or .rdlc file which is xml
3) A Datasource like a Dataset or a Collection
4) A BindingSource

Because there are four things to bind together we need a special kind of glue. It is called the ReportDataSource Class. See how it connects everything together below.

Myproj_EsnItems is the Namespace=projectname of the datasource,
and EsnItems is the class type.

 

In myproj.Report1.rdlc the projectname is myproj, Report1 is report name with rdlc being for
report description language, client version. The reports won't run without proper spelling and case.

 

Code Listing#2

 

Public Class Form2

 

  Private Sub Form2_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

    If Utility.WhichReport = 1 Then

      Dim rptSource1 As New Microsoft.Reporting.WinForms.ReportDataSource

      rptSource1.name = "WindowsApplication1_EsnItems"

      rptSource1.value = Me.BindingSource1

      Me.ReportViewer1.LocalReport.DataSources.Add(rptSource1)

      Me.ReportViewer1.LocalReport.ReportEmbeddedResource = "myproj.Report1.rdlc"

      Dim eitems As New EsnItems

      Dim eitem As New EsnItem

      Dim eitem1 As New EsnItem

      eitem.SerialNo = "778889"

      eitem.ModelNo = "244444444"

      eitem.OrderNo = "233333332"

      eitems.Add(eitem)

      eitem1.SerialNo = "99333333"

      eitem1.ModelNo = "44444455555"

      eitem1.OrderNo = "3222222223"

      eitems.Add(eitem1)

      Me.BindingSource1.DataSource = eitems

    Else

      Dim rptSource2 As New Microsoft.Reporting.WinForms.ReportDataSource

      rptSource2.name = "WindowsApplication1_PoItems"

      rptSource2.value =  Me.BindingSource1

      Me.ReportViewer1.LocalReport.DataSources.Add(rptSource2)

      Me.ReportViewer1.LocalReport.ReportEmbeddedResource = "myproj.Report2.rdlc"

      Dim items As New PoItems

      Dim item As New PoItem

      Dim item1 As New PoItem

      item.ProdNo = "2222222"

      item.Quantity = "2"

      item.Retail = "66"

      items.Add(item)

      item1.ProdNo = "1111111"

      item1.Quantity = "1"

      item1.Retail = "55"

      items.Add(item1)

      Me.BindingSource1.DataSource = items

    End If

    Me.ReportViewer1.RefreshReport()

  End Sub

 

End Class

 

Conclusion

For more information on the ReportViewer control click here.

Now we have a local Reporting system that previews, prints, et al supported solely by Visual Studio that we can have a lot of control over by manipulating the properties of it and any kind of Object can be our data.