Microsoft Reporting Services in Action:用自定义代码扩展 Microsoft SQL Server 2000 Reporting Services

Teodor Lachev

适用于: SQL Server 2000 Reporting Services

**摘要:**本文简要介绍了由 Teodor Lachev 所著的 Microsoft Reporting Services in Action 一书。了解如何使用自定义代码实现高级报表功能。

下载 Code.zip 示例代码以获取本文的示例代码。

本页内容

用自定义代码扩展 Microsoft SQL Server 2000 Reporting Services 用自定义代码扩展 Microsoft SQL Server 2000 Reporting Services
编写嵌入式代码 编写嵌入式代码
使用外部程序集 使用外部程序集
运转中的自定义代码:实现报表预测 运转中的自定义代码:实现报表预测
迁移 OpenForecast 迁移 OpenForecast
小结 小结

用自定义代码扩展 Microsoft SQL Server 2000 Reporting Services

Microsoft 在 2004 年初发布了 Microsoft SQL Server 2000 Reporting Services (Reporting Services),以便为开发人员提供一个完整的报表平台,无论目标平台或开发语言是什么,它都可以轻松地与所有类型的应用程序集成。Reporting Services 最显著的功能之一是它的可扩展特性,这是包括我在内的许多开发人员所欣赏的。您可以扩展或替换 Reporting Services 的几乎任何方面,包括数据、传递、安全性以及报表呈现功能。例如,扩展您的报表功能的一个方法是将它们与您或其他人编写的自定义 .NET 代码集成在一起。

在本文中,我将为您展示如何利用 Reporting Services 独特的可扩展体系结构来增强您的报表功能。首先,我将说明嵌入式和自定义代码选项是如何工作的。其次,我将为您展示您可以如何利用自定义代码来编写带有销售预测功能的高级报表。

我将假定您已经具有关于 Reporting Services 的基础知识,并且知道如何用表达式编写报表。如果您还不熟悉 Reporting Services,请访问其官方站点。本文中讨论的代码示例和示例报表均包含在文章源代码中。示例报表将 AdventureWorks2000 数据库用作其数据源,该数据库可以从 Reporting Services 安装程序中安装。

编写嵌入式代码

顾名思义,嵌入式代码保存在报表定义 (RDL) 文件中;它的作用范围在报表层。您只能在 Microsoft Visual Basic .NET 中编写嵌入式代码。一旦代码准备好,您就可以使用全局定义的 Code 成员在报表表达式中调用它。例如,如果您编写了一个名为 GetValue 的嵌入式代码函数,就可以使用下列语法从您的表达式中调用它:

=Code.GetValue()

除了共享方法外,您的嵌入式代码可以包含任何与 Visual Basic .NET 兼容的代码。实际上,如果您将嵌入式代码视为项目中的私有类,就差不多了。您可以声明类级别的成员和常数、私有或公共方法等。

您可以编写嵌入式代码来创建可重用的实用函数,它们可以从报表的几个表达式中调用。例如,请考虑图 1 中所示的 Territory Sales Crosstab 报表。

erscstcode01

图 1. 您可以使用嵌入式代码来实现作用范围在报表层的有用实用函数。

当数据缺失时(给定的行列组合中没有报表数据),该报表会使用一个名为 GetValue 的嵌入式函数来显示 “N/A”。此外,GetValue 还将缺失数据与 NULL 值区分开来。当基础值为 NULL 时,嵌入式代码会将其转换为零。

使用代码编辑器

要编写自定义嵌入式代码,您可以使用报表设计器代码编辑器 — 您可以在 Report Properties 对话框的 Code 选项卡上找到它,如图 2 所示。

erscstcode02

图 2. 使用用于编写嵌入式代码的代码编辑器。编辑器中所示的 GetValue 函数可确定某个值是缺失还是 NULL。

诚然,上述函数可以很容易地由一个基于 Iif 的表达式替换。但是,将逻辑封装在嵌入式函数中有两点优势。首先,它将表达式的逻辑集中在一个地方,而不是在报表中的每个字段都使用 Iif 函数。其次,它使报表具有更好的可维护性,这是因为如果您决定对函数进行逻辑更改,将不必跟踪并更改报表中的每个 Iif 函数。

报表设计器将嵌入式代码保存在报表定义文件的<Code> 元素下。执行此操作时,报表设计器将对文本进行 URL 编码。如果您出于某些原因决定直接更改 Code 元素,就需要注意这一点。

处理缺失值

一旦 GetValue 函数可以区分报表中的 NULL 和缺失数据,我们就能以下列表达式作为交叉表报表的 txtSalestxtNoOrders 数据字段的基础:分别为

=Iif(CountRows()=0, "N/A", Code.GetValue(Sum(Fields!Sales.Value)))

=Iif(CountRows()=0, "N/A", Code.GetValue(Sum(Fields!NoOrders.Value)))

CountRows 函数是 Reporting Services 所提供的几个原生函数之一,它可以返回指定范围内的行数。如果没有指定范围,它将默认为最里面的范围,这在我们的示例中解析为在数据单元格中定义值的静态组。两个表达式都会先使用 CountRows 来检查缺失数据(没有行),如果未发现缺失数据,将显示 “N/A”。否则,它们将调用 GetValue 嵌入式函数来转换 NULL 值。

我推荐您使用嵌入式代码来编写简单的报表专用且类似于实用工具的函数。当您的编程逻辑变得越来越复杂时,请考虑将您的代码移到外部程序集,我们将在下一步讨论这一点。

使用外部程序集

以编程方式扩展报表的第二种方法是使用外部 .NET 程序集中的预打包逻辑,这些程序集可以使用任何 .NET 支持的语言编写。这种将报表与外部程序集中的自定义代码集成在一起的能力显著提高了编程的选择余地。例如,通过使用自定义代码,您可以:

  • 利用 .NET framework 丰富的功能集 — 或示例,比如说您需要一个集合来存储某一矩阵区域的交叉表数据以执行一些计算。您可以“借用”.NET 随附的任何集合类,如 ArrayArrayListHashtable 等。

  • 将您的报表与您或第三方供应商编写的自定义 .NET 程序集集成在一起。例如,为了向第二部分中的 Sales by Product Category 报表添加预测功能,我使用了开放源码的 OpenForecast 包。

  • 通过使用功能强大的 Visual Studio .NET IDE 而不是原始的代码编辑器,编写代码变得更加简单了。

引用外部程序集

要使用位于外部程序集中的类型,您必须先使用 Report Properties 对话框中的 References 选项卡让报表设计器知道它,如图 3 所示。

erscstcode03

图 3. 使用“Report Properties”对话框来引用外部程序集。

假定我的报表需要使用自定义的 AWC.RS.Library 程序集(包含在文章的源代码中),我必须首先使用 References 选项卡引用它。虽然这个选项卡允许您浏览并引用任意文件夹中的程序集,但请注意,当报表被执行时,.NET 公共语言运行库 (CLR) 将根据 CLR 探测规则尝试定位程序集。简单地说,这些规则为您提供了两个部署自定义程序集的选项:

  • 将程序集部署为私有程序集。

  • 将程序集部署为 .NET 全局程序集缓存 (GAC) 中的共享程序集。作为一个先决条件,您必须强命名您的程序集。有关如何执行此操作的详细信息,请参阅 .NET 文档。

如果您选择了第一个选项,则需要将程序集同时部署到报表设计器和报表服务器中,这样引用该程序集的报表将在测试期间顺利执行,而且是作为托管报表分别执行。假定您已经接受了默认的安装设置,那么如果要将程序集部署到报表设计器二进制文件夹中,请将程序集复制到 C:\Program Files\Microsoft SQL Server\80\Tools\Report Designer 中。一旦您完成此操作,就可以构建报表并在 Visual Studio .NET 的预览模式中呈现它。

作为将报表部署到报表目录的一部分,请确保您将程序集复制到报表服务器二进制文件夹中,其默认位置是 C:\Program Files\Microsoft SQL Server\MSSQL\Reporting Services\ReportServer\bin。

请注意,将自定义程序集复制到正确位置只是部署过程的一部分。根据代码的任务,您可能还需要调整代码访问安全策略,以便程序集代码可以顺利执行。如果您需要有关部署自定义程序集的详细信息,请参阅 Reporting Services 文档中的“Using Custom Assemblies with Reports”部分。

调用共享方法

如果您只需要调用程序集中的共享方法(在 C# 中也称为静态方法),那么就可以这么做了,这是因为共享方法在报表中全局可用。

您可以通过下列语法,用完全限定的类型名称来调用共享方法:

<Namespace>.<Type>.<Method>(argument1, argument2, ..., argumentN)

例如,如果我需要从一个表达式或嵌入式代码中调用 RsLibrary 类(AWC.RS.Library 程序集)中的 GetForecastedSet 共享方法,我会使用下列语法:

=AWC.Reporting Services.Library.RsLibrary.GetForecastedSet(forecastedSet, forecastedMonths)

其中,AWC.RS.Library 是命名空间,RsLibrary 是类型,GetForecastedSet 是方法,还有 forecastedSetforecastedMonths 是参数。

调用实例方法

要调用实例方法,您还有一些额外的工作要做。首先,您必须枚举需要在 Classes 网格中实例化的所有实例类(类型)。对于每个类,您都必须指定一个实例名称。在后台,Reporting Services 将创建一个具有该名称的变量,以保存对此类型实例的引用。

当您在 Classes 网格中指定类名称时,请确保您输入的是完全限定的类型名称(包含命名空间)。在我的示例中(图 3),命名空间是 AWC.RS.Library,而类名称是 RsLibrary。如果您不确定完全限定类名称是什么,请使用 Visual Studio .NET Object Browser 或其他实用工具(如出色的 Lutz Roeder 的 .NET Reflector)来定位类名称并查找其命名空间。

例如,假定我需要调用 AWC.RS.Library 程序集中的一个实例方法,那么现在我必须声明一个实例变量 m_Library,如图 3 所示。在我的示例中,这个变量将保存对 RsLibrary 类的引用。

如果您要声明多个指向同一类型的变量,则每个变量都需要引用一个该类型的单独实例。在后台,当报表被处理时,Reporting Services 将实例化和实例变量数量一样多的所引用类型的实例。

一旦完成引用设置,您就可以通过所指定的实例类型名称来调用实例方法。就像使用嵌入式代码一样,您可以使用 Code 关键字来调用实例方法。共享方法和实例方法之间的区别是您使用变量名称来调用方法,而不是使用类名称。

例如,如果 RsLibrary 类型具有一个实例方法 DummyMethod(),我就能从一个表达式或嵌入式代码中调用它,如下所示:

Code.m_Library.DummyMethod()

了解了我们作为开发人员,以编程方式扩展报表功能时所使用的选择之后,下面看一下如何将其付诸实践。在下一部分中,我们将了解如何使用嵌入式代码和外部代码向我们的报表中添加高级功能。

运转中的自定义代码:实现报表预测

在本部分中,我将向您展示如何在我们的报表中植入预测功能。下面是我们将要创建的示例报表的设计目标:

  • 让用户可以生成任意阶段的销售数据交叉表报表。

  • 让用户可以指定预测列的数量。

  • 使用数据外推法来预测销售数据。

以下是我们的虚拟案例。假设您的用户请求了一个报表,以显示按产品类别分组的 Adventure Works 月度预测销售数据。为了让事情变得更加有趣,我们将允许报表用户指定一个数据范围来筛选销售数据以及预测月份的数量。为了实现上述要求,我们将编写一个交叉表报表 Sales by Product Category,如图 4 所示。

erscstcode04

图 4. Sales by Product Category 使用嵌入式代码和外部自定义代码进行预测。

用户可以输入起止日期来筛选销售数据。此外,用户可以指定报表上将显示多少个月的预测数据。报表以交叉表方式显示数据,在行上显示产品类别,在列上显示时间。报表的数据部分首先显示所请求时间段内的实际销售额,后面以粗体显示预测销售额。

例如,如果用户输入 4/30/2003 作为起始日期,输入 3/31/2004 作为截止日期,然后请求查看三个预测月,那么报表将显示 2004 年 4 月、5 月和 6 月的预测数据(为节省空间,图 4 只显示了一个月的预测数据)。

您可能也承认,独自实现预测功能并不是一件简单的任务。但如果已经有了为我们执行此操作的预打包代码,又会怎么样呢?如果这个代码可以运行在 .NET 上,我们的报表就可以将其作为自定义代码来访问。输入 OpenForecast。

用 OpenForecast 来预测

预测本身就是一门科学。一般来说,预测关心的是用于预言未知的过程。预测专业人员使用数学模型来分析数据、发现趋势并作出有根据的推断,而不是去看水晶球。在我们的示例中,Sales by Product Category 报表将通过数据外推方法来预测未来的销售数据。

用来外推一组数据的众所周知的数学模型有很多,如多项式回归、简单指数平滑法等。但是,实现这些模型并不是一件简单的任务。相反,为了我们的销售预测示例,我们将使用由 Steven Gould 编写的出色的开放源码 OpenForecast 包。OpenForecast 是一个包含基于 Java 的预测模型的通用软件包,这些模型可以应用于任何数据系列。这个软件包不要求您了解任何预测知识,并且支持几个数学预测模型,包括单变量线性回归、多变量线性回归等。要了解有关 OpenForecast 的详细信息,请访问它的主页 http://OpenForecast.sourceforge.net/

现在,让我们看一下如何实现预测示例,并通过编写一些嵌入式代码和外部代码与 OpenForecast 集成。

实现报表预测功能

创建一个具有预测功能的交叉表报表需要以下几个实现步骤。让我们从一个预想方法的高级视图开始,然后向下追溯到实现细节。

选择一种实现方法

图 5 显示了解决方案的逻辑体系结构视图。

erscstcode05

图 5. Sales by Product Category 报表使用嵌入式代码来调用 AwRsLibrary 程序集,接着调用 J# OpenForecast 包。

我们的报表将使用嵌入式代码来调用一个自定义程序集 (AwRsLibrary) 中的共享方法,并获得预测数据。AwRsLibrary 会将现有的销售数据加载到一个 OpenForecast 数据集中,并从 OpenForecast 获得预测模型。然后,它将调用 OpenForecast 来获取所请求月份的预测值。AwRsLibrary 将预测数据返回给报表,然后再显示它。

我们至少有两种实现选择可以将交叉表销售数据传递到 AwRsLibrary

  • 再次从数据库中获取销售数据。要实现这一点,报表可以按行传递选定的产品类别和月销售额。然后,AwRsLibrary 可以进行数据库调用以检索匹配的销售数据。

  • 使用报表内部的嵌入式代码将现有销售数据加载到某种类型的结构中,并将该结构传递到 AwRsLibrary

后一种方法的优点是:

  • 自定义代码逻辑是独立的。我们不必再次查询数据库。

  • 使用默认的自定义代码安全策略。我们不必为 AwRsLibrary 程序集提高默认的代码访问安全策略。如果我们选择了第一个选项,就不能略过默认代码访问安全设置,这是因为 Reporting Services 将只授予自定义程序集“执行”的权力,而这对于进行数据库调用是不够的。实际上,对于 OpenForecast,我必须对两个程序集都授予完全信任的权力,这是因为任何 J# 代码都需要完全信任权力来顺利执行。但是,如果我选择 C# 作为编程语言,就不必再执行此操作了。

  • 无需数据同步。我们不必考虑同步两个数据容器、矩阵区域和 AwRsLibrary 数据集。

出于上述原因,我选择了第二种方法。为了实现此方法,我们将使用一个表达式来填充矩阵区域数据值。该表达式将调用我们的嵌入式代码以加载一个数组结构,该数组结构是在嵌入式代码中按行来维护的。一旦给定的行被加载,我们就将该数组传递到 AwRsLibrary 以获得预测数据。

现在,让我们从将 OpenForecast 转换到 .NET 开始来讨论实现细节。

迁移 OpenForecast

OpenForecast 是用 Java 编写的,因此我必须克服的第一个障碍就是将其与 .NET 集成在一起。我有两个选择:

  • 我可以使用一个第三方的 Java 到 .NET 网关来集成两个平台。由于这种方法的复杂性,我很快就放弃了它。

  • 将 OpenForecast 转换到支持 .NET 的语言之一。Microsoft 为此提供了两个选择。第一,您可以使用 Microsoft Java Language Conversion Assistant 将 Java 语言代码转换到 C#。第二,我可以将 OpenForecast 转换到 J#。这会保留 Java 语法,尽管该代码将在 .NET 公共语言运行库(而不是 Java 虚拟机)的控制下执行。

我决定将 OpenForecast 转换到 J#。这种方法附带的好处是,开放源码开发人员可以只维护一个基于 Java 的 OpenForecast 版本。

将 OpenForecast 转换到 J# 比我想象的要简单。我创建了一个新的 J# 库项目,将其命名为 OpenForecast,然后在其中加载了所有 *.java 源文件。我在源代码中包含了 .NET 版本的 OpenForecast,本文随附有该源代码。我只须顾及几个 MultipleLinearRegression 中的编译错误,实际结果是,有几个 Java 哈希表方法在 J# 中不受支持,如 keySet()entries() 以及哈希表克隆。我还包含了一个 WinForm 应用程序 (TestHarness),您可以使用它来测试转换后的 OpenForecast。

实现 AwRsLibrary 程序集

下一步是创建自定义的 .NET 程序集 AwRsLibrary,它将跨接报表嵌入式代码和 OpenForecast。我将 AwRsLibrary 作为一个 C# 类库项目来实现。在其中,我创建了一个类 RsLibrary,它公开了一个静态(共享)方法 GetForecastedSet。该方法的 AwRsLibrary 代码包含在本文的示例代码中。

GetForecastedSet 方法以数据集数组的形式接收给定产品类别的现有销售数据,以及对预测数据请求的月数。接着,集成 OpenForecast 有五个步骤:

步骤 1:首先,我们创建一个新的 OpenForecast 数据集,并用来自矩阵行数组的现有数据加载它。

步骤 2:接下来,我们获得一个给定的预测模式。OpenForecast 可让开发人员通过调用 getBestForecast 方法,来根据给定数据系列获得最佳的预测数学模型。该方法将检查数据集并尝试几个预测模型,以便选择最理想的一个。如果返回的模型不是很合适,您可以通过实例化在模型项目文件夹下找到的任何类来显式地请求一个预测模型。

步骤 3:接下来,我们准备另一个数据集来保存预测数据,并使用与预测月数一样多的元素来初始化该数据集。

步骤 4:最后,我们调用 forecast 方法来外推数据并返回预测结果。

步骤 5:剩下的最后一件事是将预测数据加载回数据集数组中,以便我们能够将其传回报表嵌入式代码。

在我们完成了 AwRsLibraryOpenForecast 两个 .NET 程序集之后,就需要部署它们。

部署自定义程序集

我们需要将自定义程序集同时部署到报表设计器和报表服务器的二进制文件夹中。自定义程序集的部署过程由下列步骤组成:

  • 将程序集复制到报表设计器和报表服务器的二进制文件夹中。

  • 如果自定义代码需要一个提高的代码访问安全权限集,则需要调整基于代码的安全性。

要让 AwRsLibraryOpenForecast 两个程序集在设计时都可用,我们必须将 AWC.RS.Library.dll 和 OpenForecast.dll 复制到报表设计器文件夹中,其默认位置是 C:\Program Files\Microsoft SQL Server\80\Tools\Report Designer。

同样,要在报表服务器下顺利呈现已部署的报表,我们必须将两个程序集都部署到报表服务器的二进制文件夹中,其默认位置是 C:\Program Files\Microsoft SQL Server\MSSQL\Reporting Services\ReportServer\bin。事实上,如果所引用的自定义程序集尚未全部部署好,报表服务器将不会让您从 Visual Studio .NET IDE 中部署报表。

默认的 Reporting Services 代码访问安全策略在默认情况下对所有自定义程序集授予执行权限。但是,J# 程序集需要完全信任的代码访问权限。因为 .NET 公共语言运行库沿调用堆栈向上遍历来验证所有调用方都具有必要的权限集,所以我们需要将两个程序集的代码访问安全策略提升到完全信任级别。这将需要对报表设计器和报表服务器的安全配置文件进行更改。

为了帮助您设置代码访问安全策略,我提供了我的 Config 文件夹中的 rssrvpolicy.config 的副本。在接近文件末尾的地方,您将看到两个 CodeGroup XML 元素,分别指向 AwRsLibraryOpenForecast 文件。您需要将这些元素复制到报表服务器的安全配置文件 (rssrvpolicy.config) 中。

此外,如果您希望在报表设计器的预览窗口中预览(运行)报表,那么还需要将这些更改传播到报表设计器的安全配置文件 (rspreviewpolicy.config) 中。

在自定义程序集部署之后,我们需要在报表中编写一些 Visual Basic .NET 嵌入式代码来调用 AwRsLibrary 程序集,此内容将在下一部分中进行讨论。

编写报表嵌入式代码

要将报表与 AwRsLibrary 集成,我编写了 GetValue 函数,如清单 2 所示。

清单 2. 嵌入式 GetValue 函数调用 AwRsLibrary 程序集

Dim forecastedSet() As Double  ' array with sales data
Dim productCategoryID As Integer = -1
Dim bNewSeries As Boolean = False
Public Dim m_ExString = String.Empty  ' holds the error message, if any
Function GetValue(productCategoryID As Integer, orderDate As DateTime, sales As Double, reportParameters as Parameters, txtRange as TextBox) As Double
        Dim startDate as DateTime = reportParameters!StartDate.Value
        Dim endDate as DateTime = reportParameters!EndDate.Value
        Dim forecastedMonths as Integer = reportParameters!ForecastedMonths.Value
       
If (forecastedSet Is Nothing) Then
               ReDim forecastedSet(DateDiff(DateInterval.Month, startDate, endDate) +
              forecastedMonths)          #1
       End If
        If Me.productCategoryID <> productCategoryID Then    #2
            Me.productCategoryID = productCategoryID
            bNewSeries = True
            Array.Clear(forecastedSet, 0, forecastedSet.Length - 1)
        End If
        Dim i = DateDiff(DateInterval.Month, startDate , orderDate)
        ' Is this a forecasted value?
        If orderDate <= endDate Then
            ' No, just load the value in the array
            forecastedSet(i) = sales
        Else
            If bNewSeries Then
                   Try
                       AWC.RS.Library.RsLibrary.GetForecastedSet(forecastedSet, forecastedMonths)  #3
                       bNewSeries = False
                    Catch ex As Exception
                           m_ExString  = "Exception: " & ex.Message
                           System.Diagnostics.Trace.WriteLine(ex.ToString())
                          throw ex
                    End Try
            End If
        End If
        Return forecastedSet(i)
    End Function

因为矩阵区域数据单元格使用了一个引用 GetValue 函数的表达式,所以这个函数可由每个数据单元格调用。表 1 列出了 GetValue 函数所采用的输入参数。

表 1. 矩阵区域中的每个数据单元格都将调用 GetValue 嵌入式函数,并传递下列输入参数。

参数

用途

productCategoryID

与单元格相对应的 rowProductCategory 行分组的 ProductCategoryID 值。

orderDate

与单元格相对应的 colMonth 列分组的 OrderDate 值。

sales

该单元格的合计销售总数。

reportParameters

为了计算数组维度,GetValue 需要报表参数的值。我传递了一个对报表参数集合的引用,而不是使用 Parameters!ParameterName.Value 个别地传递参数。

txtRange

保存错误消息的变量,以防在获取预测数据时发生异常。

要了解 GetValue 如何工作,请注意矩阵区域内的每个数据单元格都是来自 forecastedSet 数组的。如果单元格不需要预测(它的相应日期处于请求日期范围内),则我们只需在数组中加载单元格的值,并将其传回以便在矩阵区域中显示它。为了实现此操作,我们需要初始化该数组以获得一个等于请求月数加预测月数的秩。一旦矩阵区域移动到一个新的行并调用我们的函数后,我们就可以通过调用 AwRsLibrary:GetForecastedSet 方法来预测数据了。

实现 Sales by Product Category 交叉表报表

编写报表本身最困难的部分就是设置它的数据,以确保我们在矩阵区域中始终通过正确的列数来显示预测的列。默认情况下,矩阵区域将不显示没有数据的列。这会扰乱从数组传送到单元格的正确偏移量的计算。

因此,我们必须确保数据库返回请求数据范围内所有月份的记录。要实现这一点,我们需要在数据库中预处理销售数据。这正是 spGetForecastedData 存储过程的任务。在这个存储过程中,我用请求数据范围内的所有月度周期预填充了一个自定义表,如清单 3 所示。

清单 3. spGetForecastedData 存储过程确保返回的行集合具有正确的列数

CREATE  PROCEDURE spGetForecastedData (  
  @StartDate smalldatetime, 
  @EndDate smalldatetime
)
AS
DECLARE @tempDate smalldatetime
DECLARE @dateSet TABLE   (       #1
  ProductCategoryID   tinyint,    
  OrderDate    smalldatetime    
  )              
SET   @tempDate = @EndDate
WHILE (@StartDate <= @tempDate)      #2
BEGIN              
  INSERT INTO @dateSet         
  SELECT ProductCategoryID,  @tempDate    
  FROM ProductCategory        
  SET @tempDate = DATEADD(mm, -1, @tempDate)
END
SELECT      DS.ProductCategoryID, PC.Name as ProductCategory, OrderDate AS Date, NULL AS Sales
FROM      @dateSet DS INNER JOIN ProductCategory PC ON DS.ProductCategoryID=PC.ProductCategoryID
UNION ALL            #3
SELECT     PC.ProductCategoryID, PC.Name AS ProductCategory, SOH.OrderDate AS Date, SUM(SOD.UnitPrice * SOD.OrderQty) AS Sales
FROM         ProductSubCategory PSC INNER JOIN
             ProductCategory PC ON PSC.ProductCategoryID = PC.ProductCategoryID INNER JOIN
             Product P ON PSC.ProductSubCategoryID = P.ProductSubCategoryID INNER JOIN
             SalesOrderHeader SOH INNER JOIN
             SalesOrderDetail SOD ON SOH.SalesOrderID = SOD.SalesOrderID ON P.ProductID = SOD.ProductID
WHERE     (SOH.OrderDate BETWEEN @StartDate AND @EndDate)
GROUP BY SOH.OrderDate, PC.Name, PC.ProductCategoryID
ORDER BY PC.Name, OrderDate

最后,我用可获取销售数据的实际 Transact-SQL 语句联合了表 **@dateSet**(其 Sales 列值设置为 NULL)的所有记录。

设置数据集之后,编写报表的其余部分就简单多了。我们对报表的交叉表部分使用了一个矩阵区域。要了解矩阵区域魔法是如何工作并调用嵌入式 GetValue 函数的,您可能要用下列表达式替换 txtSales 文本框的表达式:

图 6 显示了在应用该表达式时,Sales by Product Category 的外观。

erscstcode06

图 6. 矩阵区域如何聚合数据。

如您所见,我们可以轻松地获得相应的行和列组值,矩阵区域将使用这些值来计算区域数据单元格中的聚合值。现在,我们有一种方法来标识每个数据单元格。矩阵区域的设置如表 2 所示。

表 2. 用预测值填充矩阵区域的窍门是让其数据单元格以表达式为基础。

矩阵区域

名称

表达式

rowProductGroup

=Fields!ProductCategory.Value

colYear

colMonth

=Fields!Date.Value.Year

=Fields!Date.Value.Month

数据

txtSales

=Code.GetValue(Fields!ProductCategoryID.Value, Fields!Date.Value, Sum(Fields!Sales.Value), Parameters, ReportItems!txtRange)

要实现预测列(以粗体显示)的条件格式,我对 txtSales 文本框的字体属性使用了下列表达式:

=Iif(Code.IsForecasted(Fields!Date.Value, Parameters!EndDate.Value), "Bold", "Normal")

该表达式调用了报表嵌入式代码中的 IsForecasted 函数。该函数仅仅将销售月度日期与所请求的截止日期相比较,如果销售日期在截止日期之前,则返回 false

最后,剩下的最后一件事情就是使用报表的 References 选项卡来引用 AwRsLibrary 程序集,如我们在图 3 中看到的那样。请注意,对于该报表,我们不需要设置实例名称(不需要在 Classes 网格中输入任何内容),这是因为我们不调用任何实例方法。

调试自定义代码

您可能发现调试自定义代码比较有挑战性。因为这个原因,我想与您分享几个对于调试自定义代码比较有用的技术。

对于调试嵌入式代码来说,没有太多的选择。到目前为止,我所发现的唯一方法就是:当报表在报表设计器中呈现时,使用 MsgBox 函数来输出消息和变量值。确保在将报表部署到报表服务器之前删除对 MsgBox 的调用。如果您没有删除,则所有 MsgBox 调用都将导致异常。因为某些原因,在嵌入式代码内使用 System.Diagnostics.Trace (OutputDebugString API) 跟踪消息会导致“被吞没”,并且既不会在 Visual Studio .NET 输出窗口中显示,也不会在使用一个外部跟踪工具时显示。

在使用外部程序集时,您至少有两个调试选择:

  • 输出跟踪消息。

  • 使用 Visual Studio .NET 调试器来逐句通过自定义代码。

跟踪

例如,在 AwRsLibrary.GetForecastedSet 方法中,我正在使用 System.Dianogistics.Trace.WriteLine 输出跟踪消息,以显示观察值和预测值。要想在 Visual Studio .NET 或报表服务器中运行报表时查看这些消息,您可以使用 Mark Russinovich 开发的出色的 DebugView 工具,如图 7 所示。

erscstcode07

图 7. 在 DebugView 中输出来自外部程序集的跟踪消息。

调试自定义代码

您还可以通过将 Visual Studio .NET 调试器附加到报表设计器进程,来逐句通过自定义程序集代码,如下所示:

  • 在 Visual Studio .NET 的一个新实例中打开您要调试的自定义程序集。像往常一样在您的代码中设置断点。

  • 在您的自定义程序集项目属性中,选择 Configuration Properties->Debugging,然后将 Debug Mode 设置为 Wait to Attach to an External Process

  • 在 Visual Studio .NET 的另一个实例中打开您的商业智能项目。

  • 返回到自定义程序集项目,单击 Debug 菜单,然后单击 Processes...。找到承载商业智能项目的 devevn 进程,并附加到它上面。在 Attach To Process 对话框中,确保 Common Language Runtime 复选框已选中,然后单击 Attach。此时,您的 Processes 对话框应该类似于图 8 中所示的对话框。

    ERSCstCode08_thumb

    图 8. 要调试自定义程序集,应连接到承载商业智能项目的 Visual Studio 实例。

在我的示例中,我想在 Sales by Product Category 报表调用 AwRsLibrary 程序集时,调试其中的代码。出于这个原因,我在 AwRsLibrary 项目中连接到 AWReporter devenv 进程。

  • 在商业智能项目中,预览调用自定义程序集的报表。或者,如果您已经在预览报表,则单击 Preview Tab 工具栏上的 Refresh Report 按钮。此时,您的断点应该被 Visual Studio .NET 调试器发现。

您很快就会发现,如果需要更改代码并重新编译自定义程序集,则在尝试将其重新部署到报表设计器文件夹时,会导致下列异常:

Cannot copy <assembly name>: It is being used by another person or program.

问题在于 Visual Studio .NET IDE 保留了对自定义程序集的引用。您需要关闭 Visual Studio .NET,然后重新部署新的程序集。要避免此问题,您可以使用 Report Host(预览窗口)来调试自定义程序集代码。要进行此操作,请执行以下步骤:

  • 将自定义程序集添加到包含商业智能项目的 Visual Studio .NET 解决方案中。

  • 将商业智能项目的起始项更改为调用自定义代码的报表,如图 9 所示。

    ERSCstCode09_thumb

    图 9. 使用 Report Host 调试选项来避免锁定程序集。

  • 按下 F5 在预览窗口中运行报表。当报表调用自定义代码时,您的断点将被捕获。

在使用预览窗口方法时,Visual Studio .NET 不会锁定自定义程序集。这可让您将程序集的生成位置更改到报表设计器文件夹,以便在您重新生成程序集时,它始终包含最新的副本。在预览窗口中运行项目,是报表设计器配置文件 (rspreviewpolicy.config) 中指定的代码访问安全策略设置的一个主题。

小结

在本文中,我们了解到如何将报表与我们或其他人编写的自定义代码集成在一起。

对于简单的报表专用编程逻辑,可以使用嵌入式 Visual Basic .NET 代码。当代码的复杂性增加或者您希望使用 Visual Basic .NET 以外的编程语言时,可以将您的代码移到外部程序集中。

使用自定义代码只是开发人员扩展 Reporting Services 的几种方法之一。要了解有关 Reporting Services 扩展性的详细信息,请参阅 Reporting Services 联机丛书的“Extending Reporting Services”部分。

更多信息:

http://www.microsoft.com/sql/

相关书籍:

Microsoft Reporting Services in Action

转到原英文页面