使用 MS Reporting Services 报告分层递归数据






4.60/5 (14投票s)
2006年6月13日
8分钟阅读

129400

1094
一篇演示使用智能客户端接口来报告的文章。
引言
我将从一个问题开始。你们有多少人有机会与 Northwind 示例数据库中的 Employee 表进行交互?好了……我可以看到无数只手举起来,为什么不呢,它是 Access 和 SQL Server 都附带的标准数据库之一。好吧,我们在这里要讨论 Northwind 数据库吗?不。Employee 表有什么特别之处吗?我想说有。为什么特别?嗯,如果你仔细观察,它和其他标准表一样;然而,表中的两个字段,“EmployeeID”和“ReportsTo”以一种有趣的方式相互关联!是的,你猜对了;我指的是层次关系,我们也通常称之为递归数据。我正试图阐明报告递归性质的数据。
什么是递归数据?
我敢肯定,如果你处理过数据库,你一定已经面对过递归数据的挑战。定义了与给定关系关联级别的层次数据可以称为递归性质。典型的例子是会计应用程序数据库,它有一个名为 ChartOfAccounts 的表。“Account_Id”主键将与另一个名为“Reporting_Account_Id”的列具有外键关系。另一个例子是我在这篇文章中使用的例子,其中每个员工都有一个经理。
还记得“自连接”吗?如上图所示,这是我们显示数据递归性质的一种方式,在此处放置是为了更好地理解。
递归数据的报告挑战
我已经准备好第二个问题了;在我提问之前,我想请您注意一下显示员工级别输出的图片。现在的问题是:您认为在没有任何自定义代码的情况下生成这样的报告是否轻而易举?我敢肯定,这次我看到举手的人会比第一次提问时少得多!或者您可以说,是的,这很简单,如果您已经尝试过 MS Reporting Services。
在处理这种情况时,我们通常会编写某种自定义代码来找出层级等。典型的开发人员思维总是对软件供应商有无尽的愿望清单,我的一个愿望是如果能在报表引擎中内置一些东西来处理递归数据。不知怎么的,我感觉我的心灵感应起作用了,微软的家伙们将此功能加入了 Reporting Services,我在写几行在这里向他们致敬。不过,我想澄清一点,我曾与其他多个报表引擎合作并很喜欢它们,然而,自从我开始使用 Reporting Services 以来,我个人感觉轻松多了。
现在让我们戴上报表写作帽……
当我看看市场上不同的报表引擎时,底层概念基本相同;我说的是页眉、页脚、数据区域、数据分组、摘要等。所以,即使您还没有接触过 Reporting Services,如果您对任何报表引擎有一定的工作经验,您也不会很难理解本文中阐述的概念。
在这篇文章中,我还想向读者展示 Reporting Services 如何在客户端环境中使用智能客户端 Windows Forms 应用程序。
我假设本文的读者熟悉 Visual Studio 2005、C#、SQL Server 2000 和 Windows Forms。本文绝不是“Reporting Services 101”,因此我将假设您会尝试使用附带的代码进行实践,并找出其中隐藏的秘密。
将 Reporting Services 实现到智能客户端就像 1.2.3……
- 创建
DataSet
- 创建报表
- 使用
Preview
控件通过 ADO.NET 代码接口生成报表
1. 随时可用的 DataSet
要创建数据集,只需从 Solution Explorer 中单击 Add New Item。从 Visual Studio 已安装的模板中选择 DataSet
,并为其命名。创建 DataSet
后,在设计器窗口中打开它并添加一个 DataTable
。添加 DataTable
后,为其添加所需的列。在此示例中,我添加了三列,分别是 EmployeeName、EmployeeID 和 Reports_to。请务必将每列的 DataType
属性分别设置为 String
、Int32
和 Int32
。
DataSet
通常应该看起来像上图。现在我们的 DataSet
已经准备好了,很快您将看到一种有趣的方式来填充它,使用 ADO.NET 中引入的一项新技术,即使用 SqlDataReader
来填充 Dataset
(我想我的心灵感应在这里也起作用了)。
2. 报表设计
正如我们为 DataSet
所做的那样,只需从 Solution Explorer 中单击 Add New Item。从 Visual Studio 已安装的模板中选择 Report,并为其命名。正如我之前澄清的,我不会深入介绍报表设计器的每个控件/元素的细节;相反,我只会指出您需要注意的重要位置,以便创建使用递归数据的报表。
如上图所示,这就是我的报表在设计器中的样子。作为报表编写工具的典型特征,Reporting Services 也有一个界面,您可以首先定义页眉和页脚,然后转到报表正文等。在页眉部分,我有报表标题(洋红色)和运行日期(蓝色)。
我感觉最有趣的部分是正文部分,也称为数据区域。数据区域允许您放置许多令人兴奋的新控件,这些控件决定了数据的输出方式。我在这里使用了“Table
”控件,当它首次放置在设计器表面时,它会附带一个现成的页眉和页脚。
TextBox
控件大量用于显示信息。如果您查看图片,您可以看到我 just 放置了一个文本框控件并直接输入了报表标题。当涉及到指定表达式时,您所要做的就是在前面加上“=”号。您可以查看运行日期示例,其中我将字符串“Run Date”与 VB.NET 函数“Today
”连接起来以返回当前日期。
在设计器表面放置所有必需的控件并确保布局符合我们的喜好后,现在是时候念出魔法咒语了,它将自动处理数据的递归性质并管理层级等。
诀窍是将分组放在 Detail 部分(确保选择 Detail 区域并右键单击以访问组菜单选项),通过指定“EmployeeID”作为组,并根据下图将“ReportsTo”作为父组。
报表编写器有一个有用的内置函数,称为“Level
”,它返回递归层次结构中当前深度级别。
对于报表中下一个输出列,我们将指定以下表达式
=Level("tableEmployee_Details_Group") + 1
Level
函数返回一个从 0 开始的整数,0 代表第一级;因此,我在最终结果中加了 1。所以,在我们的例子中,“Andrew Fuller”是最高级别。您可以轻松地使用 Switch()
或 IIF()
等函数来获取这个级别编号,并替换为“CEO”、“General Manager”等。
报表中的第三个也是最后一个列显示了向给定员工记录报告的所有员工的数量。以下表达式可以实现此功能:
=Count(Fields!EmployeeID.Value,
"tableEmployee_Details_Group", Recursive) - 1
对于这两个表达式,“tableEmployee_Details_Group
”被用作引用名称,用于我们应用于数据详细区域的分组定义。
您是否也注意到报表输出中 EmployeeName 的层次结构格式化有一个有趣之处?这也很容易实现,您需要在 Padding->Left
属性中指定以下表达式:
=Level("tableEmployee_Details_Group") * 20 & "pt"
根据每个增量级别,它将在 EmployeeName 的左侧添加 20 pt,输出将看起来像一个树形结构。
3. 展示我的报表!
我知道在经历了所有这些准备之后,我们都很想看到报表的输出,不是吗?以下代码就能做到!
您可以在标准的 Windows Form 上放置 ToolBox->Data->ReportViewer 控件。我在这里使用 C# 在 Windows Forms 应用程序框架内。对于 ASP.NET 应用程序框架,同样的内容可以很容易地进行操作,并且可以轻松地转换为 VB.NET,如果那是您使用的主要脚本语言的话。
请确保您的代码隐藏 Form Load
方法如下:
private void Form1_Load(object sender, EventArgs e)
{
//declare connection string
string cnString = @"Data Source=(local);Initial Catalog=northwind;" +
@"User Id=northwind;Password=northwind";
//use following if you use standard security
//string cnString = @"Data Source=(local);Initial
//Catalog=northwind; Integrated Security=SSPI";
//declare Connection, command and other related objects
SqlConnection conReport = new SqlConnection(cnString);
SqlCommand cmdReport = new SqlCommand();
SqlDataReader drReport;
DataSet dsReport = new dsEmployee();
try
{
//open connection
conReport.Open();
//prepare connection object to get the data through reader and
// populate into dataset
cmdReport.CommandType = CommandType.Text;
cmdReport.Connection = conReport;
cmdReport.CommandText = "Select FirstName + ' ' + Lastname" +
" AS EmployeeName, EmployeeID, " +
"ReportsTo From Employees";
//read data from command object
drReport = cmdReport.ExecuteReader();
//new cool thing with ADO.NET... load data directly from reader
//to dataset
dsReport.Tables[0].Load(drReport);
//close reader and connection
drReport.Close();
conReport.Close();
//provide local report information to viewer
reportViewer.LocalReport.ReportEmbeddedResource =
"RecursiveData.rptRecursiveData.rdlc";
//prepare report data source
ReportDataSource rds = new ReportDataSource();
rds.Name = "dsEmployee_dtEmployee";
rds.Value = dsReport.Tables[0];
reportViewer.LocalReport.DataSources.Add(rds);
//load report viewer
reportViewer.RefreshReport();
}
catch (Exception ex)
{
//display generic error message back to user
MessageBox.Show(ex.Message);
}
finally
{
//check if connection is still open then attempt to close it
if (conReport.State == ConnectionState.Open)
{
conReport.Close();
}
}
}
我最喜欢的部分 - 关于…
众所周知,解决问题总有不止一种方法。我当然不是说这是处理递归性质数据的唯一方法。我很乐意听您分享您的技巧,并期待您对我提出的任何建设性批评。
免责声明:请随意使用本文内容,但对于由此产生的任何不良后果,本人概不负责。
感谢您的阅读……下次再见。再见。