65.9K
CodeProject 正在变化。 阅读更多。
Home

Entity Framework 和 Crystal Reports - 实体到数据集

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2013年1月21日

CPOL

13分钟阅读

viewsIcon

53337

使用 Entity POCOs 作为 Crystal Reports 的数据源。

引言

Crystal Reports。对许多人,包括我自己来说,这是一种爱恨交加的关系。一方面,它(通常)与 Visual Studio 集成得很好,而且可能是 .NET 开发人员中最广为人知的报告系统。我是第一批拥抱 VS2012 的人之一,但令我沮丧的是,我很快就发现 Crystal Reports 不适用于新的 IDE!但在多次延迟后,Crystal 终于来了,我的天,它惹恼了我。经过数小时的在线客服聊天和一些幸运的鼠标点击,我终于成功安装并正常运行了……结果却发现,出于某种奇怪的原因,SAP 决定不支持 Entity Framework,不允许使用 POCO 作为报告的 RecordSource。我们的整个产品都基于 Entity Framework。我搜遍了互联网寻找解决方案,但徒劳无功。于是我坐下来认真思考,并想出了一个相当优雅的方法,利用反射和一点泛型来使用 POCO 中的信息来填充 Crystal Reports。

背景  

在继续之前,对 Entity Framework (EF) 有扎实的理解会很有帮助,因为这是一篇关于 Entity Framework 的文章…… 

如果你不确定如何利用反射的所有功能,请不用担心。我会一步步解释我的代码。使用泛型也是如此。 

你需要安装 Crystal Reports,任何版本都可以。我将使用 SAP Crystal Reports, developer version for Microsoft Visual Studio, 这是截至 2013 年 1 月的最新版本。 

我将使用 Entity Framework 5.x。如果你还没有安装 EF,请转到“工具”->“程序包管理器”->“程序包管理器控制台”。在控制台中键入“Install-Package EntityFrameWork -pre”(不带引号)。这将使你做好使用 EF 的准备。但是,本文不会解释如何使用 EF。网上有很多相关的教程。 

在我们开始之前 

下面的类将是我们将用于填充 Crystal Report 的 EF POCO

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 

这是一个非常基本的表/类,但它将为本文服务。 

你需要创建一个新的 Windows 窗体。单击“项目”->“添加 Windows 窗体…”

将新窗体命名为 'frmReportViewer',然后单击“添加”按钮。我敢打赌你可以猜到,完成后我们将使用此窗体来查看 Crystal Report。将此窗体调整到合理的大小以查看报告。接下来,我们要向窗体添加一个 CrystalReportViewer 对象。从左侧的“工具箱”中,向下滚动到“Reporting”。如果 Crystal Reports 已正确安装,你应该会在“Reporting”节点下看到 CrystalReportViewer。将报告查看器拖到窗体上。现在你就有了一个显示报告的东西。在属性窗格中将报告查看器重命名为 crvReportViewer。 

现在我们需要一个报告!但在我们创建报告之前,我们会遇到第一个障碍。如果你不使用 EF,你通常会创建一个到数据库的连接,并将报告绑定到你的数据中的一个表;选择你想要显示的字段,然后搞定,你就得到了你的报告。通过将数据绑定到报告,Crystal 就会获得它所需的信息,更准确地说,它知道它所绑定到的表的架构。但是使用 EF 的好处是你处理的是对象,而不是直接处理数据。Crystal Report 需要知道它将要绑定到的表(或表)的架构,然后才能显示信息。那么,我们如何告诉 Crystal 我们的 EF 对象的“架构”呢?...

创建 .XSD 架构文件

我不会深入探讨 .XSD 文件的细节,但简而言之,它就是一个数据集设计器。你可以图形化地创建一个数据集,向你的数据集中添加表或 DataTable,这将允许你为你的 EF 对象创建架构。 

单击“项目”->“添加新项”->“Data”(在左侧树结构下的“Common Items”下)->“DataSet”。将你的 DataSet 命名为 'dsWorkOrderSchema',然后单击“添加”按钮。这将带你进入一个无聊的设计器。让我们让它不那么无聊!从“工具箱”中,将一个 DataTable 拖到你的设计器窗口上。看,不那么无聊了!  单击表名(当前为 DataTable1),然后将其更改为 WorkOrder。   接下来,你需要右键单击你的表,在上下文菜单中单击“添加”,然后选择“Column”。将你的新列重命名为“ID”。创建另一个新列并将其重命名为“WorkOrderNumber”。看到我的意思了吗?对你在本文开头描述的 WorkOrder 类的其余属性重复此操作。确保你将列名命名为你属性的名称(完全一样!区分大小写)。 完成后,你就拥有了 EF 对象的架构!你可以使用此对象开始设计报告。那么,我们开始吧…… 

创建 Crystal Report  

在继续之前,保存你的项目。  现在我们需要创建我们的报告。单击“项目”-> “添加新项”-> “Reporting”-> “Crystal Reports”。将你的报告命名为 'crWorkOrder'。 单击“添加”按钮。 将弹出一个窗口。确保选中“Using the Report Wizard”单选按钮,然后单击“OK”。 将弹出一个新窗口。在左侧的“Available Data Sources”窗格中,展开“Project Data”节点,然后展开“ADO.NET DataSets”节点,你应该会看到你的 WorkOrder DataSet。 选中它,单击窗口中心的“>”按钮,然后单击“Next”。现在在这里,你将选择要在报告中显示的字段。在此示例中,我们将选择所有字段,因此单击“>>”按钮,然后单击“Finish”。如果你单击“Next”而不是“Finish”,你可以微调一些选项,例如对字段进行分组。你可以自己玩这些选项,本文将忽略分组。 

现在你有一个非常简单的报告! 我们不会深入讨论报告的格式,我将留给你自行探索。 现在保存你的项目,然后我们将深入代码。 

治愈“猛砸脑袋”综合症

如前所述,要将数据传递给报告,你通常会将其绑定到数据库表,然后 Crystal 会处理其余的事情。另一种方法是在代码中进行。首先创建 DataTables,用数据填充 DataTables,然后创建一个 TableAdapter,使用 TableAdapter 填充 DataSet,然后将 DataSet 绑定到报告。这是正确的做法,也是常见的做法。我们实际上已经完成了其中两个步骤。还记得我们在 DataSet 设计器中将 DataTable 拖到设计器窗口并创建列吗?在那里,我们创建了 DataTable 并将其分配给了我们的 DataSet。现在的问题是;我们如何将信息从 EF 对象中提取出来,并插入到分配给我们的 DataSet 的 DataTable 中,以便我们可以将 DataSet 绑定到报告进行显示? 

在一个完美的世界里,这也是我从 SAP 期望的,这会起作用: 

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)    

但事实并非如此。尽管所有你想要报告的数据都存在于你的 WorkOrder EF 对象中,尽管你的 WorkOrder EF 对象的属性与我们在 .XSD DataSet 文件中创建的、已绑定到报告的架构完全匹配,但 Crystal Reports 仍然要求你将一个填充好的 DataSet 对象作为它的源。糟糕。好的,那么现在尝试从你的 EF 对象中提取信息,并填充你的 DataSet... 

……还在尝试吗?别不好意思。 这是非常繁琐、冗长的方法…… 

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") 

这仅仅是针对 **一个** WorkOrder 记录!如果你想显示多个 WorkOrders,你需要循环遍历 WorkOrderIEnumerable 对象,然后添加每个行。而这仅仅是针对一个报告。你需要为你想生成的每个报告写出这些代码,并确保你知道你要添加数据的列名。如果有多张表需要绑定到数据集怎么办?(我知道这段代码可以缩短,但我只是想让你大致了解一下,如果你要创建许多,有时是数百个不同的报告,这会变得多么令人沮丧)。

那么解决方案是什么?泛型和反射。我将先展示我设计的代码,然后我会逐行解释它如何让你的生活变得更轻松…… 

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   

你可以这样使用它…… 

EntityToDataSet(dsMyDataSet, newWorkOrder)   

就这样!祝你一天愉快!

好吧,我猜我会解释的。首先,我会在一个模块中创建这个子过程,以便让你的整个项目都可以访问它。我会逐行解释。

第一行 

EntityToDataSet(Of TEntity) 

如果这是你第一次接触泛型,这可能是一个难以理解的概念。在方法名之后,通过编写(Of TEntity),你基本上是在说“我不在乎 TEntity 是什么类型的对象,但允许它在此方法中被使用/作为参数传递”。现在我们可以省略它,但当你传递参数(稍后会讲到)时,你就不得不写 'ByVal MyEntity As WorkOrder'。现在的问题是,我们需要为我们将在报告中使用的 **每一个** Entity Framework POCO 编写一个单独的过程!有些系统可能有数百个,甚至数千个 POCO。你真的想写一个几乎完全相同的过程一千次吗!?通过允许传递一个泛型对象,这个过程将适用于 **所有** 你的 POCO!很酷吧? 

(ByRef ds As DataSet, ByVal MyEntity As TEntity)

通过传递一个 DataSet 的引用,我们在过程中进行的任何操作都将直接影响/改变 DataSet。这是一个简单的概念,我不会深入探讨。如果你不清楚 'ByRef' 和 'ByVal' 之间的区别,请查阅一些解释它们之间区别的初学者教程。 

下一个参数是我们想要从中获取数据的 EF POCO。同样,因为我们使用的是泛型,所以这个对象可以是任何对象。在我们的例子中,我们将使用我们的 WorkOrder 对象。我们不需要传递 WorkOrder 对象的引用,因为我们只需要从中读取数据。我们不希望也不需要更改该对象的任何属性。 

第 2 行和第 3 行  

Dim strTableName As String
Dim drNewRow as DataRow

我认为我不需要详细说明。strTableName 将存储我们想要添加记录的数据集中的表名。 drNewRow 将用于保存来自我们 WorkOrder 对象的信息。每一行等于一个记录。就像每个 WorkOrder 对象等于你的数据库中的一个记录一样。在此场景中,我们只会向此过程传递一个 WorkOrder 对象。在本文的最后,我将快速解释如何向此过程传递多个对象,甚至是不同类型的对象。 

第 4 行 

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

反射是一件很美妙的事情。我无法深入探讨反射的细节,因为它超出了本文的范围。你可以花很多很多个月来研究反射及其所有荣耀。 

GetType(TEntity)  

因为我们传递的是一个泛型对象,所以我们仍然需要知道我们正在处理的对象类型。GetType(TEntity) 可以做到这一点。一旦我们知道了类型,我们就可以执行…… 

GetType(TEntity).GetProperties 

方法名几乎概括了它的作用。还记得我们在开头为 WorkOrder 对象创建的属性(WorkOrderNumber、PartNumber 等)吗?GetProperties 正在获取 WorkOrder 对象的所有属性。 

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

如果你不熟悉 Lambda 函数或谓词函数,请稍微谷歌一下! 通过使用这个 Lambda,我们基本上是在说,“只返回我们可以读取的属性”。在这种情况下,所有属性都是公共的,所以我们都可以读取。现在 EntityFields 对象包含我们 WorkOrder 对象的所有属性名称的数组。 

第 6 行和第 7 行  

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

我们在这里再次使用反射。MyEntity.GetType.FullName 实际上是在找出对象的类型,更具体地说,是对象的类名。例如,即使我们将 WorkOrder 对象命名为 newWorkOrder,当我们调用 MyEntity.GetType.FullName 时,它将返回 'WorkOrder',而不是 newWorkOrder。 还记得我们创建 .XSD DataSet 文件时吗?我们如何命名我们的 DataTable?“WorkOrder”!只要我们遵循这种创建 .XSD DataSets 的约定(用与我们的 POCO 相同的名称命名 DataTables),  我们就可以在甚至不知道我们传递的 EF 对象类型的情况下,获得我们要引用的表名。 

下一行是实例化我们的 DataRow 对象。我们说“我希望 drNewRow 代表我们在 strTableName(在本例中为 'WorkOrder')表中的一个新记录。” 

第 9 行至第 13 行  

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

现在我们将循环遍历我们存储在 EntityFields 数组中的 WorkOrder 对象的所有字段(或所有属性)。要获取属性的名称,我们使用 'field.Name'。这返回属性的名称。每个 field.Name 都代表我们在 .XSD 文件中创建的 DataTable 中的一个列名。 

条件 If 语句正在检查“在我们正在创建的新记录的 DataTable 中,是否存在一个名为 field.Name 的列?”例如,我们第一次循环时,field.Name 可能等于 'WorkOrderNumber'。因此,if 语句正在检查 DataTable 中是否存在一个名为 'WorkOrderNumber' 的列。  当然,我们知道实际上有一个具有此名称的列,所以现在我们要向此列添加一些数据。 

说 'drNewRow(field.Name)' 引用了 DataTable 中名为 field.Name 的列。我们通过说 field.GetValue(MyEntity, Nothing) 来分配字段的值。就像 .Name 给出属性名称一样,.GetValue 将给出属性中存储的值。.GetValue 函数接受两个参数;我们要从中获取属性值的对象(我们想要从 MyEntity 对象的属性中获取值),以及一个可选的索引值,用于索引属性。我们没有索引属性,所以我们传递 Null 或 Nothing 值。 

循环将继续为我们 MyEntity 对象的每个属性重复这些步骤,直到对象的所有数据都存储在我们的新 drNewRow 对象中。 

第 14 行 ;

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

最后,我们将我们新创建的行/记录添加到属于我们的 DataSet 对象的 DataTable 中。我们已经成功地使用我们的 Entity Framework 对象填充了一个 DataSet。现在我们可以将我们的 DataSet 绑定到我们的 Crystal Report,并尽情地报告了! 

结局

以下是如何将我们的新过程投入实践!你可以将其放在 frmReportViewer 窗体的 Form_Load 事件中。 

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 

我想在这篇文章中尽可能详细地介绍,因为我花了很长时间在网上搜索这个问题的解决方案,但结果却寥寥无几。我真心希望这篇文章至少能帮助一个人。这样,我花 4 个小时写它才不会白费!祝编程愉快! 

将 EntityToDataSet 过程再进一步  

我承诺过会告诉你如何修改该过程以接受多个 EF POCO。我会给你看代码,但我相信你现在已经足够理解了,可以明白代码是如何工作的,而无需我解释。 

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 
© . All rights reserved.