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





5.00/5 (12投票s)
本文详细介绍如何通过设置 RDCE 并利用一些技巧,动态地指向给定的共享数据源引用。
- 下载示例报表项目 - 30.9 KB
- 下载 RSExplorer++ 安装程序 - 273 KB
- 下载 RSExplorer++ 源代码 - 97.1 KB
- 下载 RDCE 源代码 - 29.9 KB
- 下载报表查看器 Web 应用程序源代码 - 237 KB
目录
- 引言
- 背景
- 准备共享数据源(报表服务器端)
- 准备报表(示例报表项目)
- RSExplorer++ 工具
- RDCE
- 报表查看器 Web 应用程序
- 特殊考虑 #1:无参数或与数据源无关的参数的报表
- 特殊考虑 #2:带有一个数据源依赖参数的报表
- 特殊考虑 #3:带有钻取式数据源依赖参数的报表
- 结论
- 参考文献
1. 介绍
您是否选择了 Microsoft SQL Reporting Services 作为您的报表策略?您是否在 RDL 文件中使用共享数据源而不是嵌入式数据源?您是否有多个环境(生产、质量保证、开发等)?您是否希望能够只部署一次 RDL 文件,然后让报表服务器自动判断要指向哪个环境?
如果您对所有问题的回答都是“是”,那么本文适合您。
本文可能看起来很长,但我已尽力使其易于理解。一旦您通读一遍,就会发现当您熟悉这个过程后,它会变得简单明了。
1.1. 问题所在
目前,要在运行时更改报表数据源,报表需要使用客户端处理模型(RDLC 文件)。如果我们想使用服务器端处理模型(部署到报表服务器的 RDL 文件)来实现这一点,至今还没有解决方案。
传统方法如下图所示
1.2. 提出的解决方案概述
微软对这个问题的解决方案是引入“基于表达式的连接字符串”(参考这篇文章),但不幸的是,这不适用于共享数据源。
幸运的是,通过使用一个工具、一些报表设计标准和一个报表定义自定义扩展(RDCE),可以更改 RDL 文件中的数据源引用,使其指向正确的服务器-数据库对。
解决方案的概述如下图所示
2. 背景知识
要理解本文,您需要:
- 对 SQL Server Reporting Services 有中级了解。
- 了解“嵌入式”和“共享”数据源之间的区别(您可以查阅此文章获取更多信息:嵌入式和共享数据连接或数据源(报表生成器 3.0 和 SSRS))。
- 对 Reporting Services 扩展的概念有基本了解(您可以查阅此文章获取更多信息:Reporting Services 扩展)。
注意:本文中使用的数据库基于 AdventureWorks 数据库(您可以从这里下载)。
我将只使用两个数据库(但模拟我们有四个)
- “AdventureWorksDW” 将作为 DEVELOPMENT 和 TEST,
- “AdventureWorksDW2008” 将作为 PRODUCTION 和 QUALITY_ASSURANCE。
- 我修改了 DEVELOPMENT 数据库中的数据,以便在指向 PRODUCTION 或 DEVELOPMENT 时能看到差异。
PRODUCTION 中的数据示例如下
DEVELOPMENT 中的数据示例如下(请注意,我在某些列中添加了“Other Data Source”或“ODS”,以便在指向它时能明显区分)
3. 准备共享数据源(报表服务器端)
要使用 Web 浏览器连接到您的报表服务器,请使用 http://<your_report_server>/Reports。在本文中,我们将在主页级别设置两个文件夹
- Data Sources,用于存放共享数据源定义。
- Reports,用于存放已部署的报表(RDL 文件)。
正如这篇文章所描述的:“共享数据源为数据源指定连接属性。如果您有一个被大量报表、模型或数据驱动订阅使用的数据源,请考虑创建一个共享数据源,以消除在多个地方维护相同连接信息的开销。”
要创建和管理共享数据源,请参阅文章:创建、修改和删除共享数据源 (SSRS)。
请确保您的报表服务器包含所有您需要的共享数据源。在本文中,我将使用
- Data Sources/DEVELOPMENT
- Data Sources/QUALITY_ASSURANCE
- Data Sources/TEST
- Data Sources/PRODUCTION
请确保所有数据源都根据您的环境正确配置。下图显示了本文将使用的 DEVELOPMENT 数据库的配置
4. 准备报表(示例报表项目)
您可以使用“Business Intelligence Development Studio”或“报表生成器”来设计和部署报表。有关“Business Intelligence Development Studio”的概述,您可以阅读这篇文章:Business Intelligence Development Studio 中的 Reporting Services (SSRS)。有关“报表生成器”的概述,您可以阅读这篇文章:报表生成器 3.0 入门。在本节中,我将使用“Business Intelligence Development Studio”。
以下内容适用于示例报表项目
- 请检查项目属性是否根据您的环境正确配置。
- TargetDataSourceFolder = Data Sources (与图 5 保持一致)
- TargetReportFolder = Reports (与图 5 保持一致)
- TragetServerURL = http://<your_report_server>/reportserver
- 添加您报表服务器上已有的共享数据源(我将使用图 6 中的四个)
- DEVELOPMENT
- QUALITY_ASSURANCE
- TEST
- PRODUCTION
- 确保每个数据源都配置正确。在下图中,我展示了应与图 7 中的设置相匹配的配置。
- 该项目包含三个报表。在本节中,我将重点介绍“NamesReport.rdl”,尽管它们都遵循相同的约定。
- “NamesReport.rdl”的设计视图如下所示
- 为了让报表做好准备,它们需要引用可用的共享数据源。为此,请打开“报表数据”窗口(在 Visual Studio [Business Intelligence Development Studio] 中按“Ctrl + Alt + D”或转到“视图 - 报表数据”)。您应该会看到以下窗口
- 在“数据源”文件夹中,您应该有与项目级别完全相同的数据源
- DEVELOPMENT
- QUALITY_ASSURANCE
- TEST
- PRODUCTION
- 每个数据源的属性都应指向报表项目级别的共享数据源
- 现在让我们看一下报表级别的数据集
- 每个数据集的属性都必须配置为指向 PRODUCTION 数据源,如下所示
- 如果我们预览该报表,它应该看起来像下面这样,指向 PRODUCTION 数据
- 部署报表(在报表上右键单击 - 部署)。
- 现在,相同的报表应该在服务器上可见(它指向 PRODUCTION 数据)
总结一下,需要的重要步骤是
- 报表项目需要包含报表服务器上所有的共享数据源。
- 每个报表都需要包含所有指向报表服务器上共享数据源的引用。
- 每个报表中的所有数据集都需要指向 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>
5.2. 使用 RSExplorer++ 工具
- 在“Server Address”文本框中输入您的报表服务器地址,格式如下:http://<your_report_server>/reportserver,然后点击“Go”。您应该能看到根目录下的文件夹。
- 点击 Reports 文件夹,您应该会看到在本文第 4 节中部署的三个报表。这三个报表最初的“RDCE Enabled”将设置为 False,“DS Parameter”设置为 N/A,“Used In Query”也设置为 N/A。注意:为了能够在运行时更改数据源,这三个值都需要设置为 true。接下来的步骤将说明如何实现这一点。
- 如果您双击一个报表,例如第一个,您将能够预览它。
- 如果您选择一个报表并点击屏幕右上角的“Show Properties”,右侧的框中将填充报表属性信息。
- 如果您选择一个报表并点击屏幕右上角的“Enable RDCE”,该报表的记录颜色应变为橙色,“RDCE Enabled”的值应变为 True,“DS Parameter”的值应变为 False。这现在意味着这个特定的报表在呈现之前会调用 RDCE。
- 如果您再次选择该报表并点击屏幕右上角的“Show Properties”,您会看到一个名为 RDCE 的新属性,其值为“RDCE”。有关实现这一点的后台代码的详细信息,请参见“RSExplorer++ 工具的工作原理是什么?”部分。
- 如果您双击该报表,您不会注意到任何变化,但实际上,RDCE 现在正在被调用。注意:有关此信息,请参见“RDCE”部分。
- 如果您选择该报表并点击屏幕右上角的“Add DS Parameter/Use In Query”,将执行两个应属于一个事务的操作:
- 一个报表参数将被添加到 RDL(报表定义)文件中。此参数将用于确定报表需要指向哪个数据源。
- 应通知报表服务器这个新参数是“Used In Query”,以便 RDCE 能够看到该值。如果您在数据集中使用该参数,则该值在报表设计器工具的报表级别被设置为 True。为了避免这样做,我使用了一个技巧来告诉服务器该参数正在“Used In Query”,即使理论上并非如此。有关实现此功能的后台代码的详细信息,请参阅“RSExplorer++ 工具的工作原理是什么?”部分。
- 您现在可以双击报表,通过在参数中输入数据源名称并按“查看报表”来测试它是否指向正确的数据源。注意:这些测试仅在您已配置报表服务器以启用 RDCE 的情况下才能正常工作。更多参考信息请见第 6 节“RDCE”。
- DEVELOPMENT
- QUALITY_ASSURANCE
- TEST
- PRODUCTION
如果报表参数已添加但未执行“Used In Query”操作,这意味着在“配置 RSExplorer++ 工具”部分指定的连接字符串是错误的,您将看到一个红色高亮的记录。修复连接字符串,然后再次点击“Add DS Parameter/Use In Query”。
如果事务成功,报表记录应变为绿色。这表示该报表已完全准备好动态指向给定的数据源。
请注意,在报表的右上角添加了文本以指示报表指向哪个数据源。由于本文第 2 节对“DEVELOPMENT”数据库所做的更改,我们可以看到数据与 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
}
检查报表是否具有数据源参数以及该参数是否被“用于查询”:(此实现用于在 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;
}
}
}
在报表上启用 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);
在报表上禁用 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);
添加 “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);
将参数的“Used In Query”设置为 true:这里比较棘手,因为报表服务器知道一个参数是否被用于查询,并不是在 RDL 层面。如果您在添加 RDCE_Report_Data_Source
参数之后,但在将“Used In Query”设置为 true 之前,对您的报表服务器运行以下 SQL 语句,您将得到如下结果。
SELECT [Parameter]
FROM [ReportServer].[dbo].[Catalog]
WHERE [Path] = '/Reports/NamesReport'
<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>
目标是直接在数据库中将 "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();
这总结了 RSExplorer++ 工具在“幕后”所做的工作。下一节将详细解释我们为什么需要执行所有这些步骤。
6. RDCE
报表定义自定义扩展 (RDCE) 是 SQL Server 2008 中引入的一项报表服务器可扩展性功能。它允许我们在运行时自定义报表定义 RDL,而无需将新的 RDL 写回报表服务器。它基本上是从服务器获取 RDL,对其进行自定义,然后将自定义后的版本发送到报表查看器。
6.1. 理解 RDL 文件
报表定义语言 (RDL) 是一种基于 XML 的文件,它代表了定义报表服务器报表的元数据。以下文章概述了 RDL 模式:报表定义概述图。我们可以在第 4 节中我们一个报表的后台代码中看到 RDL 的一些元素
- 正文
- 页面
- DataSources
- DataSets
- ReportParameters
报表定义自定义扩展允许您仅自定义报表定义的以下元素:
- 正文
- DataSets
- 页面
- PageFooter
- PageHeader
这意味着以下元素在运行时无法自定义
- DataSources
- ReportParameters
这意味着我们无法在运行时添加报表参数(这是合理的,因为这些参数需要在报表本身加载之前加载,并决定我们从报表中获取的数据)。这也意味着我们无法修改数据源。这正是关键所在!如果我们能够修改数据源,我们只需更改数据源的“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>
现在让我们看一下同一报表的 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>
由于我们无法对 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 属性”进行配置。
生成事件的生成后事件命令行最好配置为指向您的 Reporting Services bin 文件夹。这将在生成后将 DLL 文件放置在正确的位置,并允许您进行调试。
命令应如下所示(请检查您环境中 Reporting Services bin 的路径是否正确)
copy "$(TargetDir)$(TargetName).*"
"C:\Program Files\Microsoft SQL Server\MSRS10_50.MSSQLSERVER\Reporting Services\ReportServer\bin"
该项目需要引用 Microsoft.ReportingServices.Interfaces
,该引用可在 "C:\Program Files\Microsoft SQL Server\100\SDK\Assemblies" 中找到。
如您所见,该项目只有一个类 ReportDefinitionCustomizationExtension
,它实现了在 Microsoft.ReportingServices.Interfaces
中找到的 IReportDefinitionCustomizationExtension
接口。该接口公开了我们将重点关注的 ProcessReportDefinition
方法。有关此方法参数和返回值的详细信息,请参阅这篇文章:IReportDefinitionCustomizationExtension.ProcessReportDefinition 方法。
bool ProcessReportDefinition(
byte[] reportDefinition,
IReportContext reportContext,
IUserContext userContext,
out byte[] reportDefinitionProcessed,
out IEnumerable<RdceCustomizableElementId> customizedElementIds
)
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();
既然我们知道“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") + "}";
}
“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;
}
现在我们需要遍历数据集,将它们指向我们想要的数据源。
//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;
}
“特殊考虑 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);
文本框将如下所示
最后,我们将 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;
6.3. 使报表服务器能够识别和实现 RDCE
需要在报表服务器级别执行一些步骤,使其能够识别并使用该扩展。
- 启用扩展。在您的报表服务器中找到 rsreportserver.config 文件。您可能会在“C:\Program Files\Microsoft SQL Server\MSRS10_50.MSSQLSERVER\Reporting Services\ReportServer”找到它。在“
Services
”元素的末尾添加IsRdceEnabled
元素,其值为“True”。 - 注册扩展。
Extensions
元素包含传递、呈现、安全和身份验证扩展等。我们需要在本节末尾注册 RDCE,如下所示 - 配置 CAS(代码访问安全)。在您的报表服务器中找到 rssrvpolicy.config 文件。您可能会在“C:\Program Files\Microsoft SQL Server\MSRS10_50.MSSQLSERVER\Reporting Services\ReportServer”找到它。插入以下代码组,为我们创建的程序集添加完全信任。
- 将 DLL 放置在正确的位置。RDCE 项目在构建时会自动将 DLL 放置在“C:\Program Files\Microsoft SQL Server\MSRS10_50.MSSQLSERVER\Reporting Services\ReportServer\bin\rs.rdce.dll”。检查路径是否正确以及 DLL 是否存在。
<Service>
...
<IsRdceEnabled>True</IsRdceEnabled>
</Service>
<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>
...
<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>
6.4. 调试 RDCE
您可以通过检查报表中是否包含“数据源:PRODUCTION|DEVELOPMENT|...”文本框,轻松地检查 RDCE 是否正在运行并修改了报表。如果您无法看到此内容,请检查以下几点:
- 报表服务器必须拥有所有共享数据源。
- 报表项目必须包含所有共享数据源。
- 每个报表都需要有指向报表项目数据源的数据源。
- 报表中的所有数据集都需要指向一个默认数据源(PRODUCTION)。
- 报表需要启用 RDCE(使用 RSExplorer++ 工具)。
- 报表需要有 RDCE_Report_Data_Source 参数(使用 RSExplorer++ 工具)。
- 报表的 RDCE_Report_Data_Source 的“Used In Query”属性需要设置为 True(使用 RSExplorer++ 工具)。
- 报表服务器需要将 DLL 放置在正确的位置:“C:\Program Files\Microsoft SQL Server\MSRS10_50.MSSQLSERVER\Reporting Services\ReportServer\bin\rs.rdce.dll”。
- rsreportserver.config 和 rssrvpolicy.config 文件需要相应修改。
- 如果这一切都正确,您可以尝试通过将进程附加到 ReportingServicesService.exe 来调试 RDCE [调试 - 附加到进程...]。
7. 报表查看器 Web 应用程序
这个应用程序只是为了说明如何将解决方案集成到您当前的环境中。既然我们知道了一切如何运作,我们不希望向最终用户暴露 RDCE_Report_Data_Source
参数。这个应用程序主要做的是:
- 将报表路径和要指向的数据源作为查询字符串参数获取。
- 将任何其他参数值作为查询字符串参数获取。
- 将参数提供给报表。
- 向最终用户隐藏 `RDCE_Report_Data_Source`。
- 在加载报表前,检查报表是否启用了 RDCE,并在需要时添加
RDCE_Report_Data_Source
。 - 在加载报表前,如果需要,将
RDCE_Report_Data_Source
设置为“用于查询”。 - 处理“特殊考虑 #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>
一个访问指向 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
请注意,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` 隐藏且值已填入的情况下显示。
本节将不涵盖此项目的后台代码
- 检查报表是否启用了 RDCE 已在 RSExplorer++ 工具部分介绍。
- 在 RSExplorer++ 工具中已经介绍了如何根据需要添加
RDCE_Report_Data_Source
。 - 在 RSExplorer++ 工具部分已介绍了将 `RDCE_Report_Data_Source` 设置为“用于查询”。
- 处理“特殊考虑 #3:带有钻取数据源相关参数的报表”将在该部分介绍。
8. 特殊考虑 #1:无参数或与数据源无关的参数的报表
符合以下条件的报表(在添加 RDCE_Report_Data_Source
参数之前)属于此类
- 没有参数,或者
- 有一个或多个不依赖于数据集的参数。
NamesReport 报表是这类报表的一个例子,因为它没有参数
一个具有不依赖于数据集的参数的报表示例是,该报表具有“开始日期”或开放文本参数。
8.1. 所需操作
无需进一步操作。所有这些情况都已通过所解释的方法得到妥善处理。
9. 特殊考虑 #2:带有一个数据源依赖参数的报表
符合以下条件的报表(在添加 RDCE_Report_Data_Source
参数之前)属于此类
- 至少有一个参数,并且
- 这些参数中只有一个依赖于数据集。
NamesReportByDepartment 报表是这类报表的一个例子,因为它有一个参数(Department),并且它的可用值来自一个数据集。
9.1. 为何这很重要?
当调用用于填充下拉列表的数据集时,RDCE 并不会启动,因为依赖于数据集的参数会首先被填充。
这意味着即使我们将 RDCE_Report_Data_Source
参数设置为“DEVELOPMENT”,例如,我们得到的数据集依赖参数仍然会用“PRODUCTION”数据填充,因为这是它们在报表定义中默认指向的数据源。
让我们从 RSExplorer++ 工具中查看 NamesReportByDepartment,在启用 RDCE 并添加 `RDCE_Report_Data_Source` 参数后。我们向 `RDCE_Report_Data_Source` 参数添加文本“DEVELOPMENT”,然后显示的部门列表显示的是“PRODUCTION”数据,这不应该是这样。
如果我们然后点击“查看报表”,我们会得到一个空的结果集。我们知道 RDCE 自定义已经发生,因为显示了“数据源:DEVELOPMENT”文本,并且由于数据源不匹配,导致的结果集为空。
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);
通过之前的代码,现在当我们从报表查看器 Web 应用程序运行报表时,它会适当地更改获取部门名称的数据集的数据源。我们可以确定这一点,因为这些值现在以“Other Data Source”结尾。
我们可以确定,现在整个报表都指向 DEVELOPMENT,当我们点击“查看报表”时,因为我们看到 First Name 以“Other Data Source”结尾。
如果我们将请求更改为指向 PRODUCTION,我们会看到参数已恢复为 PRODUCTION 的参数
最终结果是
这就是特殊考虑 #2 的处理方式。
10. 特殊考虑 #3:带有钻取式数据源依赖参数的报表
符合以下条件的报表(在添加 RDCE_Report_Data_Source
参数之前)属于此类
- 至少有两个参数,并且
- 两个参数都依赖于一个数据集,并且
- 其中一个参数的选择会触发另一个参数的数据获取(钻取参数)。
NamesReportByDepartmentAndTitle 报表是这类报表的一个例子,因为它有两个参数(Department
和 Title
),两个参数都依赖于一个数据集(Department
依赖于 DepartmentsDataSet
,Title
依赖于 TitlesDataSet
),并且选择 Department 会触发获取该部门中存在的 Title。
我们知道有一个钻取参数,因为 TitlesDataSet
包含“@Department
”参数
也因为最终结果集依赖于两个参数
10.1. 为何这很重要?
我们已经从“特殊考虑 #2”中学到,当用于填充下拉列表的数据集被调用时,RDCE 并不会启动,因为依赖于数据集的参数会首先被填充。我们也学会了如何解决这个问题。不幸的是,当我们遇到这种情况时,这会引发一个新问题。
想象一下,用户 USER1 使用“报表查看器 Web 应用程序”请求 NamesReportByDepartmentAndTitle。USER1 希望报表指向 PRODUCTION。当他请求报表时,“报表查看器 Web 应用程序”会根据特殊考虑 #2 或 #3 的情况,在需要时将数据集更改为指向 PRODUCTION。USER1 将会看到参数工具栏正确加载,第一个参数数据集指向 PRODUCTION。USER1 在从列表中选择一个部门之前等待了一会儿。
如您所见,部门列表正确地指向了 PRODUCTION。
另一方面,我们有 USER2,他正在使用“报表查看器 Web 应用程序”请求同一个 NamesReportByDepartmentAndTitle。USER2 希望报表指向 DEVELOPMENT。按照“报表查看器 Web 应用程序”的工作方式,USER2 将会看到参数工具栏正确加载,第一个参数数据集指向 DEVELOPMENT。USER2 从列表中选择部门(这发生在 USER1 还没有做任何操作的时候)。
如您所见,部门列表正确地指向了 DEVELOPMENT。
USER2 现在选择 "Engineering Other Data Source" 值,Titles 下拉列表填充如下:
我们知道这一切都是正确的,因为两个下拉列表都指向 DEVELOPMENT。
USER2 现在点击“查看报表”,我们可以看到一切正常,报表在 RDCE 自定义后显示为指向 DEVELOPMENT。
让我们回到 USER1。他终于醒来,现在决定选择“Engineering”部门。令人惊讶的是,他发现 Title 下拉列表是空的。
当 USER1 尝试点击“查看报表”时,他/她会收到一条错误消息
解决这个问题的唯一方法是按 F5 键,让“报表查看器 Web 应用程序”发挥其魔力并修复问题。不幸的是,在这个阶段,我们无法告诉用户按 F5 键并刷新浏览器。
如果 USER1 刷新,他/她现在就能看到正确的报表了。
重要的是要知道,如果这种情况发生在参数选择之间,没有办法警告用户。但是,如果这种冲突发生在参数值被选择之后,RDCE 可以介入并让用户知道他/她需要通过按 F5 刷新浏览器。
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;
}
这就是特殊考虑 #3 的处理方式。
11. 结论
- 使用报表服务器的多数据源方法在默认情况下是不可行的。
- 通过使用报表定义自定义扩展(RDCE),可以在运行时指向数据源。
- RDCE 是系统中实现大部分魔术的部分,即使在该级别无法直接修改数据源,但是数据集和报表主体可以。
- 这个过程可能看起来很长,但为了能够通过共享数据源实现这个功能,这是值得的。
- 这种方法的缺点可能是在“特殊考虑 #3”中看到的问题,但至少它可以被识别出来。
12. 参考文献
- Lachev, T. (2008).《Applied Microsoft SQL Server 2008 Reporting Services》。Prologika Press。
特别感谢
特别感谢 Wilson Quilindo 先生在整个解决方案开发过程中提供的宝贵意见、支持和指导。