在 ASP.NET 中运行 RDL/RDLC(SQL 报表)






4.90/5 (22投票s)
SSRS 和 Report Builder(或 2008 年的旧 BIDS)在创建报表方面做得很好,但它们并不能很好地嵌入到你的 ASP.NET 项目中。这是在不需要完整 SSRS(及其安全措施)的情况下实现此目的的一种方法。
- 下载 VStudio 2010 的旧源代码 - 21.2 KB 或
- 下载 VStudio 2013 的源代码 - 179.6 KB
引言
这是一个常见的场景:你有一个用 ASP.NET(或 MVC 或 SharePoint)编写的网站,你想显示一些报表。你可能打算编写一些新报表,正在决定使用哪种技术,或者你可能已经有一些 SSRS 报表,并且想在你的 ASP.NET 网站上运行它们。
CodeProject 上有很多关于如何在 ASP.NET 或 WinForms 等环境中运行 RDLC 文件的好文章。阅读它们之后,你似乎有以下选择:
- 将报表放入 SSRS,然后使用
ReportViewer
控件调用 SSRS 来运行你的报表 - 将 .RDL 或 .RDLC 文件添加到你的项目中,并创建一些对象来容纳数据,以便报表能够访问数据库
我想要第三种选择 - 就像 SSRS 一样运行它,**但无需安装 SSRS 服务器**。我指的是最小的占地面积。我只想将 .RDL 或 .RDLC 文件的名称传递给 ASPX 页面并让它运行。SSRS 就是这样做的。我也应该能够做到。
这是第三种选择的实现。
必备组件
- 要运行此程序,你仍然需要安装报表运行时。你可以通过 NuGet 包“Microsoft.ReportViewer.Runtime.Common”和“Microsoft.ReportViewer.WebForms”(或 WinForms)来添加它。可在 Microsoft 下载网站上找到可再发行版本。
- 我有一些用旧的 Business Intelligence Development Studio (2008) 制作的旧报表。我还制作了一些用
ReportBuilder
(集成在 SSRS Web 中)制作的新报表。此解决方案对两者都适用。
策略
.RDLC 文件全是 XML。如果你用记事本打开它并检查内容,你会发现 XML 描述了显示/设计,但它还包含其他一些有用的特征。其中之一是报表的(数据获取)查询。
我的策略是提取数据库查询,设置任何参数,运行查询,将结果存储在 DataTable
(s) 中,然后将其馈送到报表中。
我的目标是从一个通用页面运行报表,并通过 URL QueryString
传递报表名称和任何查询参数,如下所示:
.../View.aspx?Report=Example.rdlc&StartDate=1/1/2012&EndDate=12/31/2012
为简单起见,我将只使用应用程序其余部分使用的相同 DB 连接字符串,但我会将其包装在一个本地工厂方法中,以便于维护。
Using the Code
从终点开始思考
这就是我开始的地方。我借鉴了 CodeProject 上其他文章的经验(请参阅文章结尾处的致谢)。示例运行良好,但没有提供使用嵌入在报表中的查询的方法。在(下面的)代码块中,你可以看到我创建了一个名为 Report
(命名空间 RDL
)的类来封装 RDLC 的内容/结构。我的 RDL.Report
类还包含一个工厂方法,用于帮助将 XML 转换为对象。
//View.aspx.cs
protected void ShowReport()
{
System.IO.FileInfo reportFullPath = this.ReportFile;
//check to make sure the file ACTUALLY exists, before we start working on it
if (reportFullPath != null)
{
//map the reporting engine to the .rdl/.rdlc file
rvReportViewer.LocalReport.ReportPath = reportFullPath.FullName;
// 1. Clear Report Data
rvReportViewer.LocalReport.DataSources.Clear();
// 2. Get the data for the report
// Look-up the DB query in the "DataSets"
// element of the report file (.rdl/.rdlc which contains XML)
RDL.ReportreportDef = RDL.Report.GetReportFromFile(reportFullPath.FullName);
// Run each query (usually, there is only one) and attach it to the report
foreach (RDL.DataSet ds in reportDef.DataSets)
{
//copy the parameters from the QueryString into the ReportParameters definitions (objects)
ds.AssignParameters(this.ReportParameters);
//run the query to get real data for the report
System.Data.DataTable tbl = ds.GetDataTable(this.DBConnectionString);
//attach the data/table to the Report's dataset(s), by name
ReportDataSource rds = new ReportDataSource();
rds.Name = ds.Name; //This refers to the dataset name in the RDLC file
rds.Value = tbl;
rvReportViewer.LocalReport.DataSources.Add(rds);
}
rvReportViewer.LocalReport.Refresh();
}
}
余下的故事
(上面的)代码块显示了应用程序的核心;运行查询并将数据附加到报表,然后运行报表。现在,让我们看看获取数据的部分。
从 RDLC 文件获取查询
在 .RDLC 文件中,查询的 XML 如下所示(在删除所有其他内容之后):
<Report>
<DataSets>
<DataSet Name=”IrrelevantToThisExample”>
<Query>
<DataSourceName>DataTableName</DataSourceName>
<CommandText>SELECT * FROM sys.Tables</CommandText>
</Query>
</DataSet>
</DataSets>
</Report>
在我第一次尝试时,我使用了 XPath 从 XML(在 RDLC 文件内部)中提取查询。它对简单的查询有效。但是,我意识到如果查询有任何参数(或存储过程等),事情就会变得混乱。
在我第二次尝试时,我采用了不同的方法。我意识到如果我将 XML 反序列化为对象堆栈,代码会更容易。这听起来很复杂且吓人,但一旦你看到它,你就会意识到 XML 序列化/反序列化有多么简单。
与此 XML 匹配的(简化的)类如下所示:
[Serializable(), System.Xml.Serialization.XmlRoot("Report")]
public class Report : SerializableBase
{
public List<DataSet> DataSets = new List<DataSet>();
}
public class DataSet
{
[System.Xml.Serialization.XmlAttribute]
public string Name;
public Query Query = new Query();
}
public class Query
{
public string DataSourceName;
public string CommandText;
}
一旦你反序列化了 XML,你就可以轻松地提取查询,如下所示:
Report report =Report.Deserialize(xml, typeof(RDL.Report));
String commandText = report.DataSets[0].Query.CommandText;
SerializableBase
对象是我从几个项目中重用的。它使得将任何对象序列化或反序列化到 XML 或反之变得简单。这是代码:
[Serializable]
public class SerializableBase
{
public static SerializableBase Deserialize(String xml, Type type)
{
//… some code omitted for brevity. See downloads.
System.Xml.Serialization.XmlSerializer ser =
new System.Xml.Serialization.XmlSerializer(type);
using (System.IO.StringReadersr = new System.IO.StringReader(xml))
{
return (SerializableBase)ser.Deserialize(sr);
}
}
}
设置任何参数
如前所述,代码非常简单,直到我处理了参数化查询和存储过程。我不得不为反序列化添加几个类。为简洁起见,我将在下载的代码中包含它们,但让你免于阅读此处的代码。别担心。它们只是非常简单(无聊)的类,与上述序列化类一样,与 XML 的结构匹配。
重构
其余代码最初在实用工具类中。在查看它们之后,我意识到将实用工具代码封装在序列化类作为方法而不是外部辅助函数中会更面向对象。这使得序列化类看起来更复杂。这就是为什么在本文中,我首先以最简单的形式描述原始类(上面)。
报表参数/查询参数
不幸的是,在 RDLC 文件中,查询块定义了它的参数,但没有为它们定义类型。数据库会因为不易转换的类型而崩溃,例如:DateTime
、Numeric
和 Integer
。幸运的是,参数类型在 RDLC 的 XML 的另一个部分中定义。我只需要将这些复制到查询参数定义中。不幸的是,这使得代码看起来有点 hacky,但它确实可靠地完成了工作。
//Report.cs
private void ResolveParameterTypes()
{
//for each report parameter, find the matching query parameter and copy-in the data type
foreach (ReportParameter rParam in this.ReportParameters)
{
foreach (DataSet ds in this.DataSets)
foreach (QueryParameter qParam in ds.Query.QueryParameters)
{
if (qParam.Value == "=Parameters!" + rParam.Name + ".Value")
{
qParam.DataType = rParam.DataType;
}
}
}
}
// override the constructor so the report param types are always resolved to the query params
//as a bonus, now you don't have to cast it after deserializing it
public static Report Deserialize(string xml, Type type)
{
Report re;
re = (Report)SerializableBase.Deserialize(xml, type);
//copy the type-names from the ReportParameters to the QueryParameters
re.ResolveParameterTypes();
return re;
}
URL 参数
现在,我将参数从(URL)QueryString
复制到报表的 param
s。当然,我对 QueryString
参数的名称与报表的名称匹配做了一些大的假设。如果不匹配,就会出错,但应该很容易弄清楚出了什么问题。我还可以添加一些诊断来检测哪些参数没有被赋值(也许以后)。
//View.aspx.cs
private System.Collections.Hashtable ReportParameters
{
get
{
System.Collections.Hashtable re = new System.Collections.Hashtable();
//gather any params so they can be passed to the report
foreach (string key in Request.QueryString.AllKeys)
{
if (key.ToLower() != "path")
//ignore the “path” param. It describes the report’s file path
{
re.Add(key, Request.QueryString[key]);
}
}
return re;
}
}
//DataSet.cs
public void AssignParameters(System.Collections.HashtablewebParameters)
{
foreach (RDL.QueryParameter param in this.Query.QueryParameters)
{
string paramName = param.Name.Replace("@", "");
//if this report param was passed as an arg to the report, then populate it
if (webParameters[paramName] != null)
param.Value = webParameters[paramName].ToString();
}
}
运行查询并填充 DataTable
这几乎是基础知识。设置命令对象,添加参数,然后使用 DataAdapter
填充表。
//DataSet.cs
public System.Data.DataTable GetDataTable(string DBConnectionString)
{
System.Data.DataTable re = new System.Data.DataTable();
using (System.Data.OleDb.OleDbDataAdapter da =
new System.Data.OleDb.OleDbDataAdapter(this.Query.CommandText, DBConnectionString))
{
if (this.Query.QueryParameters.Count > 0)
{
foreach (RDL.QueryParameter param in this.Query.QueryParameters)
{
string paramName = param.Name.Replace("@", "");
//OLEDB chokes on the @symbol, it prefers ? marks
using (System.Data.OleDb.OleDbCommand cmd = da.SelectCommand)
cmd.CommandText = cmd.CommandText.Replace(param.Name, "?");
using (System.Data.OleDb.OleDbParameterCollection params = da.SelectCommand.Parameters)
switch (param.DataType)
{
case "Text":
params.Add(new OleDbParameter(paramName, OleDbType.VarWChar)
{ Value = param.Value });
break;
case "Boolean":
params.Add(new OleDbParameter(paramName, OleDbType.Boolean)
{ Value = param.Value });
break;
case "DateTime":
params.Add(new OleDbParameter(paramName, OleDbType.Date)
{ Value = param.Value });
break;
case "Integer":
params.Add(new OleDbParameter(paramName, OleDbType.Integer)
{ Value = param.Value });
break;
case "Float":
params.Add(new OleDbParameter(paramName, OleDbType.Decimal)
{ Value = param.Value });
break;
default:
params.Add(new OleDbParameter(paramName, param.Value));
break;
}
}
}
da.fill(re);
re.TableName = this.Name;
return re;
}
后续(重构)
我确实重构了这段代码(在下载中),这使得它有点凌乱。我想让它更灵活,以便我可以在多个项目中使用它。由于我不能确定数据库连接字符串始终是 OLEDB 或 SqlClient 连接,我检查了连接字符串并使用了适合两者的适当库集(OLEDB/SQLClient)。代码长度增加了一倍,但更具可移植性。
更新:其他引起错误的原因
一位朋友帮助我运行了一些测试报表,发现 ReportViewer
控件在出现问题时不会生成任何良好/有用的错误消息。相比之下,我放在 View 页面上的“下载”按钮会轻松报告处理过程中的错误。从中我学到了一些东西:
- 如果报表具有外部图形或报表部分,则需要这些文件(意味着,你需要在 /reports 文件夹中也包含这些文件)并且路径正确。对于我的测试示例,报表使用了来自 /Reports/Web Parts/ 文件夹的图形。
- 如果报表有必需的参数(或不是“可选”的),则必须提供这些参数,否则报表将无法运行。
- 参数可能区分大小写。也许只是 .NET。无论如何,如果你的报表无法运行,请检查以确保你提供了“
PrintID
”或“PrintId
”,而不是仅仅“printid
”。
最后,我最近更新了这个示例,使其可以在 Visual Studio 2013 中运行。我添加了一个“诊断”页面来检查一些设置。我改进了“下载”选项。我改进了这个示例以使用外部图像,并检测必需的报表参数。
我希望你发现它对你来说效果更好。如果不是,我将不胜感激你的反馈,甚至可能是一个示例文件(.rdl),这样我就可以解决任何错误。
结论
这就是从 RDLC 文件中提取查询并在 ASP.NET 中运行它的全部内容。
SSRS 最初是由 Microsoft 编写的,作为如何使用这些技术来完成我在此处展示的内容的示例。当然,SSRS 具有许多超出我在此处展示的丰富功能,但如果你不需要所有这些丰富的功能,这段代码应该对你和你的 .NET 项目来说是快速且可移植的。
高级选项
本文介绍了一种非常简单的方法来运行简单的 RDLC 文件。但是,你可能有一些更复杂的报表。这里有一些资源,你可以继续阅读以涵盖基于此概念的更高级主题:
- 嵌入式报表(子报表)
https://codeproject.org.cn/Articles/473844/Using-Custom-Data-Source-to-create-RDLC-Reports - 生成 PDF 而不使用 Report Viewer 控件 https://codeproject.org.cn/Articles/492739/Exporting-to-Word-PDF-using-Microsoft-Report-RDLC
- 报表部分(使用与子报表相同的策略)
- 嵌入式图形(只需添加更多可序列化对象来捕获这些设置)
贷方
我借鉴了其他项目的一些代码。这些家伙做了出色的工作,他们的一些棘手部分应该归功于他们。
- 函数
ShowReport()
改编自 https://codeproject.org.cn/Articles/37845/Using-RDLC-and-DataSets-to-develop-ASP-NET-Reporti
历史
- 原始版本(当前)
- 2016 年 2 月:更新 Visual Studio 2013、RDL 2010(SSRS 2012 报表)和更好的诊断