Click here to Skip to main content
15,867,308 members
Articles / Programming Languages / Visual Basic

Entity Framework and Crystal Reports - Entities to Datasets

Rate me:
Please Sign up or sign in to vote.
5.00/5 (10 votes)
23 Jan 2013CPOL13 min read 50.1K   21   12
Using Entity POCOs as a DataSource for your Crystal Reports

Introduction

Crystal Reports. For a lot of people including myself, it's a Love / Hate relationship. On one hand, it (usually) integrates nicely with Visual Studio, and it is probably the most widely known reporting system for .NET developers. I was one of the first to jump onto the VS2012 bandwagon, however to my dismay, I quickly learned Crystal Reports was not available for the new IDE! But after multiple delays, Crystal is here, and my lord it angered me. After hours of chat time with tech support, and some lucky mouse clicks, I finally got it installed and working properly...only to find that for some odd reason, SAP decided NOT to support Entity Framework, and allow POCOs to be used as a RecordSource for the report. Our whole product is based around Entity Framework. I scoured the Internet for a solution, but to no avail. So I sat down and really thought about it, and worked out a fairly elegant way to use the information in the POCOs to populate your Crystal Reports using Reflection and little bit of Generics.

Background  

A pretty solid understanding of Entity Framework (EF) would be beneficial before proceeding, considering this is an article about Entity Framework.... 

Don't worry if you aren't too sure how to take advantage of all the features Reflection has to offer. I will explain my code step by step. Same goes for using Generics. 

You'll want to have Crystal Reports installed, any version will do. I will be working with SAP Crystal Reports, developer version for Microsoft Visual Studio,  the newest release as of January 2013. 

I will be working with Entity Framework 5.x If you do not have EF installed yet, please go to your Package Manager Console under Tools -> Library Package Manager -> Package Manager Console. In the console type "Install-Package EntityFrameWork -pre" without the quotes. This will get you prepared to use EF. However, this article will not explain how to use EF. There are many tutorials around the web for that. 

Before We Get Into It 

The following class is going to be our EF POCO that we will be using to populate our Crystal Report:

VB.NET
Partial Public Class WorkOrder
  Public Property ID As Integer
  Public Property WorkOrderNumber As Integer
  Public Property Description As String
  Public Property PartNumber As Integer
  Public Property Quantity As Integer
End Class 

This is a very basic table / class but it will serve it's purpose for this article.

You are going to want to create a new Windows Form. Click Project - > Add Windows Form...

Name your new form 'frmReportViewer' and click the Add button. I'm sure you can guess we are going to use this form to view our Crystal Report when we are finished. Resize this form to a reasonable size for viewing reports. Next we want to add a CrystalReportViewer object to the form. From your Toolbox on the left, scroll down to Reporting. If Crystal Reports is installed correctly, you should see CrystalReportViewer under the Reporting node. Drag the report viewer onto your form. Now you've got something to display your report. Rename your report viewer in the properties pane to crvReportViewer

Now we need a report! But before we can create one, we reach our first road block. If you aren't using EF, you would normally create a connection to your database, and bind your report to a table from your data; selecting the fields you would like to display and presto, you got your report. By binding the data to your report, Crystal has the information it needs, more accurately, it knows the Schema of the table its bound to. But the joy of using EF is you are working with objects, and not directly with the data. A Crystal Report needs to know the Schema of the table(s) its going to be bound to before displaying the information. So how to we tell Crystal the 'Schema' of our EF object?...

Creating an .XSD Schema File

I won't go into the gritty details of the .XSD file, but in short, all it really is is a Dataset designer. You can graphically create a Dataset, adding tables, or DataTables to your Dataset, which will allow you to create the Schema for your EF object. 

Click on Project ->Add New Item -> Data (under Common Items on the left Tree Structure) -> DataSet. Name your DataSet 'dsWorkOrderSchema' and click the Add button. This will bring to a boring designer. Let's make it less boring! From the Toolbox, drag a DataTable onto your designer window. See, less boring!  Click on the table name (currently name DataTable1), and change it to WorkOrder.   Next you want to right-click on your table, from the context menu click 'Add', then select 'Column'. Rename your new column to 'ID'. Create another new column and rename it 'WorkOrderNumber'. See where I'm going? Repeat this for the rest of the properties from your WorkOrder class described at the start of this article. Ensure you name the columns the same as your property names (exactly! Is Case Sensitive). Once completed, you now have a Schema for your EF object!! You can use this object to start designing your report. So lets do that... 

Creating the Crystal Report  

Before continuing, Save your project.  Now we need to create our report. Click Project-> Add New Item -> Reporting -> Crystal Reports. Name your report 'crWorkOrder'. Click the Add button. A window will pop up. Make sure the 'Using the Report Wizard' radio button is selected and click 'OK'. A new window will pop up. From your 'Available Data Sources' pane on the left, you want to expand the 'Project Data' node, then expand the 'ADO.NET DataSets' node, and you should see your WorkOrder DataSet. Select it, click the '>' button in the center of the window and click 'Next'. Now here is where you will choose the fields you want to display on your report. For this example we are just going to select all of them so click the '>>' button and click 'Finish'. If you click 'Next' instead of 'Finish', you can fine tune things a little bit by grouping your fields. You can play with these options on your own, for this article we will be ignoring grouping.

Now you have a very simple report! We are not going to go into formatting of the report, I'll leave that for your own exploration. Now Save your project, and we will dive into the code.

The Cure For The "Banging Your Head Against The Desk" Syndrome

To pass data to your report, like I mentioned earlier, you would usually bind your report to a database table, and Crystal would take care of the rest. Another way would be in code. First create DataTables, fill the DataTables with data, then create a TableAdapter, use the TableAdapter to fill a DataSet, then bind the DataSet to the report. Now this is the right way to do it and common practice. We actually did two of these steps. Remember in the DataSet designer when we dragged a DataTable onto the designer window and created the columns? There we created our DataTable and assigned it to a our DataSet. Now here's the issue; how to we get the information from our EF object, and insert it into the DataTable that is assigned to our Dataset so that we can bind our DataSet to our report to display? 

In a perfect world, and kind of what I was expecting from SAP, this would work: 

VB.NET
Dim crNewReport As New crWorkOrder
'Your DBContext object
Dim context As New MyContext
Dim newWorkOrder As WorkOrder
'Query for a WorkOrder with an ID of 1 
newWorkOrder = context.Set(Of WorkOrder). Find(1)
 
'Load your Crystal Report file 
crNewReport.Load("crWorkOrder.rpt")

'Try and set the datasource to your EF Object 
crNewReport.SetDataSource(newWorkOrder)    

But it doesn't. Even though all the data you want to report resides in your WorkOrder EF Object, even tho the properties of your WorkOrder EF Object exactly match the Schema we created in our .XSD DataSet file that we bound to the report, Crystal Reports still requires you to pass a filled DataSet object as it's source. Crap. Okay, so now try and take your information from your EF Object, and fill your DataSet...

...Still trying? Don't feel bad.  Here is the tedious, long winded way of doing this... 

VB.NET
Dim dsDataSet As New DataSet 
Dim dtDataTable As New DataTable("WorkOrder")
Dim drNewRow as DataRow
Dim newContext As New MyContext 
Dim newWorkOrder As WorkOrder 
Dim crNewReport As New crWorkOrder
 
dtDataTable.Columns.Add("ID")
dtDataTable.Columns.Add("WorkOrderNumber")
dtDataTable.Columns.Add("PartNumber")
dtDataTable.Columns.Add("Description")
dtDataTable.Columns.Add("Quantity")
 
dsDataSet.Tables.Add(dtDataTable)
 
newWorkOrder = newContext.Set(Of WorkOrder).Find(1) 
drNewRow = dsDataSet.Tables(0).NewRow
drNewRow("ID") = newWorkOrder.ID
drNewRow("WorkOrderNumber") = newWorkOrder.WorkOrderNumber
drNewRow("Description") = newWorkOrder.Description
drNewRow("PartNumber") = newWorkOrder.PartNumber
drNewRow("Quantity") = newWorkOrder.Quantity
 
dsDataSet.Tables(0).Rows.Add(drNewRow) 
crNewReport.Load("crWorkOrder.rpt") 

This is for just one WorkOrder record! If you had multiple WorkOrders you would like to display, you would loop through an IEnumerable object of WorkOrder and add each row. And this is for just ONE report. You would have to write this out for each report you wanted to generate, making sure you know the names of all the columns you want to add data to. And what if you had multiple tables that you wanted to bind to the dataset? (I know this code can be shortened, but I wanted to give you an idea of how frustrating it can become if you were creating multiple, sometimes hundreds, of different reports).

So what's the solution? Generics and Reflection. I'm just going to show you the code I designed first, then I will go through it and explain how it makes every part of your life easier...

VB.NET
Sub EntityToDataSet(Of TEntity)(ByRef ds As DataSet, ByVal MyEntity As TEntity)
    Dim strTableName As String
    Dim drNewRow As DataRow
    Dim EntityFields = GetType(TEntity).GetProperties.Where(Function(a) a.CanRead)
 
    strTableName = MyEntity.GetType.FullName
    drNewRow = ds.Tables(strTableName).NewRow
 
    For Each field in EntityFields
        If drNewRow.Table.Columns.Contains(field.Name) Then
            drNewRow(field.Name) = field.GetValue(MyEntity, Nothing)
        End If
    Next
    ds.Tables(strTableName).Rows.Add(drNewRow)
End Sub   

You would use it like this... 

VB.NET
EntityToDataSet(dsMyDataSet, newWorkOrder)   

That's it! Have a good day!

Okay, I guess I'll explain. First, I would (and did) create this Sub Procedure in a Module, to allow your entire Project access to it. I will go through it line by line.

The First Line 

EntityToDataSet(Of TEntity) 

If this is your first foray into Generics this can be a hard concept to grasp. After the method name, by writing (Of TEntity), you are basically saying 'I don't care what kind of object TEntity is, but allow it to be used/passed as an argument for this method'. Now we could have omitted this, but when passing our arguments (which I'll get to in a second), you would have had to write 'ByVal MyEntity As WorkOrder'. Now the problem with this is we would have to write a separate procedure for EACH and EVERY Entity Framework POCO that we would be using in our reports! Some systems can have hundreds, even thousands of POCOs. Do you really want to write a procedure that is nearly identical a thousand times!? By allowing a Generic object to be passed, this one procedure will work for ALL of your POCOs! Cool eh? 

(ByRef ds As DataSet, ByVal MyEntity As TEntity)

By passing a Reference to a DataSet, any action we perform in our procedure will directly effect / change the DataSet. Simple concept, I won't get into it. If you are unclear of the difference between 'ByRef' and 'ByVal', please refer to a few beginner tutorials that explain the differences. 

The next argument is our EF POCO we want to get our data from. Again, because we are using Generics, this object can be any object. In our case, we will be using our WorkOrder object. We do not need to pass a reference to our WorkOrder object, as we only need to pull the data from it. We don't want or need to change any of the properties of the object. 

Lines 2 and 3  

VB
Dim strTableName As String
Dim drNewRow as DataRow

I don't think I need to go into detail here. strTableName will store the name of the table in the DataSet we would like to add a record to.  drNewRow will be used to hold the information from our WorkOrder object. Each row is equal to 1 record. Just as each WorkOrder object is equal to 1 record in your database. In this scenario, we will only be passing one WorkOrder object to this procedure. At the end of this article, I will quickly explain how you can pass multiple objects, even of different types, to this procedure.

Line 4

VB
Dim EntityFields = GetType(TEntity).GetProperties.Where(Function(a) a.CanRead)

Reflection is a wonderful thing. I can't get into the details of Reflection as it is was beyond the scope of this article. You could spend months and months studying up on Reflection and all it's glory.

GetType(TEntity)  

Because we are passing in a Generic object, we still need to know why type of object we are working with. GetType(TEntity) will do just that. Once we know the type, we can then perform...

GetType(TEntity).GetProperties 

The method name pretty much sums it up. Remember the properties we created for our WorkOrder object and the beginning (WorkOrderNumber, PartNumber, etc)? GetProperties is getting all the properties of the WorkOrder object. 

GetType(TEntity).GetProperties.Where(Function(a) a.CanRead)

If you are not familiar with Lambda functions or Predicate Functions, please do a little Googling! By using this Lambda, we are basically saying, 'Return only the properties from which we can read from'. In this case, it's all properties as they are all public. Now the EntityFields object contains an array of all the property names of our WorkOrder object. 

Line 6 and 7 

VB
strTableName = MyEntity.GetType.FullName 
drNewRow = ds.Tables(strTableName).NewRow

Again we use Reflection here. MyEntity.GetType.FullName is actually finding out the name of object, more specifically, the Class name of the object. For example, even though we name our WorkOrder object newWorkOrder, when we call MyEntity.GetType.FullName, it will return 'WorkOrder', not newWorkOrder. Remember when we created our .XSD DataSet file? What did we name our DataTable name? 'WorkOrder'! As long as we follow this convention for creating .XSD DataSets (Naming the DataTables with the same name as our POCOs),  we can get the table name we want to reference without even knowing the type of EF object we have passed.

The next line is instantiating our DataRow object. We are saying 'I want drNewRow to represent a new record in our strTableName (in this case 'WorkOrder') table. 

Lines 9 to 13  

VB
For Each field In EntityFields
    If drNewRow.Table.Columns.Contains(field.Name) Then
        drNewRow(field.Name) = field.GetValue(MyEntity, Nothing)
    End If
Next

Now we are going to loop through all the fields (or all the properties) of our WorkOrder object which we stored in the EntityFields array. To get the name of the property, we use 'field.Name'. This returns the name of the property. Each field.Name represents a column name in our DataTable we created in our .XSD file. 

The conditional If statement is saying asking 'in the DataTable of the new record we are creating, is there a column with the name field.Name'? For example, the first time we run through the loop, field.Name might equal 'WorkOrderNumber'. So the if statement is checking to see if there is a column with the name 'WorkOrderNumber' in the DataTable.  Of course we know that in fact there is a column with this name, so now we want to add some data to this column.

Saying 'drNewRow(field.Name)' references the column in the DataTable with the name field.Name. We assign the value of the field by saying field.GetValue(MyEntity, Nothing). Just as .Name gives us the name of the property, .GetValue will give us the value stored in the property. The .GetValue function takes two parameters; the object to which we want to get the property value from (We want the value from a property of our MyEntity object), and an optional Index value for indexed properties. We don't have Indexed properties, so we pass it a Null, or Nothing value.

The loop will continue to repeat these steps for each property of our MyEntity object, until all the data of our object is stored in our new drNewRow object.

Line 14 ;

VB
ds.Tables(strTableName).Rows.Add(drNewRow)

Finally, we are adding our newly created row / record to the DataTable that belongs to our DataSet object. We have officially used our Entity Framework object to fill a DataSet. We can now bind our DataSet to our Crystal Report, and report to our hearts content!

The Finale

Here is how we can put our new procedure in action! You can put this in the Form_Load event of your frmReportViewer Form. 

VB.NET
Dim dsWorkOrder As New dsWorkOrderSchema
Dim newWorkOrder As WorkOrder
Dim newContext As New MyContext 
Dim crNewReport As New crWorkOrder 
 
newWorkOrder = newContext.Set(Of WorkOrder).Find(1)
EntityToDataSet(dsWorkOrder, newWorkOrder)
crNewReport.Load("crWorkOrder.rpt")
crNewReport.SetDataSource(dsWorkOrder)
 
'This will display the report on your Report Viewer object we added at the start of 
'this article 
crvReportViewer.Report = crNewReport 

I wanted to go into as much detail as possible with this article because I spent a lot of time searching the web for solution to this problem, and came up relatively empty. I truly hope this article helps at least one person out. At least then the 4 hours I spent writing it will not be in vain! Happy Coding! 

Taking The EntityToDataSet Procedure One Step Further  

I promised I would show you how you could modify the procedure to accept multiple EF POCO's. I'll show you the code, but I believe you have enough understanding now to realize how the code works without my explanation. 

Sub EntityToDataSet(Of TEntity)(ByRef ds As DataSet, ByVal MyEntities As List(Of TEntity))
  Dim strTableName As String 
  Dim drNewRow As DataRow 
  For Each POCO In MyEntities 
      Dim EntityFields = POCO.GetType.GetProperties.Where(Function(a) a.CanRead)
      strTableName = POCO.GetType.FullName
      drNewRow = ds.Tables(strTableName).NewRow
      For Each field In EntityFields
          If drNewRow.Table.Columns.Contains(field.Name) Then
              drNewRow(field.Name) = field.GetValue(POCO, Nothing)
          End If
       Next
       ds.Tables(strTableName).Rows.Add(drNewRow)
  Next POCO 
End Sub 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Cadorath Aerospace
Canada Canada
Started as a hobbyist programmer at age 15, creating small programs in Java to help make my everyday computing easier. Well, I tried to at least.

Worked my way through Red River College, going with Computer Programmer/Analyst as my major. Continued my studies part time to increase my knowledge of Visual Basic and the .NET platform.

Landed my career at Cadorath Aerospace as Lead .NET Developer.

I am also proficient in Java, C#, SQL, JavaScript, HTML, XML, and VBA.

Comments and Discussions

 
GeneralMy vote of 5 Pin
ItsaHardwareProblem22-Jan-13 3:52
ItsaHardwareProblem22-Jan-13 3:52 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.