天天看点

在没有SSRS的ASP.NET中运行RDL/RDLC(SQL报告)先决条件战略使用代码结论

目录

介绍

先决条件

战略

使用代码

以终为始

故事的其余部分

从RDLC文件中获取查询

设置任何参数

重构

报表参数/查询参数

网址参数

运行查询并填写数据表

跟进(重构)

更新:导致错误的其他事情

结论

  • 下载 VStudio 2010 的旧源 - 21.2 KB  或
  • 下载 VStudio 2013 的源代码 - 179.6 KB

这是一个常见的场景:您有一个用ASP.NET(或MVC或SharePoint)编写的网站,并且您想显示一些报告。您可能正计划编写一些新报告,并且您正在尝试决定使用哪种技术,或者您可能有几个之前已经制作的SSRS报告,并且您希望从您的ASP.NET站点运行它们。

CodeProject上有很多很好的文章,展示了如何使用RDLC文件运行报表,无论是来自ASP.NET还是WinForms等。阅读它们之后,您的选择似乎是:

  1. 将报表放入SSRS并使用ReportViewer控件调用SSRS运行您的报表
  2. 将.RDL或.RDLC文件添加到您的项目中,并创建一些对象来存放数据,以便报表具有与数据库的接口,我想要第三种选择:
  3. 只需像SSRS一样运行它,但无需安装SSRS服务器。我说的是最低限度的足迹。我只想将.RDL或.RDLC文件的名称传递给ASPX页面并让它运行。这就是SSRS的做法。我应该也能做到。

这是第三种选择的实现。

先决条件

  1. 要运行它,您仍然需要安装报告运行时。您可以将其添加为Nuget包“Microsoft.ReportViewer.Runtime.Common”和“Microsoft.ReportViewer.WebForms”(或WinForms)。Microsoft 下载站点上提供了可再发行组件。
  2. 我有一些旧报告,它们是用旧的商业智能开发工作室(2008)制作的。我还有一些使用ReportBuilder(内置SSRS网络)制作的新报告。此解决方案适用于任何一个。

战略

.RDLC文件都是XML 。如果您使用记事本打开它并检查内容,您会看到XML描述了显示/设计,但它还包含其他一些有用的特征。其中之一是报表的查询(获取数据)。

我的策略是提取数据库查询,设置任何参数,运行查询,将结果存储在DataTable(s)中并将其提供给报告。

我的目标是从通用页面运行我的报告,并通过URL QueryString传递报告名称和任何查询参数,如下所示:

.../View.aspx?Report=Example.rdlc&StartDate=1/1/2012&EndDate=12/31/2012
           

为简单起见,我将只使用应用程序其余部分使用的相同DB连接字符串,但我会将其包装在本地工厂方法中,以实现可维护性。

使用代码

以终为始

这就是我开始的地方。这些示例效果很好,但无法使用嵌入在报告中的查询。在代码块(如下)中,您可以看到我创建了一个名为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文件中,查询块定义了它的参数,但没有为它们定义类型。DB会阻塞不容易转换的类型,例如:DateTime、Numeric和Integer。幸运的是,参数类型在RDLC的XML的单独部分中定义。我只需要将它们复制到查询参数定义中。不幸的是,它使代码看起来有点老套,但它确实可靠地完成了工作。

//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)QueryString中的参数复制到报告的param中。自然,我对与报告中的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();
    }
}
           

运行查询并填写数据表

这是很基本的。设置命令对象,添加参数,然后只需使用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;
}
           

跟进(重构)

我确实重构了这段代码(在下载中),这使它有点混乱。我想让它变得灵活,这样我就可以在多个项目中使用它。由于我无法确定db连接字符串将始终是OLEDB或SqlClient连接,因此我检查了连接字符串并为其中任何一个使用了适当的库集(OLEDB/SQLClient)。代码长度增加了一倍,但更便携。

更新:导致错误的其他事情

一位朋友帮我运行了一些测试报告,结果表明如果出现问题,该ReportViewer控件不会生成任何好的/有用的错误消息。相比之下,我放在View页面的“Download”按钮,在处理过程中很容易报错。从中,我学到了一些东西:

  1. 如果报表具有外部图形或报表部件,则这些文件需要可用(也就是说,您也需要/reports文件夹中的这些文件)并且在正确的路径中。对于我的测试示例,报告使用文件夹/Reports/Web Parts/中的图形。
  2. 如果报告具有必需的参数(或不是“可选的”),则必须提供这些参数,否则报告将不会运行。
  3. 参数可以区分大小写。也许它只是.NET。无论哪种方式,如果您的报告无法运行,请检查以确保您提供的是“PrintID”或“PrintId”,而不仅仅是“printid”。

最后,我最近更新了这个示例以使用Visual Studio 2013运行。我添加了一个“诊断”页面,以检查一些设置。我改进了“下载”选项。我改进了这个例子来处理外部图像,并检测所需的报告参数。

我希望你发现它对你更有效。如果没有,我会很感激反馈,甚至可能是一个示例文件(.rdl),这样我就可以解决任何错误。

结论

这就是从RDLC文件中提取查询并在ASP.NET中运行它所需的全部内容。

SSRS最初是由Microsoft编写的,作为如何使用这些技术来完成我在此处展示的内容的示例。当然,SSRS的许多功能远远超出了我所展示的范围,但如果您不需要所有这些丰富的功能,那么此代码对于您和您的.NET项目来说应该是快速且可移植的。

https://www.codeproject.com/Articles/607382/Running-a-RDL-RDLC-SQL-Report-in-ASP-NET-without-S

继续阅读