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

使用报表定义自定义扩展 (RDCE) 动态指向 SQL 报表服务的共享数据源

2012年3月27日

Ms-PL

30分钟阅读

viewsIcon

190747

downloadIcon

5857

本文详细介绍如何通过设置 RDCE 并利用一些技巧,动态地指向给定的共享数据源引用。

目录

  1. 引言
    1. 问题所在
    2. 提出的解决方案概述
  2. 背景
  3. 准备共享数据源(报表服务器端)
  4. 准备报表(示例报表项目)
  5. RSExplorer++ 工具
    1. 配置 RSExplorer++ 工具
    2. 使用 RSExplorer++ 工具
    3. RSExplorer++ 工具的工作原理是什么?
  6. RDCE
    1. 理解 RDL 文件
    2. 创建 RDCE
    3. 使报表服务器能够识别和实现 RDCE
    4. 调试 RDCE
  7. 报表查看器 Web 应用程序
    1. 输入
    2. 输出
  8. 特殊考虑 #1:无参数或与数据源无关的参数的报表
    1. 所需操作
  9. 特殊考虑 #2:带有一个数据源依赖参数的报表
    1. 为何这很重要?
    2. 如何解决?
  10. 特殊考虑 #3:带有钻取式数据源依赖参数的报表
    1. 为何这很重要?
    2. 如何解决?
  11. 结论
  12. 参考文献

1. 介绍

您是否选择了 Microsoft SQL Reporting Services 作为您的报表策略?您是否在 RDL 文件中使用共享数据源而不是嵌入式数据源?您是否有多个环境(生产、质量保证、开发等)?您是否希望能够只部署一次 RDL 文件,然后让报表服务器自动判断要指向哪个环境?

如果您对所有问题的回答都是“是”,那么本文适合您。

本文可能看起来很长,但我已尽力使其易于理解。一旦您通读一遍,就会发现当您熟悉这个过程后,它会变得简单明了。

1.1. 问题所在

目前,要在运行时更改报表数据源,报表需要使用客户端处理模型(RDLC 文件)。如果我们想使用服务器端处理模型(部署到报表服务器的 RDL 文件)来实现这一点,至今还没有解决方案。

传统方法如下图所示

Conventional Reporting Server Approach

图 1. 传统报表服务器方法

1.2. 提出的解决方案概述

微软对这个问题的解决方案是引入“基于表达式的连接字符串”(参考这篇文章),但不幸的是,这不适用于共享数据源。

幸运的是,通过使用一个工具、一些报表设计标准和一个报表定义自定义扩展(RDCE),可以更改 RDL 文件中的数据源引用,使其指向正确的服务器-数据库对。

解决方案的概述如下图所示

Proposed Solution Overview

图 2. 提出的解决方案概述

2. 背景知识

要理解本文,您需要:

注意:本文中使用的数据库基于 AdventureWorks 数据库(您可以从这里下载)。

我将只使用两个数据库(但模拟我们有四个)

  • “AdventureWorksDW” 将作为 DEVELOPMENT 和 TEST,
  • “AdventureWorksDW2008” 将作为 PRODUCTION 和 QUALITY_ASSURANCE。
  • 我修改了 DEVELOPMENT 数据库中的数据,以便在指向 PRODUCTION 或 DEVELOPMENT 时能看到差异。

PRODUCTION 中的数据示例如下

PRODUCTION DimEmployee Table Contents

图 3. PRODUCTION 中 DimEmployee 表的内容

DEVELOPMENT 中的数据示例如下(请注意,我在某些列中添加了“Other Data Source”或“ODS”,以便在指向它时能明显区分)

DEVELOPMENT DimEmployee Table Contents

图 4. DEVELOPMENT 中 DimEmployee 表的内容

3. 准备共享数据源(报表服务器端)

要使用 Web 浏览器连接到您的报表服务器,请使用 http://<your_report_server>/Reports。在本文中,我们将在主页级别设置两个文件夹

  • Data Sources,用于存放共享数据源定义。
  • Reports,用于存放已部署的报表(RDL 文件)。

The Report Server Environment

图 5. 报表服务器环境

正如这篇文章所描述的:“共享数据源为数据源指定连接属性。如果您有一个被大量报表、模型或数据驱动订阅使用的数据源,请考虑创建一个共享数据源,以消除在多个地方维护相同连接信息的开销。”

要创建和管理共享数据源,请参阅文章:创建、修改和删除共享数据源 (SSRS)

请确保您的报表服务器包含所有您需要的共享数据源。在本文中,我将使用

  • Data Sources/DEVELOPMENT
  • Data Sources/QUALITY_ASSURANCE
  • Data Sources/TEST
  • Data Sources/PRODUCTION

Data Sources at the Report Server

图 6. 报表服务器上的数据源

请确保所有数据源都根据您的环境正确配置。下图显示了本文将使用的 DEVELOPMENT 数据库的配置

Data Sources Contents

图 7. DEVELOPMENT 数据源内容

4. 准备报表(示例报表项目)

您可以使用“Business Intelligence Development Studio”或“报表生成器”来设计和部署报表。有关“Business Intelligence Development Studio”的概述,您可以阅读这篇文章:Business Intelligence Development Studio 中的 Reporting Services (SSRS)。有关“报表生成器”的概述,您可以阅读这篇文章:报表生成器 3.0 入门。在本节中,我将使用“Business Intelligence Development Studio”。

以下内容适用于示例报表项目

  1. 请检查项目属性是否根据您的环境正确配置。
    • TargetDataSourceFolder = Data Sources (与图 5 保持一致)
    • TargetReportFolder = Reports (与图 5 保持一致)
    • TragetServerURL = http://<your_report_server>/reportserver

    Sample Reports Project Properties

    图 8. 示例报表项目属性
  2. 添加您报表服务器上已有的共享数据源(我将使用图 6 中的四个)
    • DEVELOPMENT
    • QUALITY_ASSURANCE
    • TEST
    • PRODUCTION

    Shared Data Sources on the Sample Reports Project

    图 9. 示例报表项目中的共享数据源
  3. 确保每个数据源都配置正确。在下图中,我展示了应与图 7 中的设置相匹配的配置。
  4. Shared Data Source Properties 1

    图 10. 共享数据源常规属性

    Shared Data Source Properties 2

    图 11. 共享数据源常规凭据属性
  5. 该项目包含三个报表。在本节中,我将重点介绍“NamesReport.rdl”,尽管它们都遵循相同的约定。
  6. Reports on the Sample Reports Project

    图 12. 示例报表项目中的报表
  7. NamesReport.rdl”的设计视图如下所示
  8. Design view on NamesReport.rdl

    图 13. NamesReport.rdl 的设计视图
  9. 为了让报表做好准备,它们需要引用可用的共享数据源。为此,请打开“报表数据”窗口(在 Visual Studio [Business Intelligence Development Studio] 中按“Ctrl + Alt + D”或转到“视图 - 报表数据”)。您应该会看到以下窗口
  10. The Report Data Window

    图 14. 报表数据窗口
  11. 在“数据源”文件夹中,您应该有与项目级别完全相同的数据源
    • DEVELOPMENT
    • QUALITY_ASSURANCE
    • TEST
    • PRODUCTION

    Data Sources at the Report Level

    图 15. 报表级别的数据源
  12. 每个数据源的属性都应指向报表项目级别的共享数据源
  13. Data Source at the Report Level Properties

    图 16. 报表级别的数据源常规属性
  14. 现在让我们看一下报表级别的数据集
  15. Data Sets at the Report Level

    图 17. 报表级别的数据集
  16. 每个数据集的属性都必须配置为指向 PRODUCTION 数据源,如下所示
  17. Data Sets Properties at the Report Level

    图 18. 报表级别的数据集查询属性
  18. 如果我们预览该报表,它应该看起来像下面这样,指向 PRODUCTION 数据
  19. Report Preview

    图 19. NamesReport 报表预览
  20. 部署报表(在报表上右键单击 - 部署)。
  21. 现在,相同的报表应该在服务器上可见(它指向 PRODUCTION 数据)
  22. Report at the Server Level

    图 20. 服务器级别的报表

总结一下,需要的重要步骤是

  • 报表项目需要包含报表服务器上所有的共享数据源。
  • 每个报表都需要包含所有指向报表服务器上共享数据源的引用。
  • 每个报表中的所有数据集都需要指向 PRODUCTION 数据源。

5. RSExplorer++ 工具

此工具基于 Microsoft Reporting Services 产品示例中的“RSExplorer 示例应用程序”构建。您可以在此处下载原始工具:RSExplorer 示例应用程序。请注意,此工具遵循微软公共许可证(Ms-PL)。

您可以在页面顶部下载本节中使用的 RSExplorer++ 工具的源代码和安装程序。

原始的 Microsoft RSExplorer 示例应用程序允许您:

  • 浏览您的报表服务器
  • 预览您的报表
  • 查看项目的属性

此外,RSExplorer++ 工具还允许您:

  • 启用/禁用报表的 RDCE 功能(稍后解释)。
  • 向报表添加数据源参数(稍后解释)。
  • 检查您的报表是否启用了 RDCE 并具有数据源参数(稍后解释)。

5.1. 配置 RSExplorer++ 工具

App.config 文件中,确保 MSQLSRS_ConnectionString 连接字符串可以直接访问您的 ReportServer 数据库,如下所示

<connectionStrings>
    <add name="MSQLSRS_ConnectionString" 
      connectionString="Data Source=(local);Initial Catalog=ReportServer;User ID=UserReader;Password=XXXXXXX" 
      providerName="System.Data.SqlClient" /> 
</connectionStrings>
代码示例 1. [XML] RSExplorer++ 工具 Web.config 中的 "connectionStrings" 部分。

5.2. 使用 RSExplorer++ 工具

  1. 在“Server Address”文本框中输入您的报表服务器地址,格式如下:http://<your_report_server>/reportserver,然后点击“Go”。您应该能看到根目录下的文件夹。
  2. Reports Explorer first screen

    图 21. RSExplorer++ 工具的初始界面
  3. 点击 Reports 文件夹,您应该会看到在本文第 4 节中部署的三个报表。这三个报表最初的“RDCE Enabled”将设置为 False,“DS Parameter”设置为 N/A,“Used In Query”也设置为 N/A。注意:为了能够在运行时更改数据源,这三个值都需要设置为 true。接下来的步骤将说明如何实现这一点。
  4. Reports Explorer at the Reports Level

    图 22. RSExplorer++ 工具在报表级别的界面
  5. 如果您双击一个报表,例如第一个,您将能够预览它。
  6. Report Preview from the RSExplorer++ tool

    图 23. 从 RSExplorer++ 工具预览报表
  7. 如果您选择一个报表并点击屏幕右上角的“Show Properties”,右侧的框中将填充报表属性信息。
  8. Report Properties from the RSExplorer++ tool

    图 24. 从 RSExplorer++ 工具查看报表属性
  9. 如果您选择一个报表并点击屏幕右上角的“Enable RDCE”,该报表的记录颜色应变为橙色,“RDCE Enabled”的值应变为 True,“DS Parameter”的值应变为 False。这现在意味着这个特定的报表在呈现之前会调用 RDCE。
  10. Enabling RDCE on a report from the RSExplorer++ tool

    图 25. 从 RSExplorer++ 工具在报表上启用 RDCE
  11. 如果您再次选择该报表并点击屏幕右上角的“Show Properties”,您会看到一个名为 RDCE 的新属性,其值为“RDCE”。有关实现这一点的后台代码的详细信息,请参见“RSExplorer++ 工具的工作原理是什么?”部分。
  12. The RDCE property on a report from the RSExplorer++ tool

    图 26. 从 RSExplorer++ 工具查看报表的 RDCE 属性
  13. 如果您双击该报表,您不会注意到任何变化,但实际上,RDCE 现在正在被调用。注意:有关此信息,请参见“RDCE”部分。
  14. 如果您选择该报表并点击屏幕右上角的“Add DS Parameter/Use In Query”,将执行两个应属于一个事务的操作:
    • 一个报表参数将被添加到 RDL(报表定义)文件中。此参数将用于确定报表需要指向哪个数据源。
    • 应通知报表服务器这个新参数是“Used In Query”,以便 RDCE 能够看到该值。如果您在数据集中使用该参数,则该值在报表设计器工具的报表级别被设置为 True。为了避免这样做,我使用了一个技巧来告诉服务器该参数正在“Used In Query”,即使理论上并非如此。有关实现此功能的后台代码的详细信息,请参阅“RSExplorer++ 工具的工作原理是什么?”部分。

    如果报表参数已添加但未执行“Used In Query”操作,这意味着在“配置 RSExplorer++ 工具”部分指定的连接字符串是错误的,您将看到一个红色高亮的记录。修复连接字符串,然后再次点击“Add DS Parameter/Use In Query”。

    'Add DS Parameter/Use In Query' transaction failure from the RSExplorer++ tool

    图 27. RSExplorer++ 工具中 'Add DS Parameter/Use In Query' 事务失败

    如果事务成功,报表记录应变为绿色。这表示该报表已完全准备好动态指向给定的数据源。

    'Add DS Parameter/Use In Query' transaction successful from the RSExplorer++ tool

    图 28. RSExplorer++ 工具中 'Add DS Parameter/Use In Query' 事务成功
  15. 您现在可以双击报表,通过在参数中输入数据源名称并按“查看报表”来测试它是否指向正确的数据源。注意:这些测试仅在您已配置报表服务器以启用 RDCE 的情况下才能正常工作。更多参考信息请见第 6 节“RDCE”。
    • DEVELOPMENT
    • QUALITY_ASSURANCE
    • TEST
    • PRODUCTION

    Testing a report from the RSExplorer++ tool pointing to DEVELOPMENT

    图 29. 从 RSExplorer++ 工具测试指向 DEVELOPMENT 的报表

    请注意,在报表的右上角添加了文本以指示报表指向哪个数据源。由于本文第 2 节对“DEVELOPMENT”数据库所做的更改,我们可以看到数据与 PRODUCTION 报表不同。

    Testing a report from the RSExplorer++ tool pointing to PRODUCTION

    图 30. 从 RSExplorer++ 工具测试指向 PRODUCTION 的报表

总结一下,需要的重要步骤是:

  • 报表需要启用 RDCE。
  • 报表需要包含数据源参数。
  • 报表需要将数据源参数的 "Used In Query" 属性设置为 true。

5.3. RSExplorer++ 工具的工作原理是什么?

检查报表是否启用了 RDCE:(此功能用于在 RSExplorer++ 工具中将“RDCE Enabled”属性设置为 True 或 False)

Microsoft.SqlServer.ReportingServices2010.Property[] props = 
   new Microsoft.SqlServer.ReportingServices2010.Property[1];
Microsoft.SqlServer.ReportingServices2010.Property setProp = 
   new Microsoft.SqlServer.ReportingServices2010.Property();
setProp.Name = "RDCE";
setProp.Value = "RDCE";
props[0] = setProp;

Microsoft.SqlServer.ReportingServices2010.Property[] current_props = 
          rs.GetProperties("Reports/NamesReport", props);
if (current_props.Length == 0)
{
    //The Report is not RDCE Enabled
    //RDCE Enabled = False
}
else
{
    //The Report is RDCE Enabled
    //RDCE Enabled = True
}
代码示例 2. [C#] 检查报表是否启用了 RDCE

检查报表是否具有数据源参数以及该参数是否被“用于查询”:(此实现用于在 RSExplorer++ 工具中将“DS Parameter”和“Used In Query”属性设置为 True 或 False)

bool forRendering = false;
string historyID = null;
ParameterValue[] values = null;
DataSourceCredentials[] credentials = null;
ItemParameter[] parameters = null;
parameters = rs.GetItemParameters("/Reports/NamesReport", 
                   historyID, forRendering, values, credentials);

bool hasParameter = false;
bool usedInQuery = false;
foreach (ItemParameter ip in parameters)
{
    //RDCE_Report_Data_Source is the standardized name of the Data Source parameter
    if (ip.Name == "RDCE_Report_Data_Source")
    {
        //DS Parameter = True
        hasParameter = true;
        if (ip.QueryParameter == true)
        {
            //Used In Query = True
            usedInQuery = true;
        }
    }
}
代码示例 3. [C#] 检查报表是否具有数据源参数以及是否“用于查询”

在报表上启用 RDCE

Microsoft.SqlServer.ReportingServices2010.Property[] props = 
       new Microsoft.SqlServer.ReportingServices2010.Property[1];
Microsoft.SqlServer.ReportingServices2010.Property setProp = 
       new Microsoft.SqlServer.ReportingServices2010.Property();
setProp.Name = "RDCE";
setProp.Value = "RDCE";
props[0] = setProp;

Microsoft.SqlServer.ReportingServices2010.Property[] current_props = 
          rs.GetProperties("/Reports/NamesReport", props);
rs.SetProperties(selItem.Path, props);
代码示例 4. [C#] 在报表上启用 RDCE

在报表上禁用 RDCE

Microsoft.SqlServer.ReportingServices2010.Property[] props = 
         new Microsoft.SqlServer.ReportingServices2010.Property[1];
Microsoft.SqlServer.ReportingServices2010.Property setProp = 
         new Microsoft.SqlServer.ReportingServices2010.Property();
setProp.Name = "RDCE";
setProp.Value = "";
props[0] = setProp;

Microsoft.SqlServer.ReportingServices2010.Property[] current_props = 
          rs.GetProperties("/Reports/NamesReport", props);
rs.SetProperties(selItem.Path, props);
代码示例 5. [C#] 在报表上禁用 RDCE

添加 “RDCE_Report_Data_Source” 参数

byte[] reportDefinitionProcessed;

MemoryStream mstream = null;
System.Xml.XmlDocument doc = new System.Xml.XmlDocument();

//Get RDL from Report Server
using (mstream = new MemoryStream(rs.GetItemDefinition("/Reports/NamesReport")))
{
    doc.Load(mstream);
    mstream.Position = 0;
}

//Load RDL to a XElement
XElement xreport = XElement.Load(new XmlNodeReader(doc));

string dns = "{" + xreport.GetDefaultNamespace() + "}";

//Prepare the parameter
var entry = new XElement(dns + "ReportParameter");
entry.SetAttributeValue("Name", "RDCE_Report_Data_Source");
entry.Add(new XElement(dns + "DataType", "String"));
entry.Add(new XElement(dns + "Prompt", "Report Server:"));

//Check if report already has parameters
bool hasParameters = xreport.Element(dns + "ReportParameters") != null;

if (!hasParameters)
{
    //If it doesn't have parameters, add the "ReportParameters" element
    XElement parent = new XElement(dns + "ReportParameters");
    parent.Add(entry);

    xreport.Element(dns + "DataSets").AddAfterSelf(parent);
}
else
{
    //If it already has parameters, add as first parameter
    xreport.Element(dns + "ReportParameters").AddFirst(entry);
}

System.Text.Encoding encoding = new System.Text.UTF8Encoding();
reportDefinitionProcessed = encoding.GetBytes(xreport.ToString());

//Save new RDL to the Report Server
rs.SetItemDefinition("/Reports/NamesReport", reportDefinitionProcessed, null);
代码示例 6. [C#] 添加 "RDCE_Report_Data_Source" 参数

将参数的“Used In Query”设置为 true:这里比较棘手,因为报表服务器知道一个参数是否被用于查询,并不是在 RDL 层面。如果您在添加 RDCE_Report_Data_Source 参数之后,但在将“Used In Query”设置为 true 之前,对您的报表服务器运行以下 SQL 语句,您将得到如下结果。

SELECT [Parameter]
  FROM [ReportServer].[dbo].[Catalog]
  WHERE [Path] = '/Reports/NamesReport'
代码示例 7. [SQL] 查询检查报表是否“用于查询”
<Parameters>
  <UserProfileState>0</UserProfileState>
  <Parameter>
    <Name>RDCE_Report_Data_Source</Name>
    <Type>String</Type>
    <Nullable>False</Nullable>
    <AllowBlank>False</AllowBlank>
    <MultiValue>False</MultiValue>
    <UsedInQuery>False</UsedInQuery>
    <State>MissingValidValue</State>
    <Prompt>Report Server:</Prompt>
    <DynamicPrompt>False</DynamicPrompt>
    <PromptUser>True</PromptUser>
  </Parameter>
</Parameters>
代码示例 8. [XML] 检查报表是否“用于查询”

目标是直接在数据库中将 "UsedInQuery" 行更改为 True。这可以通过以下方式实现:

string xml_parameters = "";

SqlConnection conn = new SqlConnection(
  ConfigurationManager.ConnectionStrings["MSQLSRS_ConnectionString"].ConnectionString);
SqlCommand command = new SqlCommand(string.Format("SELECT [Parameter]  " + 
   "FROM [dbo].[Catalog]  WHERE [Path] = '{0}'", "/Reports/NamesReport"), conn);
command.CommandType = CommandType.Text;

conn.Open();
SqlDataReader reader = command.ExecuteReader();

while (reader.Read())
{
    //Get current paramaters XML
    xml_parameters = reader["Parameter"].ToString();
}

reader.Close();
conn.Close();

MemoryStream mstream = null;
byte[] ascii = System.Text.Encoding.UTF8.GetBytes(xml_parameters);
System.Xml.XmlDocument doc = new System.Xml.XmlDocument();
using (mstream = new MemoryStream(ascii))
{
    doc.Load(mstream);
    mstream.Position = 0;
}

XElement xparameters = XElement.Load(new XmlNodeReader(doc));

var dsds = xparameters.Elements("Parameter");
foreach (XElement xe in dsds)
{
    if (xe.Element("Name").Value == "RDCE_Report_Data_Source")
    {
        //This is where the UsedInQuery element is changed to True
        XElement temp = xe.Element("UsedInQuery");
        temp.Value = "True";
    }
}

string new_parameters = xparameters.ToString();

conn = new SqlConnection(ConfigurationManager.ConnectionStrings["MSQLSRS_ConnectionString"].ConnectionString);
command = new SqlCommand(
    string.Format("UPDATE [dbo].[Catalog]   SET [Parameter] = '{0}'      " + 
           "WHERE [Path] = '{1}'", new_parameters, "/Reports/NamesReport"), conn);
command.CommandType = CommandType.Text;

conn.Open();

//Changes are written to the database
reader = command.ExecuteReader();

while (reader.Read())
{
}

reader.Close();
conn.Close();
代码示例 9. [C#] 直接在数据库中将 "UsedInQuery" 行更改为 True

这总结了 RSExplorer++ 工具在“幕后”所做的工作。下一节将详细解释我们为什么需要执行所有这些步骤。

6. RDCE

报表定义自定义扩展 (RDCE) 是 SQL Server 2008 中引入的一项报表服务器可扩展性功能。它允许我们在运行时自定义报表定义 RDL,而无需将新的 RDL 写回报表服务器。它基本上是从服务器获取 RDL,对其进行自定义,然后将自定义后的版本发送到报表查看器。

6.1. 理解 RDL 文件

报表定义语言 (RDL) 是一种基于 XML 的文件,它代表了定义报表服务器报表的元数据。以下文章概述了 RDL 模式:报表定义概述图。我们可以在第 4 节中我们一个报表的后台代码中看到 RDL 的一些元素

  • 正文
  • 页面
  • DataSources
  • DataSets
  • ReportParameters

An example of an RDL

图 31. RDL 示例

报表定义自定义扩展允许您仅自定义报表定义的以下元素:

  • 正文
  • DataSets
  • 页面
  • PageFooter
  • PageHeader

Customizable Elements of an RDL file

图 32. RDL 文件的可自定义元素

这意味着以下元素在运行时无法自定义

  • DataSources
  • ReportParameters

Non-Customizable Elements of an RDL file

图 33. RDL 文件的不可自定义元素

这意味着我们无法在运行时添加报表参数(这是合理的,因为这些参数需要在报表本身加载之前加载,并决定我们从报表中获取的数据)。这也意味着我们无法修改数据源。这正是关键所在!如果我们能够修改数据源,我们只需更改数据源的“DataSourceReference”节点,使其指向我们想要的那个。基于此,我们需要在报表设计期间将所有数据源添加到报表中(见第 4 节),然后让 RDCE 更改数据集所指向的数据源。

让我们看一下我们其中一个报表的 DataSources 元素

<DataSources>
    <DataSource Name="DEVELOPMENT">
      <DataSourceReference>DEVELOPMENT</DataSourceReference>
      <rd:SecurityType>None</rd:SecurityType>
      <rd:DataSourceID>XXXX-YYYY-ZZZZ</rd:DataSourceID>
    </DataSource>
    <DataSource Name="PRODUCTION">
      <DataSourceReference>PRODUCTION</DataSourceReference>
      <rd:SecurityType>None</rd:SecurityType>
      <rd:DataSourceID>AAAA-YYYY-ZZZZ</rd:DataSourceID>
    </DataSource>
    <DataSource Name="QUALITY_ASSURANCE">
      <DataSourceReference>QUALITY_ASSURANCE</DataSourceReference>
      <rd:SecurityType>None</rd:SecurityType>
      <rd:DataSourceID>WWWWW-YYYY-ZZZZ</rd:DataSourceID>
    </DataSource>
    <DataSource Name="TEST">
      <DataSourceReference>TEST</DataSourceReference>
      <rd:SecurityType>None</rd:SecurityType>
      <rd:DataSourceID>MMMMMM-YYYY-ZZZZ</rd:DataSourceID>
    </DataSource>
</DataSources>
代码示例 10. [XML] RDL 文件的 DataSources 元素

现在让我们看一下同一报表的 DataSets 元素

<DataSets>
    <DataSet Name="ResultsDataSet">
      <Query>
        <DataSourceName>PRODUCTION</DataSourceName>
        <CommandText>SELECT      DISTINCT  FirstName, LastName
        FROM            DimEmployee
        ORDER BY FirstName, LastName
    </CommandText>
      </Query>
      <Fields>
        <Field Name="FirstName">
          <DataField>FirstName</DataField>
          <rd:TypeName>System.String</rd:TypeName>
        </Field>
        <Field Name="LastName">
          <DataField>LastName</DataField>
          <rd:TypeName>System.String</rd:TypeName>
        </Field>
      </Fields>
    </DataSet>
</DataSets>
代码示例 11. [XML] RDL 文件的 DataSets 元素

由于我们无法对 DataSources 元素进行任何操作,RDCE 将更改每个 DataSets 中的 DataSourceName 元素,以指向不同的 DataSource。重要的是,DataSets 默认指向 PRODUCTION,因为那是要被替换的 DataSourceName

注意:要指向的数据源必须存在于报表定义中。这意味着该报表在运行时只能指向以下数据源之一:

  • DEVELOPMENT
  • QUALITY_ASSURANCE
  • TEST
  • PRODUCTION

6.2. 创建 RDCE

RDCE 是一个 Visual Studio 项目,它将生成一个类库 (DLL)。我们将程序集命名为“rs.rdce”,默认命名空间为“RS.Extensibility”。如果需要,可以点击“项目 - RDCE 属性”进行配置。

RDCE Project Properties

图 34. RDCE 项目属性

生成事件的生成后事件命令行最好配置为指向您的 Reporting Services bin 文件夹。这将在生成后将 DLL 文件放置在正确的位置,并允许您进行调试。

RDCE Project Properties Build Events

图 35. RDCE 项目属性的生成事件

命令应如下所示(请检查您环境中 Reporting Services bin 的路径是否正确)

copy "$(TargetDir)$(TargetName).*" 
   "C:\Program Files\Microsoft SQL Server\MSRS10_50.MSSQLSERVER\Reporting Services\ReportServer\bin"
代码示例 12. [脚本] 生成事件命令

该项目需要引用 Microsoft.ReportingServices.Interfaces,该引用可在 "C:\Program Files\Microsoft SQL Server\100\SDK\Assemblies" 中找到。

RDCE Solution Explorer

图 36. RDCE 解决方案资源管理器

如您所见,该项目只有一个类 ReportDefinitionCustomizationExtension,它实现了在 Microsoft.ReportingServices.Interfaces 中找到的 IReportDefinitionCustomizationExtension 接口。该接口公开了我们将重点关注的 ProcessReportDefinition 方法。有关此方法参数和返回值的详细信息,请参阅这篇文章:IReportDefinitionCustomizationExtension.ProcessReportDefinition 方法

bool ProcessReportDefinition(
    byte[] reportDefinition,
    IReportContext reportContext,
    IUserContext userContext,
    out byte[] reportDefinitionProcessed,
    out IEnumerable<RdceCustomizableElementId> customizedElementIds
)
代码示例 13. [C#] ProcessReportDefinition 方法

如您所见,该方法接收原始报表定义 (reportDefinition)、报表上下文 (reportContext) 和用户会话上下文 (userContext)。该方法的输出是自定义后的报表定义 (reportDefinitionProcessed) 和被自定义的元素集合 (customizedElementIds),可以是以下内容

  • 正文
  • DataSets
  • 页面
  • PageFooter
  • PageHeader

如果报表定义被自定义,该方法将返回 `true`,否则返回 `false`。

reportContext 参数对于实现我们的目标至关重要,因为它包含了 QueryParameters 列表及其值。这就是为什么 RSExplorer++ 工具将 RDCE_Report_Data_Source 的“Used In Query”属性设置为 True 变得极其重要,因为它使我们能够获取该值并用它来定制我们的报表。

if (reportContext.QueryParameters.Count == 0)
{
    //The report has no "Used In Query" parameters
    //RDL is not customized
    return false; 
}

if (reportContext.QueryParameters["RDCE_Report_Data_Source"] == null)
{
    //The "RDCE_Report_Data_Source" parameter was not found
    //RDL is not customized
    return false; 
}

if (reportContext.QueryParameters["RDCE_Report_Data_Source"].Values.Length == 0)
{
    //The "RDCE_Report_Data_Source" was found but has no value
    //RDL is not customized
    return false; 
}

//The "RDCE_Report_Data_Source" was found and has a value
string ParamValue = reportContext.QueryParameters["RDCE_Report_Data_Source"].Values[0].ToString();
代码示例 14. [C#] 检查 "RDCE_Report_Data_Source" 参数

既然我们知道“RDCE_Report_Data_Source”参数有值,我们就可以继续自定义报表了。

//We load the report definition to an XML document in memory and then to
//an XElement for easier manipulation
MemoryStream mstream = null;
System.Xml.XmlDocument doc = new System.Xml.XmlDocument();
using (mstream = new MemoryStream(reportDefinition))
{
    doc.Load(mstream);
    mstream.Position = 0;
}                

XElement xreport = XElement.Load(new XmlNodeReader(doc));

//We get the XML namespaces yo be able to work with them later on
string dns = "{" + xreport.GetDefaultNamespace() + "}";
string seconddns = "";

if (xreport.GetNamespaceOfPrefix("rd") != null)
{
    seconddns = "{" + xreport.GetNamespaceOfPrefix("rd") + "}";
}
代码示例 15. [C#] 自定义的第一阶段

“RDCE_Report_Data_Source” 参数的值应为以下之一:

  • DEVELOPMENT
  • QUALITY_ASSURANCE
  • TEST
  • PRODUCTION

我们需要检查作为参数接收的数据源是否存在

//Get the DataSources
var data_sources = xreport.Element(dns + "DataSources").Elements(dns + "DataSource");
//Go through the DataSources to see if the Shared Data Source we are trying to
//point to exists
bool dataSourceExists = false;
foreach (XElement xe_ds in data_sources)
{
    //The DataSources must have the DataSourceReference element for them to
    //be pointing to Shared Data Sources
    bool isSharedDataSource = xe_ds.Element(dns + "DataSourceReference") != null;
    if (isSharedDataSource)
    {
        if ((xe_ds.Element(dns + "DataSourceReference").Value).ToUpper() == ParamValue.ToUpper())
        {
            dataSourceExists = true;
        }
    }
}

if (!dataSourceExists)
{
    //The shared data source we are trying to point to does not exist
    //RDL is not customized
    return false; 
}
代码示例 16. [C#] 检查我们尝试指向的数据源是否存在

现在我们需要遍历数据集,将它们指向我们想要的数据源。

//Get the DataSets
var data_sets_data_sources = xreport.Element(dns + "DataSets").Elements(dns + "DataSet");
//Go through the DataSets
foreach (XElement xe in data_sets_data_sources)
{
    //Check if the DataSet is pointing to the default data source to be replaced
    //defined at the beggining of the class:
    //private const string dataSourceToReplace = "PRODUCTION";
    if ((xe.Element(dns + "Query").Element(dns + 
         "DataSourceName").Value).ToUpper() == dataSourceToReplace.ToUpper())
    {
        //Change the data source to point to the one we want to point to
        //This is the key part of the algorithm!
        xe.Element(dns + "Query").Element(dns + "DataSourceName").Value = ParamValue.ToUpper();
        datasourcewaschanged = true;   
    } 
}

if (!datasourcewaschanged)
{
    //There are no data sources pointing to the default data source to be replaced
    //Nothing was changed
    //RDL is not customized
    return false;
}
代码示例 17. [C#] 检查数据集是否指向要替换的默认数据源

“特殊考虑 3:带有钻取数据源相关参数的报表”区域内的代码块将在本文第 10 节中解释。

为了确认 RDCE 自定义了报表,以下代码向报表添加了一个文本框,以指示报表现在指向哪个数据源。

//Create a text box to be added to the report
//to indicate that the RDCE processed it
//and customized it
var entry = new XElement(dns + "Textbox");
entry.SetAttributeValue("Name", "tb_RDCE_DataSource");
entry.Add(new XElement(dns + "CanGrow", "true"));
entry.Add(new XElement(dns + "KeepTogether", "true"));

var paragraphs = new XElement(dns + "Paragraphs");                   

var paragraph = new XElement(dns + "Paragraph");

var textruns = new XElement(dns + "TextRuns");
var textrun = new XElement(dns + "TextRun");
textrun.Add(new XElement(dns + "Value", "Data Source: " + ParamValue.ToUpper()));

var style = new XElement(dns + "Style");
style.Add(new XElement(dns + "FontFamily", "Tahoma"));
style.Add(new XElement(dns + "FontSize", "6pt"));
style.Add(new XElement(dns + "Color", "SteelBlue"));

textrun.Add(style);
textruns.Add(textrun);
paragraph.Add(textruns);
paragraphs.Add(paragraph);

entry.Add(paragraphs);

entry.Add(new XElement(seconddns + "DefaultName", "tb_RDCE_DataSource"));
entry.Add(new XElement(dns + "Top", "0.03in"));
entry.Add(new XElement(dns + "Left", "4.84249in"));
entry.Add(new XElement(dns + "Height", "0.26056in"));
entry.Add(new XElement(dns + "Width", "1.52209in"));
entry.Add(new XElement(dns + "ZIndex", "4"));

var style3 = new XElement(dns + "Style");
var border = new XElement(dns + "Border");
border.Add(new XElement(dns + "Style", "None"));
style3.Add(border);

style3.Add(new XElement(dns + "PaddingLeft", "2pt"));
style3.Add(new XElement(dns + "PaddingRight", "2pt"));
style3.Add(new XElement(dns + "PaddingTop", "2pt"));
style3.Add(new XElement(dns + "PaddingBottom", "2pt"));

entry.Add(style3);

//Add data source text box to the report
xreport.Element(dns + "Body").Element(dns + "ReportItems").Add(entry);
代码示例 18. [C#] 向报表添加数据源文本框

文本框将如下所示

Testing a report from the RSExplorer++ tool pointing to DEVELOPMENT

图 37. 从 RSExplorer++ 工具测试指向 DEVELOPMENT 的报表

最后,我们将 XML 转换为字节数组,并准备 `RdceCustomizableElementId` 列表,以指示我们自定义的部分。

  • 正文
  • DataSets
//Add data source text box to the report
xreport.Element(dns + "Body").Element(dns + "ReportItems").Add(entry);                

//Convert our XML to a byte array to send as output
System.Text.Encoding encoding = new System.Text.UTF8Encoding();
reportDefinitionProcessed = encoding.GetBytes(xreport.ToString());

//Set up the list of elements we customized to let the Report Server know
List<RdceCustomizableElementId> ids = new List<RdceCustomizableElementId>();
ids.Add(RdceCustomizableElementId.DataSets);
ids.Add(RdceCustomizableElementId.Body);
customizedElementIds = ids;

//RDL is customized
return true;
代码示例 19. [C#] 自定义的最后阶段

6.3. 使报表服务器能够识别和实现 RDCE

需要在报表服务器级别执行一些步骤,使其能够识别并使用该扩展。

  1. 启用扩展。在您的报表服务器中找到 rsreportserver.config 文件。您可能会在“C:\Program Files\Microsoft SQL Server\MSRS10_50.MSSQLSERVER\Reporting Services\ReportServer”找到它。在“Services”元素的末尾添加 IsRdceEnabled 元素,其值为“True”。
  2. <Service>
        ...
        <IsRdceEnabled>True</IsRdceEnabled>
    </Service>
    代码示例 20. [XML] 在 rsreportserver.config 文件中启用 RDCE 扩展
  3. 注册扩展。Extensions 元素包含传递、呈现、安全和身份验证扩展等。我们需要在本节末尾注册 RDCE,如下所示
  4. <Extensions>
        <Delivery>
        ...
        </Delivery>
        <DeliveryUI>
        ...
        </DeliveryUI>
        <Render>
        ...
        </Render>
        <Data>
        ...
        </Data>
        <SemanticQuery>
        ...
        </SemanticQuery>
        <ModelGeneration>
        ...
        </ModelGeneration>
        <Security>
        ...
        </Security>
        <Authentication>
        ...
        </Authentication>
        <EventProcessing>
        ...
        </EventProcessing>
        <ReportDefinitionCustomization>
            <Extension Name="RDCE"
                Type="RS.Extensibility.ReportDefinitionCustomizationExtension,rs.rdce">
                <Configuration>
                    <RDCEConfiguration>
                    </RDCEConfiguration>                
                </Configuration>
            </Extension>
        </ReportDefinitionCustomization>    
    </Extensions>
    代码示例 21. [XML] 在 rsreportserver.config 文件中注册 RDCE 扩展
  5. 配置 CAS(代码访问安全)。在您的报表服务器中找到 rssrvpolicy.config 文件。您可能会在“C:\Program Files\Microsoft SQL Server\MSRS10_50.MSSQLSERVER\Reporting Services\ReportServer”找到它。插入以下代码组,为我们创建的程序集添加完全信任。
  6.         ...
            <CodeGroup>
            ...
            </CodeGroup>
            <CodeGroup>
            ...
            </CodeGroup>
            <CodeGroup class="UnionCodeGroup" version="1" Name="RDCE"
            Description="Code group for the Report Definition Customization Extension"
            PermissionSetName="FullTrust">
                <IMembershipCondition class="UrlMembershipCondition" version="1"
                Url="C:\Program Files\Microsoft SQL Server\MSRS10_50.MSSQLSERVER\Reporting Services\ReportServer\bin\rs.rdce.dll"/>
            </CodeGroup> 
        </CodeGroup>
    </CodeGroup>
    代码示例 22. [XML] 在 rssrvpolicy.config 文件中为 RDCE 扩展配置代码访问安全
  7. 将 DLL 放置在正确的位置。RDCE 项目在构建时会自动将 DLL 放置在“C:\Program Files\Microsoft SQL Server\MSRS10_50.MSSQLSERVER\Reporting Services\ReportServer\bin\rs.rdce.dll”。检查路径是否正确以及 DLL 是否存在。

6.4. 调试 RDCE

您可以通过检查报表中是否包含“数据源:PRODUCTION|DEVELOPMENT|...”文本框,轻松地检查 RDCE 是否正在运行并修改了报表。如果您无法看到此内容,请检查以下几点:

  1. 报表服务器必须拥有所有共享数据源。
  2. 报表项目必须包含所有共享数据源。
  3. 每个报表都需要有指向报表项目数据源的数据源。
  4. 报表中的所有数据集都需要指向一个默认数据源(PRODUCTION)。
  5. 报表需要启用 RDCE(使用 RSExplorer++ 工具)。
  6. 报表需要有 RDCE_Report_Data_Source 参数(使用 RSExplorer++ 工具)。
  7. 报表的 RDCE_Report_Data_Source 的“Used In Query”属性需要设置为 True(使用 RSExplorer++ 工具)。
  8. 报表服务器需要将 DLL 放置在正确的位置:“C:\Program Files\Microsoft SQL Server\MSRS10_50.MSSQLSERVER\Reporting Services\ReportServer\bin\rs.rdce.dll”。
  9. rsreportserver.config 和 rssrvpolicy.config 文件需要相应修改。
  10. 如果这一切都正确,您可以尝试通过将进程附加到 ReportingServicesService.exe 来调试 RDCE [调试 - 附加到进程...]。

Attaching the Debugger to the ReportingServicesService.exe process to Debug it

图 38. 将调试器附加到 ReportingServicesService.exe 进程以进行调试

7. 报表查看器 Web 应用程序

这个应用程序只是为了说明如何将解决方案集成到您当前的环境中。既然我们知道了一切如何运作,我们不希望向最终用户暴露 RDCE_Report_Data_Source 参数。这个应用程序主要做的是:

  1. 将报表路径和要指向的数据源作为查询字符串参数获取。
  2. 将任何其他参数值作为查询字符串参数获取。
  3. 将参数提供给报表。
  4. 向最终用户隐藏 `RDCE_Report_Data_Source`。
  5. 在加载报表前,检查报表是否启用了 RDCE,并在需要时添加 RDCE_Report_Data_Source
  6. 在加载报表前,如果需要,将 RDCE_Report_Data_Source 设置为“用于查询”。
  7. 处理“特殊考虑 #3:带有钻取式数据源相关参数的报表”(在该部分解释)。

要了解有关 ReportViewer 控件如何工作的更多信息,您可以查看这篇文章:添加和配置 ReportViewer 控件 或这篇文章:为远程处理配置 ReportViewer

7.1. 输入

假设应用程序运行在“https:///ReportViewerWebApplication”,应使用以下语法进行访问:

https:///ReportViewerWebApplication/Default.aspx
?report_url=<Encoded_Report_URL>
&report_data_source=<DEVELOPMENT|PRODUCTION|QUALITY_ASSURANCE|TEST>
代码示例 23. [URL] 从 URL 调用报表查看器 Web 应用程序

一个访问指向 DEVELOPMENT 的 Names Report 的示例是:

https:///ReportViewerWebApplication/Default.aspx
?report_url=http%3A%2F%2Flocalhost%2FReportServer%2FPages%2FReportViewer.aspx
%3F%252fReports%252fNamesReport%26rs%3ACommand%3DRender
&report_data_source=DEVELOPMENT
代码示例 24. [URL] 从 URL 调用指向 DEVELOPMENT 的报表查看器 Web 应用程序的示例

请注意,report_url 必须进行编码。它从:https:///ReportServer/Pages/ReportViewer.aspx?%2fReports%2fNamesReport&rs:Command=Render 变成了 http%3A%2F%2Flocalhost%2FReportServer%2FPages%2FReportViewer.aspx%3F%252fReports%252fNamesReport%26rs%3ACommand%3DRender

7.2. 输出

报表应该在 `RDCE_Report_Data_Source` 隐藏且值已填入的情况下显示。

Names Report from the Report Viewer Web Application

图 39. 来自报表查看器 Web 应用程序的“名称报表”

本节将不涵盖此项目的后台代码

  • 检查报表是否启用了 RDCE 已在 RSExplorer++ 工具部分介绍。
  • 在 RSExplorer++ 工具中已经介绍了如何根据需要添加 RDCE_Report_Data_Source
  • 在 RSExplorer++ 工具部分已介绍了将 `RDCE_Report_Data_Source` 设置为“用于查询”。
  • 处理“特殊考虑 #3:带有钻取数据源相关参数的报表”将在该部分介绍。

8. 特殊考虑 #1:无参数或与数据源无关的参数的报表

符合以下条件的报表(在添加 RDCE_Report_Data_Source 参数之前)属于此类

  • 没有参数,或者
  • 有一个或多个不依赖于数据集的参数。

NamesReport 报表是这类报表的一个例子,因为它没有参数

Names Report having no parameters

图 40. Names Report 没有任何参数

一个具有不依赖于数据集的参数的报表示例是,该报表具有“开始日期”或开放文本参数。

8.1. 所需操作

无需进一步操作。所有这些情况都已通过所解释的方法得到妥善处理。

9. 特殊考虑 #2:带有一个数据源依赖参数的报表

符合以下条件的报表(在添加 RDCE_Report_Data_Source 参数之前)属于此类

  • 至少有一个参数,并且
  • 这些参数中只有一个依赖于数据集。

NamesReportByDepartment 报表是这类报表的一个例子,因为它有一个参数(Department),并且它的可用值来自一个数据集。

Names Report by Department having one data source dependent parameter

图 41. Names Report by Department,带有一个依赖于数据源的参数

9.1. 为何这很重要?

当调用用于填充下拉列表的数据集时,RDCE 并不会启动,因为依赖于数据集的参数会首先被填充。

这意味着即使我们将 RDCE_Report_Data_Source 参数设置为“DEVELOPMENT”,例如,我们得到的数据集依赖参数仍然会用“PRODUCTION”数据填充,因为这是它们在报表定义中默认指向的数据源。

让我们从 RSExplorer++ 工具中查看 NamesReportByDepartment,在启用 RDCE 并添加 `RDCE_Report_Data_Source` 参数后。我们向 `RDCE_Report_Data_Source` 参数添加文本“DEVELOPMENT”,然后显示的部门列表显示的是“PRODUCTION”数据,这不应该是这样。

Names Report by Department values of the Department parameter

图 42. Names Report by Department 中 Department 参数的值

如果我们然后点击“查看报表”,我们会得到一个空的结果集。我们知道 RDCE 自定义已经发生,因为显示了“数据源:DEVELOPMENT”文本,并且由于数据源不匹配,导致的结果集为空。

Names Report by Department pointing to a different data source at the parameters level

图 43. Names Report by Department 在参数级别指向不同的数据源

9.2. 如何解决?

这必须在报表查看器 Web 应用程序级别(或者在您的情况下,在负责显示报表的任何应用程序中)处理。

这个技巧在于物理上更改报表服务器上的报表定义,将那些用于参数的数据集指向我们需要的数据源。这意味着每次从不同的数据源调用报表时,报表定义都会物理上发生改变。

//Find the report parameters that have data set references
var report_parameters_data_set_references = xreport.Elements(dns + "ReportParameters").Elements(
    dns + "ReportParameter").Elements(dns + "ValidValues").Elements(
    dns + "DataSetReference").Elements(dns + "DataSetName");
//Get data sets
var data_sets = xreport.Elements(dns + "DataSets").Elements(dns + "DataSet");
//Data source that the report parameters need to point to: PRODUCTION|DEVELOPMENT...
string parameter_value = report_data_source.ToUpper();

//Go through all the data sets identified and point them to the right data source
foreach (XElement xe in report_parameters_data_set_references)
{
    foreach (XElement xerp in data_sets)
    {
        if (xerp.Attribute("Name").Value == xe.Value)
        {
            xerp.Element(dns + "Query").Element(dns + "DataSourceName").Value = parameter_value.ToUpper();
        }
    }
}

System.Text.Encoding encoding = new System.Text.UTF8Encoding();
reportDefinitionProcessed = encoding.GetBytes(xreport.ToString());

//Save the edited report definition to the server
rs.SetItemDefinition(path, reportDefinitionProcessed, null);

//Reset the RDCE_Report_Data_Source as UsedInQuery
ModifiyUsedInQueryProperty(path);
代码示例 25. [C#] 在报表服务器上物理更改报表定义,以更改参数中使用的数据集

通过之前的代码,现在当我们从报表查看器 Web 应用程序运行报表时,它会适当地更改获取部门名称的数据集的数据源。我们可以确定这一点,因为这些值现在以“Other Data Source”结尾。

Names Report by Department from the RSExplorer++ tool (parameter)

图 44. 从 RSExplorer++ 工具查看的 Names Report by Department (参数)

我们可以确定,现在整个报表都指向 DEVELOPMENT,当我们点击“查看报表”时,因为我们看到 First Name 以“Other Data Source”结尾。

Names Report by Department from the RSExplorer++ tool (report)

图 45. 来自 RSExplorer++ 工具的部门名称报表(报表)

如果我们将请求更改为指向 PRODUCTION,我们会看到参数已恢复为 PRODUCTION 的参数

Names Report by Department from the RSExplorer++ tool (parameter) pointing to PRODUCTION

图 46. 从 RSExplorer++ 工具查看的 Names Report by Department (参数),指向 PRODUCTION

最终结果是

Names Report by Department from the RSExplorer++ tool (report) pointing to PRODUCTION

图 47. 来自 RSExplorer++ 工具的按部门名称报表(报表),指向 PRODUCTION

这就是特殊考虑 #2 的处理方式。

10. 特殊考虑 #3:带有钻取式数据源依赖参数的报表

符合以下条件的报表(在添加 RDCE_Report_Data_Source 参数之前)属于此类

  • 至少有两个参数,并且
  • 两个参数都依赖于一个数据集,并且
  • 其中一个参数的选择会触发另一个参数的数据获取(钻取参数)。

NamesReportByDepartmentAndTitle 报表是这类报表的一个例子,因为它有两个参数(DepartmentTitle),两个参数都依赖于一个数据集(Department 依赖于 DepartmentsDataSetTitle 依赖于 TitlesDataSet),并且选择 Department 会触发获取该部门中存在的 Title。

Names Report by Department and Title Report Data

图 48. 按部门和职位划分的姓名报表数据

我们知道有一个钻取参数,因为 TitlesDataSet 包含“@Department”参数

Names Report by Department and Title Report Data with dependency to the Department parameter

图 49. 按部门和职位划分的姓名报表数据,依赖于部门参数

也因为最终结果集依赖于两个参数

Names Report by Department and Title Report Data with dependency to the Department and Title parameter

图 50. 依赖于部门和职位参数的部门和职位姓名报表数据

10.1. 为何这很重要?

我们已经从“特殊考虑 #2”中学到,当用于填充下拉列表的数据集被调用时,RDCE 并不会启动,因为依赖于数据集的参数会首先被填充。我们也学会了如何解决这个问题。不幸的是,当我们遇到这种情况时,这会引发一个新问题。

想象一下,用户 USER1 使用“报表查看器 Web 应用程序”请求 NamesReportByDepartmentAndTitle。USER1 希望报表指向 PRODUCTION。当他请求报表时,“报表查看器 Web 应用程序”会根据特殊考虑 #2 或 #3 的情况,在需要时将数据集更改为指向 PRODUCTION。USER1 将会看到参数工具栏正确加载,第一个参数数据集指向 PRODUCTION。USER1 在从列表中选择一个部门之前等待了一会儿。

Names Report by Department and Title actions by USER1

图 51. 用户 USER1 对“按部门和职位名称报表”的操作

如您所见,部门列表正确地指向了 PRODUCTION。

另一方面,我们有 USER2,他正在使用“报表查看器 Web 应用程序”请求同一个 NamesReportByDepartmentAndTitle。USER2 希望报表指向 DEVELOPMENT。按照“报表查看器 Web 应用程序”的工作方式,USER2 将会看到参数工具栏正确加载,第一个参数数据集指向 DEVELOPMENT。USER2 从列表中选择部门(这发生在 USER1 还没有做任何操作的时候)。

Names Report by Department and Title actions by USER2

图 52. 用户 USER2 对“按部门和职位名称报表”的操作

如您所见,部门列表正确地指向了 DEVELOPMENT。

USER2 现在选择 "Engineering Other Data Source" 值,Titles 下拉列表填充如下:

Names Report by Department and Title actions by USER2

图 53. 用户 USER2 对“按部门和职位名称报表”的操作

我们知道这一切都是正确的,因为两个下拉列表都指向 DEVELOPMENT。

USER2 现在点击“查看报表”,我们可以看到一切正常,报表在 RDCE 自定义后显示为指向 DEVELOPMENT。

Names Report by Department and Title actions by USER2

图 54. 用户 USER2 对“按部门和职位名称报表”的操作

让我们回到 USER1。他终于醒来,现在决定选择“Engineering”部门。令人惊讶的是,他发现 Title 下拉列表是空的。

Names Report by Department and Title actions by USER1

图 55. 用户 USER1 对“按部门和职位名称报表”的操作

当 USER1 尝试点击“查看报表”时,他/她会收到一条错误消息

Names Report by Department and Title actions by USER1

图 56. 用户 USER1 对“按部门和职位名称报表”的操作

解决这个问题的唯一方法是按 F5 键,让“报表查看器 Web 应用程序”发挥其魔力并修复问题。不幸的是,在这个阶段,我们无法告诉用户按 F5 键并刷新浏览器。

如果 USER1 刷新,他/她现在就能看到正确的报表了。

Names Report by Department and Title actions by USER1

图 57. 用户 USER1 对“按部门和职位名称报表”的操作

重要的是要知道,如果这种情况发生在参数选择之间,没有办法警告用户。但是,如果这种冲突发生在参数值被选择之后,RDCE 可以介入并让用户知道他/她需要通过按 F5 刷新浏览器。

Names Report by Department and Title modified by the RDCE to indicate there was a conflict

图 58. 由 RDCE 修改的“按部门和职位名称报表”,以指示存在冲突

10.2. 如何解决?

不幸的是,唯一的解决办法是让最终用户通过按浏览器上的 F5 键重新加载报表。我们无法在“报表查看器 Web 应用程序”中为他们做到这一点,但我们可以通过在 RDCE 中检查这一点,并将报表主体更改为仅显示“要查看此报表,请重新加载页面。如果您在 PC 上,可以按 F5。”消息来告知用户这样做。

这个技巧是通过 RDCE 中的以下代码实现的

//Get the report parameters that get values from data sets
var report_parameters_data_set_references = xreport.Elements(dns + "ReportParameters").Elements(
    dns + "ReportParameter").Elements(dns + "ValidValues").Elements(
    dns + "DataSetReference").Elements(dns + "DataSetName");
//Get the data sets
var data_sets = xreport.Elements(dns + "DataSets").Elements(dns + "DataSet");

//Check if the parameters that get values from data sets are more than 1
//If it's 0 or 1, we don't have an issue
if (report_parameters_data_set_references.Count() > 1)
{
    //Loop through the parameters and see if they are pointing to the right one
    foreach (XElement xe in report_parameters_data_set_references)
    {
        foreach (XElement xerp in data_sets)
        {
            if (xerp.Attribute("Name").Value == xe.Value)
            {
                if (xerp.Element(dns + "Query").Element(
                         dns + "DataSourceName").Value != ParamValue.ToUpper())
                {
                    rightDataSetDataSource = false;
                    break;
                }
            }
        }
    }
}                   

//If inconsistent data sources where found
if (!rightDataSetDataSource)
{ 
    //Add "inconsistent data sources" text to the report
    var entry2 = new XElement(dns + "Textbox");
    entry2.SetAttributeValue("Name", "tb_Error");
    entry2.Add(new XElement(dns + "CanGrow", "true"));
    entry2.Add(new XElement(dns + "KeepTogether", "true"));

    var paragraphs2 = new XElement(dns + "Paragraphs");

    var paragraph2 = new XElement(dns + "Paragraph");

    var textruns2 = new XElement(dns + "TextRuns");
    var textrun2 = new XElement(dns + "TextRun");
    textrun2.Add(new XElement(dns + "Value", "To see this report please " + 
       "reload the page. If you are on a PC you may press F5."));

    var style2 = new XElement(dns + "Style");
    style2.Add(new XElement(dns + "FontFamily", "Tahoma"));
    style2.Add(new XElement(dns + "Color", "SteelBlue"));

    textrun2.Add(style2);
    textruns2.Add(textrun2);
    paragraph2.Add(textruns2);
    paragraphs2.Add(paragraph2);

    entry2.Add(paragraphs2);

    entry2.Add(new XElement(seconddns + "DefaultName", "tb_Error"));
    entry2.Add(new XElement(dns + "Height", "0.25in"));
    entry2.Add(new XElement(dns + "Width", "8.65625in"));
    entry2.Add(new XElement(dns + "ZIndex", "4"));

    var style22 = new XElement(dns + "Style");
    var border2 = new XElement(dns + "Border");
    border2.Add(new XElement(dns + "Style", "None"));
    style22.Add(border2);

    style22.Add(new XElement(dns + "PaddingLeft", "2pt"));
    style22.Add(new XElement(dns + "PaddingRight", "2pt"));
    style22.Add(new XElement(dns + "PaddingTop", "2pt"));
    style22.Add(new XElement(dns + "PaddingBottom", "2pt"));

    entry2.Add(style22);

    //This replaces the whole body of the report with our text box
    xreport.Element(dns + "Body").Element(dns + "ReportItems").ReplaceAll(entry2);

    System.Text.Encoding encoding2 = new System.Text.UTF8Encoding();
    reportDefinitionProcessed = encoding2.GetBytes(xreport.ToString());

    List<RdceCustomizableElementId> ids2 = new List<RdceCustomizableElementId>();
    ids2.Add(RdceCustomizableElementId.DataSets);
    ids2.Add(RdceCustomizableElementId.Body);
    customizedElementIds = ids2;
    // RDL is customized but to show the error message
    return true; 
}
代码示例 26. [C#] 当需要处理特殊考虑 #3 时更改报表正文

这就是特殊考虑 #3 的处理方式。

11. 结论

  • 使用报表服务器的多数据源方法在默认情况下是不可行的。
  • 通过使用报表定义自定义扩展(RDCE),可以在运行时指向数据源。
  • RDCE 是系统中实现大部分魔术的部分,即使在该级别无法直接修改数据源,但是数据集和报表主体可以。
  • 这个过程可能看起来很长,但为了能够通过共享数据源实现这个功能,这是值得的。
  • 这种方法的缺点可能是在“特殊考虑 #3”中看到的问题,但至少它可以被识别出来。

12. 参考文献

  • Lachev, T. (2008).《Applied Microsoft SQL Server 2008 Reporting Services》。Prologika Press。

特别感谢

特别感谢 Wilson Quilindo 先生在整个解决方案开发过程中提供的宝贵意见、支持和指导。

© . All rights reserved.