Использование ADO.NET DataSet в качестве источника данных для Reporting Services


Автор:
Опубликовано: сентябрь 2004 года

Продукты и технологии: ADO.NET 1.0, SQL Server 2000, Reporting Services

Как создать расширение для обработки данных для служб отчета SQL Server 2000 Reporting Services, в котором в качестве источника данных используется ADO.NET DataSet.


Загрузить программный код для этой статьи на Visual Basic

Загрузить программный код для этой статьи на C#

Оригинал статьи (EN)

Введение

Службы отчетов Reporting Services обеспечивают доступ к источникам данных SQL Server, Oracle, ODBC и OLE DB в качестве стандартных функций. Во многих случаях все, что требуется для отчета — это подключиться к базе данных и сделать запрос, после чего все данные для отчета будут получены. Но что делать, если в качестве источника данных требуется использовать DataSet? Например, если уже имеется промежуточный программный слой, на котором происходит обработка данных и результат хранится в виде DataSet. Или просто вы хотите перед публикацией в отчете обработать данные таким образом , который проще реализовать на Microsoft Visual Basic или C#, чем на SQL, и при этом набор DataSet мог бы быть логичным результатом такой обработки. К счастью, это вполне можно реализовать. Это даже сравнительно просто сделать, если вы выясните, какие расширения интерфейсов обработки данных действительно должны быть реализованы, чтобы обернуть DataSet так, чтобы с ним можно было работать из Reporting Services.

В этой статье мы рассмотрим создание и разработку простого расширения для обработки данных, которое можно использовать для передачи данных DataSet в отчет Reporting Services.

Общие сведения о расширениях для обработки данных

Службы отчетов Reporting Services дают возможность настраивать источники данных, которые доступны благодаря расширениям для обработки данных (data processing extensions). Расширение для обработки данных — это сборка, в которой реализован набор интерфейсов из пространства имен Microsoft.ReportingServices.DataProcessing. Существует шесть интерфейсов, которые должны быть реализованы в любом расширении для обработки данных.

Табл. 1. Шесть интерфейсов в расширении для обработки данных
Интерфейс
Описание
IDbConnection
Уникальное соединение с источником данных. В системе «клиент-сервер» может быть эквивалентно сетевому соединению с сервером.
IDbTransaction
Локальная транзакция.
IDbCommand
Запрос или команда, которые используются после подключении к источнику данных.
IDataParameter
Параметр или пара «имя/значение», которые передаются в команду или запрос.
IDataParameterCollection
Коллекция всех параметров, относящихся к команде или запросу.
IDataReader
Метод чтения потока данных из источника данных, только в одном направлении и только для чтения.

Существует еще несколько интерфейсов, которые обеспечивают дополнительную функциональность для подключений, транзакций и т. д., но эти функции не требуются в расширении для обработки данных DataSet. Подробнее о дополнительных интерфейсах можно почитать в разделе Preparing to Implement a Data Processing Extension (EN) в документации по Reporting Services (на английском языке).

Посмотрев на интерфейсы, перечисленные выше, можно убедиться, что создание расширения для обработки данных довольно похоже на создание поставщика данных в Microsoft .NET Framework. Часть интерфейсов имеет такие же названия и обеспечивают подобные (но не идентичные) функциональные возможности, поэтому, если вам доводилось раньше работать с интерфейсами System.Data, обязательно следует учитывать эти отличия.

В целях доступа к DataSet для половины этих интерфейсов необходима самая минимальная реализация, а остальные используются с самыми общими настройками. Давайте посмотрим, что нужно сделать, чтобы заставить это расширение работать.

Реализация расширения

Ниже рассмотрен пример приложения, в котором данные считываются из двух или более XML-файлов и затем собираются в одну таблицу в одном наборе данных DataSet. Имена файлов предоставляются как текст команд. Схема для проверки структуры файла считывается из файла настройки сервера Reporting Services с помощью свойства Connection. Этот файл и данные схемы используются для создания набора DataSet, который является источником данных для DataReader. В проектировщике Report Designer будет использоваться метод DataReader.Read для доступа к данным DataSet.

Стандартные действия

Откройте новый проект библиотеки классов в Microsoft Visual Studio и добавьте ссылку на файл Microsoft.ReportingServices.Interfaces.dll. Он содержит пространство имен Microsoft.ReportingServices.DataProcessing, на которое потребуется указать ссылку, чтобы получить доступ к расширениям для обработки данных.

Первым пунктом в нашей повестке дня являются классы, в которых реализуются интерфейсы IDbTransaction, IDataParameter и IDataParameterCollection. Эти классы включены только потому, что они требуются в любом расширении для обработки данных. Поскольку в нашем примере мы не подключаемся к базе данных, не изменяем данные и не используем SQL, эти классы не используются ни для чего. Их можно реализовать в формальном виде, как это сделано в коде примера, дополняющего эту статью.

В ваших приложениях может потребоваться более полная реализация этих интерфейсов. Например, в случае, если для получения данных в DataSet потребуется запросить их в реляционной базе данных.

Суть дела

Так как эти обязательные классы в нашем примере фактически не используются, давайте рассмотрим реализации IDbConnection, IDbCommand и IDataReader, которые и выполняют реальную работу в этом расширении.

Поскольку мы не подключаемся к базе данных, класс подключения используется для двух целей. Во-первых, с помощью метода SetConfiguration из конфигурационного файла считываются сведения о том, какая схема должна использоваться для проверки XML-данных. Сохранение сведений о схеме обеспечивает большую гибкость приложения, так как эти сведения всегда можно обновить, чтобы они соответствовали требуемому типу данных. В действительности используется два config-файла. Так как разработчик отчетов может и не иметь доступа к серверу отчетов Report Server, на котором этот отчет потом будет использоваться, требуются отдельные источники данных о конфигурации для Report Designer и Report Server. Поэтому RSReportServer.config обращается к данным для расширения, которое используется в развернутых отчетах, а RSReportDesigner.config — к данным расширения, которые используются в проектировщике в процессе разработки. В этих файлах настройки элементы, определяющие расширение для обработки данных, должны быть идентичны.

Во-вторых, мы используем класс Connection из-за его метода CreateCommand, с помощью которого создаем новый объект Command с помощью перегрузки объекта Connection. При этом будет предоставлен доступ к сведениям конфигурации в контексте объекта Command. Остальные члены IDbConnection описаны минимально.

using System;
using System.Data;
using System.Configuration;
using System.Xml;
using Microsoft.ReportingServices.DataProcessing;

namespace Microsoft.Samples.ReportingServices.DataSetExtension
{
   public class DSXConnection 
      : Microsoft.ReportingServices.DataProcessing.IDbConnection
   {
      private string _connString;
      // IDbConnection.ConnectionTimeout defaults to 15 seconds.
      private int _connTimeout = 15;
      private ConnectionState _state = ConnectionState.Closed;
      private string _locName = "DataSet Data Extension";
      internal string _xmlSchema;

      // Default constructor.
      public DSXConnection()
      {
      }

      // Connection string constructor overload.
      public DSXConnection(string connString)
      {
         _connString = connString;
      }

      public string ConnectionString
      {
         get
         {
            return _connString;
         }
         set
         {
            _connString = value;
         }
      }

      public int ConnectionTimeout
      {
         get
         {
            return _connTimeout;
         }
      }

      public ConnectionState State
      {
         get 
         {
            return _state;
         }
      }

      // Not used.
      public 
         Microsoft.ReportingServices.DataProcessing.IDbTransaction 
         BeginTransaction()
      {
         return (null);
      }

      // Not used.
      public void Open()
      {
         _state = ConnectionState.Open;
         return;
      }

      // Not used.
      public void Close()
      {
         _state = ConnectionState.Closed;
         return;
      }

      // Implemented.
      public 
         Microsoft.ReportingServices.DataProcessing.IDbCommand 
         CreateCommand()
      {
         // Create a Command object and pass in the
         // Connection object to provide config info.
           return new DSXCommand(this);
      }
      
      public string LocalizedName
      {
         get
         {
            return _locName;
         }
      }

      // Implemented. Inherited from 
      // IExtension through IDbConnection.
      public void SetConfiguration(string configuration)
      {

         // Get the XML schema file 
         // from the config file settings.
         XmlDocument schemaDoc = new XmlDocument();
         schemaDoc.LoadXml(configuration);
         if (schemaDoc.DocumentElement.Name 
            == "XSDConfiguration")
         {
            foreach (XmlNode schemaChild in 
               schemaDoc.DocumentElement.ChildNodes)
            {
               if(schemaChild.Name == "XSDFile")
               {
                  _xmlSchema = schemaChild.InnerText;
               }
               else
               {
                  throw new Exception
                     ("Cannot find XSD configuration element.");
               }
            }
         }
         else
         {
            throw new Exception
               ("Error returning data from the configuration file.");
         }
      }

      public void Dispose() 
      {
      }

      }
}

Затем следует реализация IDbCommand. Как и в случае с классом Connection, для нашей задачи необходимы только некоторые члены класса Command. В данном случае это перегруженный конструктор объекта Connection, свойство CommandText, а также перегрузка CommandBehavior в методе ExecuteReader.

Перегруженный конструктор обеспечивает ссылку на объект Connection. Все, что нам требуется, — это доступ к сведениям XML-схемы, которые сохранены как внутренняя переменная в DSXConnection. Эта переменная впоследствии передается в реализацию DataReader в вызове в ExecuteReader, и затем ее можно использовать при работе с источниками XML-данных.

В свойстве CommandText записывается строка, разделенная запятыми, в которой указаны XML-файлы. Эта строка вводится в проектировщике отчетов при настройке нового отчета.

При вызове ExecuteReader создаются DataReader и DataSet в качестве его источника данных. Ссылка на DataReader возвращается в вызвыющий объект, то есть в данном случае в наш отчет. Обратите внимание, что необходимо реализовать перегрузку CommandBehavior с поддержкой значения типа SchemaOnly. Как в проектировщике отчетов, так и на сервере отчетов используется эта перегрузка, а не версия без параметров, что позволяет получить сведения о полях в дополнение к основным данным.


using System;
using System.Data;
using System.ComponentModel;
using Microsoft.ReportingServices.DataProcessing;

namespace Microsoft.Samples.ReportingServices.DataSetExtension
{
   public class DSXCommand 
      : Microsoft.ReportingServices.DataProcessing.IDbCommand
   {
      private string _cmdText;
           private DSXConnection _connection;
      // IDbCommand.CommandTimeout defaults to 30 seconds.
      private int _cmdTimeout = 30;
      private Microsoft.ReportingServices.DataProcessing.CommandType 
         _cmdType; 
      private DSXParameterCollection _parameters = 
         new DSXParameterCollection();
                     
      // Default constructor.
      public DSXCommand()
      {
      }

      // Command text constructor overload.
      public DSXCommand(string cmdText)
      {
         _cmdText = cmdText;
      }

      // Connection object constructor overload.
      public DSXCommand(DSXConnection connection)
      {
         _connection = connection;
      }

       public string CommandText
      {
         get { return _cmdText;  }
         set  { _cmdText = value;  }
      }

      public int CommandTimeout
      {
         get  {return _cmdTimeout;}
         set  {_cmdTimeout = value;}
      }

      public Microsoft.ReportingServices.DataProcessing.CommandType 
         CommandType
      {
         get { return _cmdType; }
         set { _cmdType = value; }
      }

      public 
         Microsoft.ReportingServices.DataProcessing.IDataParameterCollection 
         Parameters
      {
         get  { return _parameters; }
      }

      public 
         Microsoft.ReportingServices.DataProcessing.IDbTransaction 
         Transaction
      {
         get { return (null); }
         set { throw new NotSupportedException(); }
      }

      // Not used.
      public void Cancel()
      {
         throw new NotSupportedException();
      }

      // Not used.
      public 
         Microsoft.ReportingServices.DataProcessing.IDataParameter 
         CreateParameter()
      {
         return (null);
      }

      // Implemented.
      public 
         Microsoft.ReportingServices.DataProcessing.IDataReader 
         ExecuteReader
         (Microsoft.ReportingServices.DataProcessing.CommandBehavior behavior)
      {
         try
         {
            // Create the DataReader.
            DSXDataReader testReader = 
               new DSXDataReader(_cmdText);
            // Call the custom method that 
            // populates the DataSet.
            testReader.CreateDataSet(_connection._xmlSchema);
            // Return the DataReader.
            return testReader;
         }
         catch(Exception e)
         {
            throw new Exception(e.Message);
         }
      }

      public void Dispose() 
      {
      }
        
   }
}

Наконец, существует класс DataReader, реализующий IDataReader. Этот класс наиболее полно конкретизирован, так как в проектировщике отчетов используется большинство его членов, чтобы получить данные для отображения. Также имеются две специальные функции CreateDataSet и ParseCmdText, которые обеспечивают дополнительные возможности. ParseCmdText разбирает входную строку с исходными XML-файлами, а CreateDataSet объединяет их в одном наборе DataSet для создания отчета. Конечный набор DataSet затем используется в качестве источника данных, которые DataReader возвращает.

using System;
using System.Data;
using System.Xml;
using System.Collections;
using Microsoft.ReportingServices.DataProcessing;

namespace Microsoft.Samples.ReportingServices.DataSetExtension
{
   public class DSXDataReader 
      : Microsoft.ReportingServices.DataProcessing.IDataReader
   {
      private string _cmdText;
      private int _currentRow = 0;
      private int _fieldCount = 0;
      private string _fieldName;
      private int _fieldOrdinal;
      private Type _fieldType;     
      private object _fieldValue;
      private DataSet _ds = null;

      // Default constructor
      internal DSXDataReader()
      {
      }

      //Command text constructor overload.
      internal DSXDataReader(string cmdText)
      {
            _cmdText = cmdText;
      }
      
      // Implemented. Will be called
      // by the Report Server to 
      // return DataSet data.
      public bool Read()
      {
         _currentRow++;

         if (_currentRow >= _ds.Tables[0].Rows.Count) 
         {
            return (false);
         } 
         else 
         {
            return (true);
         }
      }

      public int FieldCount
      {
         get 
         { 
            _fieldCount = _ds.Tables[0].Columns.Count;   
            return _fieldCount; 
         }
      }

      public string GetName(int i)
      {
         _fieldName = _ds.Tables[0].Columns[i].ColumnName;
         return _fieldName;
      }

      public Type GetFieldType(int i)
      {
         _fieldType = 
            _ds.Tables[0].Columns[i].DataType;
         return _fieldType;
      }

      public Object GetValue(int i)
      {
         _fieldValue = 
            _ds.Tables[0].Rows[this._currentRow][i];
         return _fieldValue;
      }

      public int GetOrdinal(string name)
      {
         _fieldOrdinal = 
            _ds.Tables[0].Columns[name].Ordinal;
         return _fieldOrdinal;
      }

      // Input parameter should be the path 
      // to the .xsd file that was retrieved 
      // from the Connection.SetConfiguration call.
        internal void CreateDataSet(string schemaFile)
      {
         
         // Open an XML doc to hold the data.
         XmlDocument xmlDoc = new XmlDocument();
         // Create the DataSet.
         DataSet ds = new DataSet("Customers");
         // Create the schema for the DataSet.
         ds.ReadXmlSchema(schemaFile);
         // Parse the command text string for the files.
            string[] parameters = this.ParseCmdText();
         // Get the XML data and 
         // merge it into the DataSet.
            try
         {
            for(int i=0;i<parameters.GetLength(0);i++)
            {
               DataSet tempDs = new DataSet();
               tempDs.ReadXml(parameters[i]);
               ds.Merge(tempDs);
            }
         
         }
         catch (Exception e)
         {
            throw new Exception(e.Message);
         }

         // Set the DataSet variable used in
         // the rest of the DataReader members
         // to the one just produced.
         _ds = ds;
         // Set the current row to -1
         // to prepare for reading.
         _currentRow = -1;

      }

      private string[] ParseCmdText()
      {
         // Check format of command text.
         if (_cmdText.IndexOf(",") != -1)
         {
            string[] dsParams = 
               _cmdText.Split(new Char[]{','});
            // In production code, you'd 
            // want more error handling here 
            // confirming that the string values 
            // are appropriate XML file names, etc.
            return dsParams;
         }
         else
            throw new ArgumentException
               ("The CommandText value is not in the appropriate format.");
      }

      public void Dispose() 
      {
      }

   }
}

Возможно, что у вас уже имеется компонент промежуточного слоя, в котором выполняется определенная обработка и создается набор DataSet, и именно его необходимо использовать вместо того, чтобы создавать новый набор DataSet в коде расширения. Это довольно легко: нужно добавить ссылку на сборку и вызвать метод, который возвращает DataSet, как показано в примере ниже:

using System;
using System.Data;
using System.Xml;
using System.Collections;
using Microsoft.ReportingServices.DataProcessing;
using ThisCompany.ThisAssembly;

namespace ThisCompany.ThisNamespace.DataSetExtension
{
   public class ExtensionDataReader : 
Microsoft.ReportingServices.DataProcessing.IDataReader
   {
         private DataSet _ds = null;
      // More variables...

      internal ExtensionDataReader(thisParameter) 
      {

         // Get the DataSet from the middle-tier assembly.
         this._ds = 
new ThisCompany.ThisAssembly.
Customer.IntegrateDataSources
(thisParameter);
         
         // Set the current row.
         currentRow = -1;
      }
   ...

В качестве резюме всего этого процесса можно сказать, что сервер отчетов должен выполнить следующие операции при запуске этого отчета:

вызвать конструктор по умолчанию DSXConnection;

вызвать метод DSXConnection.CreateCommand, в котором используется перегруженный конструктор DSXCommand Connection для создания объекта Command;

вызвать DSXCommand.ExecuteReader(CommandBehavior), который, в свою очередь:

вызывает перегруженный конструктор DSXDataReader с текстом команды и создает объект DataReader;

вызывает DSXDataReader.CreateDataSet и создает источник данных DataSet. Этот метод, в свою очередь, вызывает DSXDataReader.ParseCmdText для проверки ввода;

обработка возвращается в dSXCommand.ExecuteReader(CommandBehavior), который затем возвращает DataReader серверу отчетов.

Вот и все. Честно.

Теперь вы можете создать решение, и мы переходим развертыванию.

Установка программного расширения

Во-первых, нам потребуется зарегистрировать расширение в Reporting Services, для этого нужно скопировать DLL-файл с программой в папки Report Server и Report Designer и добавить соответствующие записи в файлы настроек (.config).

Скопируйте RSCustomData.DLL в папку сервера отчетов (по умолчанию C:\Program Files\Microsoft SQL Server\MSSQL\Reporting Services\ReportServer\bin) и папку проектировщика отчетов (по умолчанию C:\Program Files\Microsoft SQL Server\80\Tools\Report Designer). Затем откройте файл c настройками сервера отчетов, RSReportServer.config, который по умолчанию находится в папке C:\Program Files\Microsoft SQL Server\MSSQL\Reporting Services\ReportServer. В узел <Data> добавьте новый дочерний узел <Extension>. В нем нужно прописать все параметры, которые используются при работе расширения. Для элемента <Extension> требуются два атрибута: атрибут Name, уникальное имя вашего расширения, и атрибут Type, который содержит полное имя класса подключения, а также имя сборки (без расширения .dll), указанные через запятую. Это все, что требуется, узел <Configuration> дополнять не обязательно, если он не используется, хотя в нашем примере мы это делаем. Новый узел должен выглядеть следующим образом:


<Data>
   <Extension Name="DataSet"
Type="Microsoft.Samples.ReportingServices.DataSetExtension.DSXConnection,RSCustomData">
      <Configuration>
         <XSDConfiguration>
<XSDFile>
C:\customer.xsd
</XSDFile>
         </XSDConfiguration>

      </Configuration>
   </Extension>
</Data>

Если вы работаете с примером на Visual Basic, не забудьте изменить пространство имен на Microsoft.Samples.ReportingServices.DataSetExtensionVB.

Сохраните и закройте файл настройки сервера отчетов и откройте файл проектировщика отчетов RSReportDesigner.config из папкиC:\Program Files\Microsoft SQL Server\80\Tools\Report Designer. Добавьте аналогичный узел <Extension> в раздел <Data> этого файла. Кроме того, потребуется добавить немного отличающийся элемент <Extension> в узел <Designer>. Этот элемент <Extension> также содержит атрибуты Name и Type. Атрибут Name должен содержать такое же уникальное имя расширения, которое указано в элементе <Extension> в узле <Data>. В атрибуте Type через запятую указаны полное имя класса проектировщика запросов и имя содержащей его сборки (без расширения .dll).


<Extension 
Name="DataSet" Type="Microsoft.ReportDesigner.Design.GenericQueryDesigner,
Microsoft.ReportingServices.Designer"
/>

Благодаря универсальному интерфейсу в проектировщике отчетов при работе с расширением используются простые панели запросов и результатов, а не полнофункциональные визуальные мастера запросов, которые больше подходят для реляционных баз данных.

Разграничение доступа для кода (code Access Security)

Теперь нам нужно добавить элементы в файлы политики сервера и проектировщика отчетов, явно указав разрешения на доступ для кода расширения. Для успешной работы расширения для обработки данных необходимы разрешения FullTrust.

Откройте файл политики сервера отчетов rssrvpolicy.config, который по умолчанию находится в папке C:\Program Files\Microsoft SQL Server\MSSQL\Reporting Services\ReportServer, а затем создайте новый узел <CodeGroup>:

<CodeGroup class="UnionCodeGroup"
version="1"
PermissionSetName="FullTrust"
Name="DataSetExtensionGroup"
Description="This code group grants data extensions full trust.">
<IMembershipCondition 
class="UrlMembershipCondition"
version="1"
Url="C:\Program Files\Microsoft SQL Server\MSSQL\Reporting Services
\ReportServer\bin\RSCustomData.dll"
      />
</CodeGroup>

Затем добавьте идентичный узел <CodeGroup> в файл политики проектировщика отчетов rspreviewpolicy.config, расположенный по умолчанию в папке C:\Program Files\Microsoft SQL Server\80\Tools\Report Designer. Эти элементы предоставляют расширению DataSet права FullTrust, основываясь на его URL-адресе, в котором указана соответствующая сборка. Дополнительные сведения о разграничении доступа кода см.в статье Code Access Security in SQL Server 2000 Reporting Services (EN).

Создание отчета

Теперь, когда мы создали и развернули расширение с DataSet, очень просто использовать его в отчете. Откройте Visual Studio и начните новый проект с отчетом. Щелкните правой кнопкой мыши на папке Shared Data Sources и выберите пункт Add New Data Source. В раскрывающемся списке Type должна появиться строка с расширением для обработки данных DataSet.

Изображение GIF Рис. 1. Папка Shared Data Source

Выберите наше расширение. Затем на панели Credentials выберите вариант No Credentials. Учетные данные не требуются, так как к базе данных подключаться не нужно. Сохраните этот источник данных.

Щелкните правой кнопкой мыши на папке Reports и выберите пункт Add New Report. В окне мастера отчетов выполните нужные действия. На экране Select the Data Source примите настройки по умолчанию для только что созданного источника данных расширения DataSet.

Изображение GIF Рис. 2. Экран «Select the Data Source»

На экране Design the Query введите через запятую строку, содержащую полные пути ко всем исходным XML-файлам.

Изображение GIF Рис. 3. Экран «Design the Query»

Завершите создание отчета, указав необходимые вам параметры. В этом примере были приняты параметры по умолчанию. При предварительном просмотре наш пример отчета должен выглядеть как на рисунке (я немного изменила компоновку поля из-за ограничений по пространству).

Изображение GIF Рис. 4. Пример отчета Reporting Services

Чтобы установить отчет на сервере, укажите имя сервера (обычно http://MachineName/ReportServer) в свойствах проекта, а затем, чтобы установить решение, выберите Debug, Start (отчет также появится в браузере).

Заключение

Итак, после доработки API-интерфейсов и настройки конфигураций получить доступ к DataSets из служб отчета Reporting Services несложно. Используйте этот способ, когда в следующий раз вам нужно будет работать с данными для отчета.

Список литературы по теме

Hitchhiker's Guide to SQL Server 2000 Reporting Services (EN)

The Rational Guide to: SQL Server Reporting Services (EN)