使用 Microsoft Reporting Services 进行动态数据分组






4.39/5 (18投票s)
使用动态数据分组,从单个物理报表模板生成多个输出。
引言
我们经常听到“一石二鸟”。如果我说“一石四鸟”呢?我敢肯定,四比二听起来好多了。那么,我的四只鸟和一块石头是什么?
我的四只鸟是通过源 NorthWind->Orders (SQL Server 2000) 生成的四个不同的输出,而我的石头是单个的物理 MS Reporting Services .rdlc 文件,我将其用作生成不同输出的模板。这是一种完全适用的技术,我现在将与您分享。
这项技术的应用并不是什么新鲜事;我们在处理数据报告时都做过类似的事情。这种方法的新颖之处在于,我称之为报表重用(就像我们经常重用代码一样)。
让我们在这里讨论一个实际场景。如果我问您在(图片 1.0)中看到什么样的输出,您可能会说一个简单的订单信息列表报表。您说得对。如果最终用户希望使用按 CustomerID 分组的数据生成相同的报表(图片 1.1),您会怎么做?在大多数情况下,您会编写一个新报表。在本文中,我将演示一种方法,让您避免这种情况并重用报表来生成不同的输出。
我假设本文的读者熟悉 Visual Studio 2005、C#、SQL Server 2000 和 Windows Forms。对报表设计器工作原理的基本了解有助于使用附带的代码。
三按钮技术
现在,为了让生活更有趣一点,我在用户界面中添加了三个额外的按钮:按客户订购、按城市订购和按国家/地区订购。这三个额外的用户界面按钮没有任何内置的魔力;它们只是帮助我演示这项技术。所以,不再让大家久等
还记得童年时玩过的“找不同”游戏吗?我能请您用图片 1.0 和 1.1 玩同一个游戏吗?当然,它们看起来不同;第一张图片标题是“订单列表”,第二张图片标题是“按客户订购”,等等……
如果我们仔细观察,那么从技术上讲,真正的区别在于输出格式,底层数据是相同的(订单信息)。到目前为止,我敢肯定,大多数人可能已经明白了我的技术是什么,以及它将如何帮助您使用动态报表生成控件生成多个输出。
一个报表如何产生四种不同的输出?
想象一下,您被要求开发一个销售订单系统;其中一个报告要求是生成四份不同的报告来计算所有已发货订单的运费。
在典型情况下,您将根据规范创建四个单独的报告。嗯,这种方法没有什么不对,我们过去一直这样做,并且将来也会继续这样做。但是,既然我们做了很多代码重用,为什么不尝试重用报表作为模板并生成不同的输出呢?
重用报表的探索促使我使用了 MS Reporting Services。我在这项报表重用业务中玩得很开心。我想我会和我的朋友们分享这个,希望它能像帮助我一样帮助您。
让您的报表生成不止一种输出的关键在于一些设计考虑。以下是我为从这个报表中生成四种不同输出所做的工作。
报表设计注意事项
需要仔细的报表设计整合才能创建一个单一的报表模板,从而产生不同的输出。我们将开始使用 Table 控件;首先,我们必须识别并列出所有对所有输出通用的详细信息列。请检查附加代码中的报表以了解格式等详细信息。
动态数据分组
如果您注意到,除了详细信息部分之外,使所有输出看起来不同的事实是信息分组的方式。现在您可能会开始想,如何在运行时更改数据分组以查看不同的输出。
好吧,解决这个问题的方法是为我们的报表引入一些智能,渲染引擎可以利用这些智能并产生所需的输出。我有以下两个参数要传递给报表,以便它可以采取不同的行动。
我将使用 parReportType
参数传递以下四个值:O-Orders(订单)、C-Customer(客户)、S-City(城市)和 T-Country(国家/地区)。这个单字母类型(O、C、S、T)作为动态值提供,用于在生成输出之前对数据进行分组。
我们为报表设计器提供的智能无非是遵循分组表达式,它根据提供的 parReportType
信息更改数据组。
=iif(Parameters!parReportType.Value = "O","",
iif(Parameters!parReportType.Value = "C", Fields!CustomerID.Value,
iif(Parameters!parReportType.Value = "S",
Fields!ShipCity.Value,Fields!ShipCountry.Value)))
如果您不确定 iif()
是什么,那么不用担心,MS Reporting 在对表达式和自定义代码进行编码时使用 VB.NET 语法。如果您已经进行了任何自定义编码,例如使用 Crystal Reports,那么与 MS Reporting Services 交互将不是问题。
表达式向渲染引擎提供有关如何根据我们选择的报表选择来分组和排序数据的指令。它首先检查选择是否为“O”,这意味着简单的输出,不分组。随后,检查其余的选择并切换报表生成行为。
我们需要在分组的排序选项卡和排序属性窗口中重复相同的表达式。
组标题和页脚的处理
在此报表中,我们处理三个不同的组,一种输出不需要分组。我们必须执行以下操作才能生成正确的组名称并处理标题和页脚的可见性属性。
将以下表达式应用于组标题和页脚可见性属性
=IIF(Parameters!parReportType.Value = "O", True, False)
上述表达式将负责在选择了默认报表“订单列表”的情况下隐藏组标题和页脚。
由于组是动态更改的,因此我们必须更改输出以反映当前情况。如果用户选择“按客户订购”,那么我们必须确保将组标题更改为“客户:xyz”,依此类推。
输入为组标题的以下表达式可以根据提供的分组条件动态更改标题
=iif(Parameters!parReportType.Value = "O","",
iif(Parameters!parReportType.Value = "C",
"Customer: " & FIRST(Fields!CustomerID.Value),
iif(Parameters!parReportType.Value = "S",
"City: " & FIRST(Fields!ShipCity.Value),
"Country: " & FIRST(Fields!ShipCountry.Value))))
编码时间
到目前为止一切顺利,我们已经将各种智能都融入了报表模板中,我们确保采取了所有步骤来实现所需的结果。但是,是什么促使报表以某种方式行事?报表如何知道是应该生成基于客户或城市的报表,还是应该忽略分组并生成纯订单列表?
我们已经完成了报表的设计部分。现在,我们必须提供一种机制来从 SQL Server 收集数据并将其绑定到报表引擎。在许多数据绑定到报表引擎的方法中,我最喜欢的是使用 DataSet
。
确保准备好一个 DataSet
,如图片 1.7 所示。
我编写了一个名为 loadReport
的方法,并向其传递一个参数 reportType
。我从所有四个按钮调用此方法,每次传递不同的参数。
以下是方法的代码
private void loadReport(String reportType)
{
//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 dsOrders();
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 * FROM Orders Order By OrderID";
//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 =
"DataGrouping.rptOrders.rdlc";
//prepare report data source
ReportDataSource rds = new ReportDataSource();
rds.Name = "dsOrders_dtOrders";
rds.Value = dsReport.Tables[0];
reportViewer.LocalReport.DataSources.Add(rds);
//add report parameters
ReportParameter[] Param = new ReportParameter[2];
//set dynamic properties based on report selection
//O-order, C-Customer, S-City, T-Country
switch (reportType)
{
case "O":
Param[0] = new ReportParameter("parReportTitle",
"Orders List");
Param[1] = new ReportParameter("parReportType", "O");
break;
case "C":
Param[0] = new ReportParameter("parReportTitle",
"Orders by Customer");
Param[1] = new ReportParameter("parReportType", "C");
break;
case "S":
Param[0] = new ReportParameter("parReportTitle",
"Orders by City");
Param[1] = new ReportParameter("parReportType", "S");
break;
case "T":
Param[0] = new ReportParameter("parReportTitle",
"Orders by Country");
Param[1] = new ReportParameter("parReportType", "T");
break;
}
reportViewer.LocalReport.SetParameters(Param);
//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();
}
}
}
按钮背后的代码如下
private void btnOrders_Click(object sender, EventArgs e)
{
//orders list
loadReport("O");
}
private void btnByCustomer_Click(object sender, EventArgs e)
{
//orders by customer
loadReport("C");
}
private void btnByCity_Click(object sender, EventArgs e)
{
//orders by city
loadReport("S");
}
private void btnByCountry_Click(object sender, EventArgs e)
{
//orders by country
loadReport("T");
}
结论
我很乐意就此方法的优缺点进行任何讨论。对我来说,基于模板的方法和可重用性比其他任何东西都更有意义。任何建设性的批评都随时欢迎。
我在这里留给您最后的思考:如果您认为我说“一石四鸟”是错误的,而我本可以“一石七鸟”,那么您完全正确,我的朋友。我将把它留给您自己去解决。这是给您的一个额外练习,如果您玩附带的代码。
这是一个提示:想象一下,最终用户要求您制作三份报表,但这次他们不关心细节。只需给我客户…运费总计,城市和国家/地区的总计。解决方案是,**隐藏详细信息**!