SQL Server 2000 Reporting Services durch benutzerdefinierten Code erweitern
Dieser Artikel ist ein aus dem Englischen übersetzter Auszug aus dem Buch Microsoft Reporting Services in Action von Teodor Lachev. In dem Artikel wird erläutert, wie Sie benutzerdefinierten Code verwenden, um erweiterte Berichtsfunktionen zu implementieren. Dieser Artikel enthält auch Links zu englischsprachigen Seiten.
Laden Sie die Beispielcodedatei Code.zip für diesen Artikel herunter.
Auf dieser Seite
Erweitern von Microsoft SQL Server 2000 Reporting Services durch benutzerdefinierten Code
Schreiben von eingebettetem Code
Verwenden von externen Assemblys
Benutzerdefinierter Code in Aktion: Implementieren von Berichtsprognosen
Migrieren von OpenForecast
Zusammenfassung
Erweitern von Microsoft SQL Server 2000 Reporting Services durch benutzerdefinierten Code
Microsoft hat Anfang 2004 Microsoft SQL Server 2000 Reporting Services (Reporting Services) veröffentlicht. Reporting Services bieten Entwicklern eine vollständige Berichtsplattform, die unabhängig von der Zielplattform oder Entwicklungssprache bequem in alle Arten von Anwendungen zu integrieren ist. Für viele Entwickler (mich eingeschlossen) ist ein herausragendes Merkmal von Reporting Services besonders wertvoll: die umfassende Erweiterbarkeit. Nahezu jeder Aspekt von Reporting Services kann erweitert oder ersetzt werden. Dazu gehören Datenverarbeitungs-, Übermittlungs- und Sicherheitsfunktionen sowie Funktionen zum Übertragen von Berichten. Eine Möglichkeit zum Erweitern der Berichtsfunktionen ist zum Beispiel die Integration von Berichten und benutzerdefiniertem .NET-Code, den Sie oder andere Entwickler verfasst haben.
In diesem Artikel zeige ich Ihnen, wie Sie die einmalige, erweiterbare Architektur von Reporting Services nutzen, um zusätzliche Berichtsfunktionen zu erzeugen. Zunächst erhalten Sie Informationen über die Funktionsweise von eingebetteten und benutzerdefinierten Codeoptionen. Anschließend zeige ich Ihnen, wie Sie benutzerdefinierten Code nutzen, um einen erweiterten Bericht mit Funktionen für die Absatzprognose zu erstellen.
Ich setze voraus, dass Sie grundlegende Kenntnisse über Reporting Services besitzen und wissen, wie Sie Berichte mit Ausdrücken verfassen. Wenn Sie Reporting Services noch nicht kennen, besuchen Sie die offizielle Website. Die erläuterten Codebeispiele und Beispielberichte sind im Quellcode für diesen Artikel enthalten. Als Datenquelle verwenden die Beispielberichte die AdventureWorks2000-Datenbank, die mit der Setup-Anwendung von Reporting Services installiert werden kann.
Schreiben von eingebettetem Code
Wie der Name schon vermuten lässt, wird eingebetteter Code in der Berichtsdefinitionsdatei (RDL - Report Definition Language) gespeichert und ist auf Berichtsebene gültig. Eingebetteten Code können Sie nur in Microsoft Visual Basic .NET schreiben. Den fertigen Code können Sie mithilfe des global definierten Code-Members in Ihren Berichtsausdrücken aufrufen. Wenn Sie zum Beispiel eine eingebettete Codefunktion namens GetValue geschrieben haben, können Sie sie aus den Ausdrücken mit der folgenden Syntax aufrufen:
=Code.GetValue()
Mit Ausnahme von gemeinsamen Methoden kann der eingebettete Code jeden Code enthalten, der zu Visual Basic .NET kompatibel ist. Sie können sich den eingebetteten Code tatsächlich fast als eine private Klasse innerhalb Ihres Projekts vorstellen sowie Members und Konstanten, private oder öffentliche Methoden usw. auf Klassenebene deklarieren.
Sie können mit eingebettetem Code wiederverwendbare Dienstprogrammfunktionen erstellen, die von mehreren Ausdrücken im Bericht aufgerufen werden. Betrachten Sie als Beispiel die "Territory Sales Crosstab" (Kreuztabelle Absatz nach Gebiet), die in Abbildung 1 gezeigt wird.
Abbildung 1. Verwenden Sie eingebetteten Code, um nützliche Dienstprogrammfunktionen auf Berichtsebene zu implementieren.
Dieser Bericht verwendet die eingebettete GetValue-Funktion, um "N/A" (nicht zutreffend) anzuzeigen, wenn keine Daten vorliegen (für eine gegebene Zeilen-Spalten-Kombination sind keine Berichtsdaten vorhanden). Des Weiteren unterscheidet GetValue zwischen nicht vorhandenen Daten und NULL-Werten. Wenn der zugrunde liegende Wert NULL ist, übersetzt der eingebettete Code ihn in null.
Verwenden des Code-Editors
Verwenden Sie zum Schreiben von eingebettetem Code den Code-Editor im Berichts-Designer im Dialogfeld Berichtseigenschaften auf der Registerkarte Code (siehe Abbildung 2).
Abbildung 2. Verwenden Sie den Code-Editor, um eingebetteten Code zu schreiben. Die im Editor gezeigte "GetValue"-Funktion bestimmt, ob ein Wert fehlt oder "NULL" ist.
Sicherlich kann die Funktion oben leicht durch einen Iif-basierten Ausdruck ersetzt werden. Das Kapseln der Logik in eine eingebettete Funktion hat allerdings zwei Vorteile: Zum einen wird die Logik des Ausdrucks an einer Stelle zentralisiert, anstatt für alle Felder im Bericht Iif-Funktionen zu verwenden. Zum anderen ist der Bericht besser zu warten, da Sie nicht jede einzelne Iif-Funktion im Bericht finden und ändern müssen, um eine logische Änderung an der Funktion vorzunehmen.
Der Berichts-Designer speichert eingebetteten Code in der Berichtsdefinitionsdatei im <Code>-Element. Dabei führt der Berichts-Designer eine URL-Codierung für den Text aus. Wenn Sie aus irgendeinem Grund beschließen, das Code-Element direkt zu ändern, müssen Sie dies berücksichtigen.
Verarbeiten von fehlenden Werten
Wenn die GetValue-Funktion fertig ist, können als Grundlage für die txtSales- und txtNoOrders-Datenfelder der Kreuztabellenberichte folgende Ausdrücke verwendet werden, um im Bericht zwischen NULL und fehlenden Daten zu unterscheiden:
=Iif(CountRows()=0, "N/A", Code.GetValue(Sum(Fields!Sales.Value)))
und
=Iif(CountRows()=0, "N/A", Code.GetValue(Sum(Fields!NoOrders.Value)))
Die CountRows-Funktion ist eine von mehreren systemeigenen Funktionen, die Reporting Services bereitstellt. Sie gibt die Anzahl der Zeilen in einem gegebenen Bereich zurück. Wird kein Bereich angegeben, gilt standardmäßig der innerste Bereich, in diesem Fall die statische Gruppe, die die Werte in den Datenzellen definiert. Beide Ausdrücke verwenden zunächst die CountRows-Funktion, um auf fehlende Daten (keine Zeilen) zu prüfen, und zeigen "N/A" an, wenn keine fehlenden Daten ermittelt werden. Andernfalls rufen die Ausdrücke die eingebettete GetValue-Funktion auf, um die NULL-Werte zu übersetzen.
Eingebetteter Code ist zu empfehlen, wenn Sie einfache, berichtsspezifische Funktionen schreiben möchten, die Dienstprogrammen ähneln. Wenn Ihre Programmierlogik komplexer wird, können Sie externe Assemblys für den Code verwenden, wie im nächsten Abschnitt erläutert wird.
Verwenden von externen Assemblys
Die zweite Möglichkeit für eine programmtechnische Erweiterung von Berichten ist die Verwendung vorgefertigter Logik aus externen Assemblys, die in jeder .NET-Sprache geschrieben werden können. Durch die Integration von Berichten und benutzerdefiniertem Code in externen Assemblys stehen Ihnen wesentlich mehr Programmieroptionen zur Verfügung. Benutzerdefinierter Code bietet Ihnen beispielsweise die folgenden Möglichkeiten:
-
Sie können die umfassenden Funktionen des .NET Framework nutzen, wenn Sie beispielsweise eine Auflistung benötigen, um Kreuztabellendaten eines Matrixbereichs zu speichern und einige Berechnungen auszuführen. Sie können jede Auflistungsklasse, die .NET bereitstellt, "ausleihen", beispielsweise Array, ArrayList oder Hashtable.
-
Sie können benutzerdefinierte, von Ihnen oder Drittanbietern geschriebene .NET-Assemblys für Ihre Berichte verwenden. Zum Beispiel habe ich das Open Source-Paket OpenForecast verwendet, um in Abschnitt 2 dem Bericht "Sales by Product Category" (Absatz nach Produktkategorie) Prognosefunktionen hinzuzufügen.
-
Mit der leistungsstarken Visual Studio .NET-IDE können Sie Code im Vergleich zum einfachen Code-Editor viel komfortabler schreiben.
Referenzieren von externen Assemblys
Um Typen zu verwenden, die sich in einer externen Assembly befinden, müssen Sie im Berichts-Designer im Dialogfeld Berichtseigenschaften auf der Registerkarte Verweise die Assembly angeben (siehe Abbildung 3).
Abbildung 3. Verweisen Sie im Dialogfeld "Berichtseigenschaften" auf eine externe Assembly.
Wenn der Bericht die benutzerdefinierte AWC.RS.Library-Assembly (im Quellcode für den Artikel enthalten) benötigt, müssen Sie zuerst auf der Registerkarte Verweise auf die Assembly verweisen. Sie können auf dieser Registerkarte eine Assembly in einem beliebigen Ordner suchen und auf sie verweisen. Wenn der Bericht ausgeführt wird, verwendet die .NET Common Language Runtime (CLR) allerdings die CLR-Testregeln, um die Assembly zu finden. Kurz gesagt, haben Sie durch diese Regeln zwei Optionen für das Bereitstellen der benutzerdefinierten Assembly:
-
Bereitstellen der Assembly als private Assembly.
-
Bereitstellen der Assembly als gemeinsame Assembly im globalen .NET-Assemblycache (GAC - Global Assembly Cache). Dies setzt voraus, dass Sie der Assembly einen starken Namen geben. Weitere Informationen zur Vergabe starker Namen finden Sie in der .NET-Dokumentation.
Wenn Sie die erste Option wählen, müssen Sie die Assembly sowohl für den Berichts-Designer als auch für den Berichtsserver bereitstellen, damit die Berichte, die auf die Assembly verweisen, beim Testen und als verwaltete Berichte erfolgreich ausgeführt werden. Wenn Sie die Standardinstallationseinstellungen akzeptiert haben, kopieren Sie die Assembly in den Ordner C:\Programme\Microsoft SQL Server\80\Tools\Report Designer, um die Assembly im binären Ordner des Berichts-Designers bereitzustellen. Anschließend können Sie den Bericht in Visual Studio .NET in einer Seitenansicht erstellen und rendern.
Beim Bereitstellen des Berichts im Berichtskatalog müssen Sie die Assembly auch in den binären Berichtsserverordner kopieren. In der Standardinstallation ist dies der Ordner C:\Programme\Microsoft SQL Server\MSSQL\Reporting Services\ReportServer\bin.
Allerdings ist das Kopieren der benutzerdefinierten Assembly an den richtigen Speicherort nur die eine Hälfte der Bereitstellung. Abhängig davon, was Ihr Code ausführt, müssen Sie möglicherweise auch die Sicherheitsrichtlinie für den Codezugriff ändern, damit der Assemblycode erfolgreich ausgeführt werden kann. Weitere Informationen über das Bereitstellen von benutzerdefinierten Assemblys finden Sie in der Dokumentation zu Reporting Services im Abschnitt "Using Custom Assemblies with Reports".
Aufrufen von gemeinsamen Methoden
Wenn Sie in der Assembly nur gemeinsame Methoden (in C# auch als statisch bezeichnet) aufrufen müssen, können Sie gleich beginnen, da gemeinsame Methoden im Bericht global verfügbar sind.
Um gemeinsame Methoden aufzurufen, verwenden Sie den vollqualifizierten Typnamen mit der folgenden Syntax:
<Namespace>.<Type>.<Method>(argument1, argument2, ..., argumentN)
Wenn ich beispielsweise aus einem Ausdruck oder eingebettetem Code die gemeinsame GetForecastedSet-Methode in der RsLibrary-Klasse (AWC.RS.Library-Assembly) aufrufen möchte, verwende ich die folgende Syntax:
=AWC.Reporting Services.Library.RsLibrary.GetForecastedSet(forecastedSet, forecastedMonths)
Dabei ist AWC.RS.Library der Namespace, RsLibrary der Typ, GetForecastedSet die Methode, und forecastedSet und forecastedMonths sind die Argumente.
Aufrufen von Instanzenmethoden
Um eine Instanzenmethode aufzurufen, müssen Sie noch einige Vorbereitungen treffen. Zunächst sind alle Instanzklassen (Typen) aufzulisten, die Sie im Classes-Raster instanziieren müssen. Für jede Klasse ist die Zuordnung eines Instanznamens erforderlich. Reporting Services erstellt unbemerkt eine Variable dieses Namens, um über einen Verweis auf die Instanz dieses Typs zu verfügen.
Stellen Sie sicher, dass Sie beim Festlegen des Klassennamens im Classes-Raster den vollqualifizierten Typnamen (einschließlich Namespace) angeben. In meinem Beispiel (siehe Abbildung 3) ist der Namespace AWC.RS.Library und der Klassenname RsLibrary. Wenn Sie den vollqualifizierten Klassennamen nicht sicher wissen, suchen Sie den Klassennamen und ermitteln Sie seinen Namespace. Verwenden Sie dazu den Visual Studio .NET-Objektbrowser oder andere Dienstprogramme, wie beispielsweise den hervorragenden Klassenbrowser .NET Reflector von Lutz Roeder.
Wenn beispielsweise eine Instanzmethode in der AWC.RS.Library-Assembly aufgerufen werden soll, muss nun eine m_Library-Instanzvariable deklariert werden, wie in Abbildung 3 gezeigt wird. In diesem Fall enthält die Variable einen Verweis auf die RsLibrary-Klasse.
Wenn Sie mehrere Variablen deklarieren, die auf den gleichen Typ zeigen, verweist jede auf eine separate Instanz dieses Typs. Beim Verarbeiten des Berichts instanziiert Reporting Services im Hintergrund eine Anzahl von Instanzen des referenzierten Typs, die der Anzahl der Instanzenvariablen entspricht.
Wenn Sie die Verweiseinstellungen vorgenommen haben, können Sie die von Ihnen angegebenen Instanztypennamen verwenden, um die Instanzmethoden aufzurufen. Genau wie bei eingebettetem Code verwenden Sie das Code-Schlüsselwort, um eine Instanzmethode aufzurufen. Im Unterschied zu einer gemeinsamen Methode nutzen Sie für den Aufruf der Instanzmethode aber nicht den Klassennamen, sondern den Variablennamen.
Wenn der RsLibrary-Typ zum Beispiel eine DummyMethod()-Instanzmethode hat, kann diese aus einem Ausdruck oder eingebettetem Code folgendermaßen aufgerufen werden:
Code.m_Library.DummyMethod()
Nachdem die Optionen dargestellt wurden, mit denen Entwickler Berichtsfunktionen programmtechnisch ändern können, soll nun ihre praktische Anwendung folgen. Im nächsten Abschnitt wird erläutert, wie eingebetteter und externer Code verwendet wird, um Berichten erweiterte Funktionen hinzuzufügen.
Benutzerdefinierter Code in Aktion: Implementieren von Berichtsprognosen
In diesem Abschnitt möchte ich Ihnen zeigen, wie Prognosefunktionen in Berichte integriert werden können. Der zu erstellende Beispielbericht soll folgende Entwurfsziele berücksichtigen:
-
Benutzer sollen einen Kreuztabellenbericht aus den Absatzdaten für einen beliebigen Zeitraum generieren können.
-
Benutzer sollen die Anzahl der Prognosespalten festlegen können.
-
Für die Prognose der Absatzdaten sollen Daten extrapoliert werden.
Hier ist das fiktive Szenario: Ihre Benutzer benötigen einen Bericht, der die monatlichen Absatzprognosen von Adventure Works darstellt, gruppiert nach Produktkategorien. Benutzer sollen einen Datumsbereich zum Filtern der Absatzdaten und die Anzahl der Prognosemonate festlegen können. Um den oben genannten Anforderungen zu entsprechen, wird der Kreuztabellenbericht "Sales by Product Category" (Absatz nach Produktkategorie) erstellt (siehe Abbildung 4).
Abbildung 4. "Sales by Product Category" (Absatz nach Produktkategorie) verwendet für die Prognose eingebetteten und externen benutzerdefinierten Code.
Die Benutzer können ein Start- und Enddatum angeben, um die Absatzdaten zu filtern. Sie können außerdem festlegen, für wie viele Monate eine Prognose im Bericht angezeigt wird. Der Bericht stellt die Daten als Kreuztabelle dar, mit den Produktkategorien in Zeilen und den Monaten in Spalten. Der Datenteil des Berichts zeigt zuerst die tatsächlichen Absatzdaten für den angeforderten Zeitraum, gefolgt von der fett formatierten Absatzprognose.
Wenn die Benutzer beispielsweise als Startdatum den 30.04.2003 eingeben, als Enddatum den 31.03.2004, und eine Prognose für drei Monate anfordern, zeigt der Bericht die Prognosedaten für April, Mai und Juni 2004 an (Abbildung 4 zeigt nur eine Prognose für einen Monat, um Platz zu sparen).
Wahrscheinlich stimmen Sie mir zu, dass eigene Prognosefunktionen schwierig zu implementieren sind. Aber wie sieht es aus, wenn vorgefertigter Code das übernimmt? Wenn dieser Code mit .NET ausgeführt werden kann, kann der Bericht auf ihn als benutzerdefinierten Code zugreifen. Im Folgenden geht es um OpenForecast.
Prognosen mit OpenForecast
Prognosen sind eine Wissenschaft für sich, die sich allgemein ausgedrückt mit Verfahren zur Voraussage des Unbekannten beschäftigt. Anstelle einer Kristallkugel verwenden Fachleute mathematische Modelle, um Daten zu analysieren, Trends zu ermitteln und begründete Schlussfolgerungen zu ziehen. In unserem Beispiel verwendet der Bericht "Sales by Product Category" (Absatz nach Produktkategorie) die Methode der Datenextrapolation, um die zukünftigen Absatzdaten vorauszusagen.
Es gibt eine Reihe bekannter mathematischer Modelle, um einen Satz von Daten zu extrapolieren, u. a. Polynom-Regression und einfache exponentielle Glättung. Das Implementieren eines dieser Modelle ist jedoch keine leichte Aufgabe. Für das Absatzprognosebeispiel wird hier stattdessen das großartige Open Source-Paket OpenForecast, verfasst von Steven Gould, verwendet. OpenForecast ist ein universell einsetzbares Paket und enthält javabasierte Prognosemodelle, die auf beliebige Datenreihen anwendbar sind. Kenntnisse über Prognosen sind nicht erforderlich. Das Paket unterstützt verschiedene mathematische Prognosemodelle, darunter die einfache und die multiple lineare Regression. Weitere Informationen zu OpenForecast finden Sie auf der Homepage: http://openforecast.sourceforge.net/.
Im Folgenden wird dargestellt, wie das Prognosebeispiel implementiert und OpenForecast integriert werden kann, indem eingebetteter und externer Code verwendet wird.
Implementieren von Prognoseberichtsfunktionen
Zum Erstellen eines Kreuztabellenberichts mit Prognosefunktionen sind mehrere Implementierungsschritte erforderlich. Es folgt zunächst eine allgemeine Darstellung des Planungsansatzes, anschließend werden die Implementierungsdetails erläutert.
Wählen eines Implementierungsansatzes
Abbildung 5 stellt den logischen Aufbau der Lösung dar.
Abbildung 5. Der Bericht "Sales by Product Category" (Absatz nach Produktkategorie) verwendet eingebetteten Code, um die "AwRsLibrary"-Assembly aufzurufen. Diese ruft wiederum das J#-OpenForecast-Paket auf.
Der Bericht verwendet eingebetteten Code, um eine gemeinsame Methode in einer benutzerdefinierten Assembly (AwRsLibrary) aufzurufen und die Prognosedaten zu erhalten. AwRsLibrary lädt die vorhandenen Absatzdaten in ein OpenForecast-DataSet und ruft ein Prognosemodell von OpenForecast ab. Anschließend erfolgt ein Aufruf an OpenForecast, um die prognostizierten Werte für die angeforderte Anzahl von Monaten zu erhalten. AwRsLibrary gibt die Prognosedaten an den Bericht zurück, in dem sie angezeigt werden.
Es stehen mindestens zwei Implementierungsoptionen zur Verfügung, um die Kreuztabellen-Absatzdaten an AwRsLibrary zu übergeben:
-
Erneutes Holen der Absatzdaten aus der Datenbank. Hierzu könnte der Bericht die ausgewählte Produktkategorie und die Monatswerte auf Zeilenbasis übergeben. Anschließend könnte AwRsLibrary die entsprechenden Absatzdaten aus der Datenbank abrufen.
-
Laden der bestehenden Absatzdaten in eine Art von Struktur, wobei im Bericht eingebetteter Code verwendet und die Struktur an AwRsLibrary übergeben wird.
Der zweite Ansatz bietet folgende Vorteile:
-
Die benutzerdefinierte Codelogik ist eigenständig. Die Datenbank muss nicht erneut abgefragt werden.
-
Verwenden der Standardsicherheitsrichtlinie für benutzerdefinierten Code. Die Standardsicherheitsrichtlinie für den Codezugriff muss für die AwRsLibrary-Assembly nicht erhöht werden. Bei Verwenden der ersten Option hingegen reichen die Standardeinstellungen der Sicherheitsrichtlinie für den Codezugriff nicht aus, da Reporting Services benutzerdefinierten Assemblys nur Ausführungsrechte gewährt, die nicht für einen Datenbankzugriff ausreichen. Im Fall von OpenForecast musste ich tatsächlich beiden Assemblys volle Vertrauenswürdigkeit gewähren, da dies zum erfolgreichen Ausführen von J#-Code erforderlich ist. Wenn C# als Programmiersprache gewählt wird, ist dies jedoch nicht nötig.
-
Es ist keine Datensynchronisation erforderlich. Die Daten in beiden Containern, dem Matrixbereich und dem AwRsLibrary-DataSet, müssen nicht synchronisiert werden.
Aus diesen Gründen habe ich den zweiten Ansatz gewählt. Für die Implementierung wird ein Ausdruck verwendet, um die Datenwerte im Matrixbereich zu füllen. Der Ausdruck ruft eingebetteten Code auf, um eine Arraystruktur zu laden, die im eingebetteten Code zeilenweise verwaltet wird. Sobald eine gegebene Zeile geladen ist, wird das Array an AwRsLibrary übergeben, um die Prognosedaten zu erhalten.
Im Folgenden werden die Implementierungsdetails erläutert, beginnend mit der Konvertierung von OpenForecast zu .NET.
Migrieren von OpenForecast
OpenForecast ist in Java geschrieben. Daher bestand die erste Hürde darin, OpenForecast in .NET zu integrieren. Zwei Möglichkeiten standen zur Auswahl:
-
Die Integration beider Plattformen kann mithilfe eines Java-to-.NET-Gateways eines Drittanbieters erfolgen. Angesichts der Komplexität dieses Ansatzes wurde dies schnell verworfen.
-
Portieren von OpenForecast in eine unterstützte .NET-Sprache. Hierfür bietet Microsoft zwei Optionen. Erstens kann der Microsoft Konvertierungs-Assistent für die Programmiersprache Java verwendet werden, um den Java-Code in C# zu konvertieren. Zweitens ist eine Konvertierung von OpenForecast in J# möglich. Dabei bliebe die Java-Syntax erhalten, obwohl zum Ausführen des Codes nicht Java Virtual Machine, sondern die .NET Common Language Runtime verwendet wird.
Ich entschied mich, OpenForecast in J# zu portieren. Der weitere Vorteil bei diesem Ansatz ist, dass die Open Source-Entwickler nur eine javabasierte Version von OpenForecast pflegen müssen.
Das Portieren von OpenForecast zu J# erwies sich als einfacher als erwartet. Ich habe ein neues J#-Bibliotheksprojekt mit der Bezeichnung OpenForecast erstellt und alle *.java-Quelldateien in das Projekt geladen. Die .NET-Version von OpenForecast ist im Quellcode zu diesem Artikel enthalten. Es mussten nur ein paar Kompilierungsfehler in MultipleLinearRegression behoben werden, die auftraten, da verschiedene Java-Hashtabellenmethoden in J# nicht unterstützt werden, zum Beispiel keySet(), entries() und Klonen von Hashtabellen. Ich habe auch eine WinForm-Anwendung (TestHarness) hinzugefügt, mit der Sie die konvertierte OpenForecast-Version testen können.
Implementieren der "AwRsLibrary"-Assembly
Der nächste Schritt war die Erstellung der benutzerdefinierten .NET-Assembly AwRsLibrary, die die Brücke zwischen dem eingebetteten Berichtscode und OpenForecast bildet. Ich habe AwRsLibrary als C#-Klassenbibliotheksprojekt implementiert. In dem Projekt habe ich eine RsLibrary-Klasse erstellt, die die statische (gemeinsame) GetForecastedSet-Methode offen legt. Der AwRsLibrary-Code für diese Methode ist im Beispielcode zu diesem Artikel enthalten.
Die GetForecastedSet-Methode erhält die vorhandenen Absatzdaten für eine gegebene Produktkategorie als DataSet-Array, genau wie die Anzahl der angeforderten Monate für die Prognose. Anschließend erfolgt die Integration von OpenForecast in fünf Schritten:
Schritt 1: Zuerst wird ein neues OpenForecast-DataSet erstellt und die vorhandenen Daten vom Matrixzeilenarray in dieses DataSet geladen.
Schritt 2: Als Nächstes wird ein gegebener Prognosemodus abgerufen. OpenForecast bietet Entwicklern die Möglichkeit, durch Aufrufen der getBestForecast-Methode das optimale mathematische Prognosemodell auf der Grundlage der gegebenen Datenreihe zu erhalten. Diese Methode untersucht das DataSet und testet einige Prognosemodelle, um das am besten geeignete auszuwählen. Wenn das zurückgegebene Modell nicht geeignet ist, können Sie ein Prognosemodell ausdrücklich anfordern. Dazu instanziieren Sie eine der Klassen, die sich im Modellprojektordner befinden.
Schritt 3: Ein weiteres DataSet wird vorbereitet, das die prognostizierten Daten enthalten soll. Es wird mit einer Anzahl von Elementen initialisiert, die der Anzahl der Prognosemonate entspricht.
Schritt 4: Schließlich wird die forecast-Methode aufgerufen, um die Daten zu extrapolieren und die Ergebnisse der Prognose zurückzugeben.
Schritt 5: Im letzen Schritt werden die Prognosedaten zurück in das DataSet-Array geladen, um sie an den eingebetteten Berichtscode zurückgeben zu können.
Wenn die AwRsLibrary- und OpenForecast-.NET-Assemblys fertiggestellt sind, müssen Sie diese bereitstellen.
Bereitstellen von benutzerdefinierten Assemblys
Benutzerdefinierte Assemblys müssen sowohl für den Berichts-Designer als auch für den Berichtsserver in den binären Ordnern bereitgestellt werden. Diese Bereitstellung besteht aus den folgenden Schritten:
-
Kopieren der Assemblys in die binären Ordner des Berichts-Designers und des Berichtsservers.
-
Anpassen der codebasierten Sicherheit, wenn der benutzerdefinierte Code einen erhöhten Sicherheitsberechtigungssatz für den Codezugriff erfordert.
Damit beide Assemblys,
AwRsLibrary und OpenForecast, zur Entwurfszeit verfügbar sind, müssen AWC.RS.Library.dll und OpenForecast.dll in den Berichts-Designer-Ordner kopiert werden; bei einer Standardinstallation ist dies C:\Programme\Microsoft SQL Server\80\Tools\Report Designer.
Um den bereitgestellten Bericht erfolgreich im Berichtsserver zu rendern, müssen beide Assemblys ebenso im binären Ordner des Berichtsservers bereitgestellt werden. In der Standardeinstellung ist dies der Ordner C:\Programme\Microsoft SQL Server\MSSQL\Reporting Services\ReportServer\bin. Der Berichtsserver lässt tatsächlich erst dann zu, dass Sie einen Bericht aus der Visual Studio .NET-IDE bereitstellen, wenn alle referenzierten benutzerdefinierten Assemblys schon bereitgestellt wurden.
Die Reporting Services-Standardsicherheitsrichtlinie für den Codezugriff gewährt allen benutzerdefinierten Assemblys standardmäßig Ausführungsrechte. Für J#-Assemblys sind jedoch voll vertrauenswürdige Codezugriffsberechtigungen erforderlich. Da die .NET Common Language Runtime die Aufrufliste prüft, um sicherzustellen, dass alle Aufrufenden über die erforderlichen Berechtigungen verfügen, muss die Sicherheitsrichtlinie für den Codezugriff für beide Assemblys auf volle Vertrauenswürdigkeit erhöht werden. Dazu müssen Sie die Sicherheitskonfigurationsdateien für Berichts-Designer und Berichtsserver ändern.
Um Sie bei der Einrichtung der Sicherheitsrichtlinie für den Codezugriff zu unterstützen, finden Sie im Ordner Config eine Kopie meiner Datei rssrvpolicy.config. Gegen Ende der Datei zeigen zwei CodeGroup-XML-Elemente auf die Dateien AwRsLibrary und OpenForecast. Kopieren Sie diese Elemente in die Sicherheitskonfigurationsdatei des Berichtsservers (rssrvpolicy.config).
Wenn Sie den Bericht in der Berichtsvorschau im Berichts-Designer vorab ausführen möchten, müssen Sie die Änderungen außerdem auch in die Sicherheitskonfigurationsdatei (rspreviewpolicy.config) des Berichts-Designers übertragen.
Nach dem Bereitstellen der benutzerdefinierten Assemblys ist im Bericht etwas eingebetteter Visual Basic .NET-Code notwendig, um die AwRsLibrary-Assembly aufzurufen.
Schreiben von eingebettetem Berichtscode
Um den Bericht mit AwRsLibrary zu integrieren, habe ich die GetValue-Funktion geschrieben, die in Listing 2 gezeigt wird.
Listing 2. Die eingebettete "GetValue"-Funktion ruft die "AwRsLibrary"-Assembly auf
Dim forecastedSet() As Double ' array with sales data Dim productCategoryID As Integer = -1 Dim bNewSeries As Boolean = False Public Dim m_ExString = String.Empty ' holds the error message, if any Function GetValue(productCategoryID As Integer, orderDate As DateTime, sales As Double, _ reportParameters as Parameters, txtRange as TextBox) As Double Dim startDate as DateTime = reportParameters!StartDate.Value Dim endDate as DateTime = reportParameters!EndDate.Value Dim forecastedMonths as Integer = reportParameters!ForecastedMonths.Value If (forecastedSet Is Nothing) Then ReDim forecastedSet(DateDiff(DateInterval.Month, startDate, endDate) + forecastedMonths) #1 End If If Me.productCategoryID <> productCategoryID Then #2 Me.productCategoryID = productCategoryID bNewSeries = True Array.Clear(forecastedSet, 0, forecastedSet.Length - 1) End If Dim i = DateDiff(DateInterval.Month, startDate , orderDate) ' Is this a forecasted value? If orderDate <= endDate Then ' No, just load the value in the array forecastedSet(i) = sales Else If bNewSeries Then Try AWC.RS.Library.RsLibrary.GetForecastedSet(forecastedSet, forecastedMonths) #3 bNewSeries = False Catch ex As Exception m_ExString = "Exception: " & ex.Message System.Diagnostics.Trace.WriteLine(ex.ToString()) throw ex End Try End If End If Return forecastedSet(i) End Function
Die Datenzellen im Matrixbereich verwenden einen Ausdruck, der die
GetValue-Funktion referenziert. Diese Funktion wird daher von jeder Datenzelle aufgerufen. Tabelle 1 listet die Eingabeargumente für die GetValue-Funktion auf.
Tabelle 1. Jede Datenzelle im Matrixbereich ruft die eingebettete "GetValue"-Funktion auf und übergibt die folgenden Eingabeargumente:
|
Argument |
Zweck |
|
productCategoryID |
Der ProductCategoryID-Wert aus der rowProductCategory-Zeilengruppierung, die der Zelle entspricht. |
|
orderDate |
Der OrderDate-Wert aus der colMonth-Spaltengruppierung, die der Zelle entspricht. |
|
sales |
Die Gesamtabsatzsumme für diese Zelle. |
|
reportParameters |
Um die Arraygröße zu berechnen, benötigt GetValue die Werte der Berichtsparameter. Anstatt die Parameter mithilfe von Parameters!ParameterName.Value einzeln zu übergeben, übergebe ich einen Verweis auf die Auflistung der Berichtsparameter. |
|
txtRange |
Eine Variable, die die Fehlermeldung enthält, wenn beim Abrufen der Prognosedaten eine Ausnahme ausgelöst wird. |
Um die Funktionsweise von
GetValue zu verstehen, müssen Sie wissen, dass jede Datenzelle im Matrixbereich vom forecastedSet-Array gefüllt wird. Wenn die Zelle keine Prognose erfordert (ihr entsprechendes Datum somit innerhalb des angeforderten Datumsbereichs liegt), wird nur der Zellenwert in das Array geladen und für die Anzeige im Matrixbereich zurückgegeben. Damit dies funktioniert, muss das Array initialisiert werden, so dass sein Rang der Anzahl der angeforderten Monate plus der Anzahl der Prognosemonate entspricht. Sobald der Matrixbereich sich in eine neue Zeile bewegt und die Funktion aufruft, können die Daten durch Aufrufen der AwRsLibrary:GetForecastedSet-Methode prognostiziert werden.
Implementieren des Kreuztabellenberichts "Sales by Product Category" (Absatz nach Produktkategorie)
Der schwierigste Teil beim Verfassen des Berichts selbst war, die Daten so einzurichten, dass immer die richtige Anzahl von Spalten im Matrixbereich für die Anzeige der Prognosespalten sichergestellt ist. In der Standardeinstellung werden Spalten, die keine Daten haben, im Matrixbereich nicht angezeigt. Das verhindert die Berechnung des richtigen Offsets, um die Zellen aus dem Array zu füllen.
Aus diesem Grund muss sichergestellt werden, dass die Datenbank Datensätze für alle Monate innerhalb des angeforderten Datumsbereichs zurückgibt. Um dies zu implementieren, müssen die Absatzdaten in der Datenbank vorverarbeitet werden. Diese Aufgabe wird von der gespeicherten Prozedur spGetForecastedData übernommen, in der eine benutzerdefinierte Tabelle vorab mit allen monatlichen Zeiträumen im angeforderten Datumsbereich gefüllt wird, wie Listing 3 zeigt.
Listing 3. Die gespeicherte Prozedur spGetForecastedData stellt sicher, dass das zurückgegebene Rowset die richtige Spaltenanzahl aufweist
CREATE PROCEDURE spGetForecastedData ( @StartDate smalldatetime, @EndDate smalldatetime ) AS DECLARE @tempDate smalldatetime DECLARE @dateSet TABLE ( #1 ProductCategoryID tinyint, OrderDate smalldatetime ) SET @tempDate = @EndDate WHILE (@StartDate <= @tempDate) #2 BEGIN INSERT INTO @dateSet SELECT ProductCategoryID, @tempDate FROM ProductCategory SET @tempDate = DATEADD(mm, -1, @tempDate) END SELECT DS.ProductCategoryID, PC.Name as ProductCategory, OrderDate AS Date, NULL AS Sales FROM @dateSet DS INNER JOIN ProductCategory PC ON DS.ProductCategoryID=PC.ProductCategoryID UNION ALL #3 SELECT PC.ProductCategoryID, PC.Name AS ProductCategory, SOH.OrderDate AS Date, SUM(SOD.UnitPrice * SOD.OrderQty) AS Sales FROM ProductSubCategory PSC INNER JOIN ProductCategory PC ON PSC.ProductCategoryID = PC.ProductCategoryID INNER JOIN Product P ON PSC.ProductSubCategoryID = P.ProductSubCategoryID INNER JOIN SalesOrderHeader SOH INNER JOIN SalesOrderDetail SOD ON SOH.SalesOrderID = SOD.SalesOrderID ON P.ProductID = SOD.ProductID WHERE (SOH.OrderDate BETWEEN @StartDate AND @EndDate) GROUP BY SOH.OrderDate, PC.Name, PC.ProductCategoryID ORDER BY PC.Name, OrderDate
Zuletzt werden alle Sätze aus der Tabelle @dateSet (deren Werte in der Spalte Sales (Absatz) auf NULL gesetzt sind) mit der tatsächlichen Transact-SQL-Anweisung vereint, die die Absatzdaten holt.
Wenn das DataSet eingerichtet ist, ist der restliche Bericht leicht zu verfassen. Für den Kreuztabellenteil des Berichts wird ein Matrixbereich verwendet. Um zu verstehen, wie der Matrixbereich funktioniert und die eingebettete GetValue-Funktion aufruft, können Sie den Ausdruck des Textfelds txtSales durch folgenden Ausdruck ersetzen:
= Fields!ProductCategoryID.Value & "," & Fields!Date.Value & "," & Format(Fields!Sales.Value, "C")
Abbildung 6 zeigt den Bericht "Sales by Product Category" (Absatz nach Produktkategorie), wenn dieser Ausdruck angewendet wird.
Abbildung 6. Aggregation von Daten im Matrixbereich
Wie Sie sehen, können die entsprechenden Zeilen- und Spaltengruppenwerte auf einfache Weise abgerufen werden, mit denen der Matrixbereich die aggregierten Werte in den Bereichsdatenzellen berechnet. Auf diese Weise besteht die Möglichkeit, jede Datenzelle zu identifizieren. Die Einrichtung des Matrixbereichs wird in Tabelle 2 gezeigt.
Tabelle 2. Um den Matrixbereich mit Prognosewerten zu füllen, basieren die Datenzellen auf einem Ausdruck.
|
Matrixbereich |
Name |
Ausdruck |
|
Zeilen |
rowProductGroup |
=Fields!ProductCategory.Value |
|
Spalten |
colYear
|
=Fields!Date.Value.Year
|
|
Daten |
txtSales |
=Code.GetValue(Fields!ProductCategoryID.Value, Fields!Date.Value, Sum(Fields!Sales.Value), Parameters, ReportItems!txtRange) |
Um eine bedingte Formatierung (Fett) für die Prognosespalten zu implementieren, wird für die Schriftarteigenschaft des txtSales-Textfelds folgender Ausdruck verwendet:
=Iif(Code.IsForecasted(Fields!Date.Value, Parameters!EndDate.Value), "Bold", "Normal")
Dieser Ausdruck ruft die IsForecasted-Methode aus dem eingebetteten Berichtscode auf. Die Funktion vergleicht einfach das Datum des Absatzmonats mit dem angeforderten Enddatum. Wenn das Absatzdatum vor dem Enddatum liegt, gibt sie FALSE zurück.
Schließlich muss nur noch auf der Registerkarte References (Verweise) des Berichts auf die AwRsLibrary-Assembly verwiesen werden, wie in Abbildung 3 gezeigt wurde. Für diesen Bericht ist kein Instanzname erforderlich (es ist keine Eingabe in die Tabelle Classes (Klassen) notwendig), da keine Instanzmethoden aufgerufen werden.
Debuggen von benutzerdefiniertem Code
Das Debuggen von benutzerdefiniertem Code kann unter Umständen eine nicht unerhebliche Aufgabe sein. Daher möchte ich Ihnen einige nützliche Methoden vorstellen.
Für das Debuggen von eingebettetem Code sind nicht viele Optionen verfügbar. Bisher habe ich nur die Möglichkeit herausgefunden, die MsgBox-Funktion zu verwenden, um Meldungen und Variablenwerte auszugeben, wenn der Bericht im Berichts-Designer gerendert wird. Entfernen Sie auf jeden Fall die MsgBox-Aufrufe, bevor Sie den Bericht für den Berichtsserver bereitstellen. Andernfalls führen alle MsgBox-Aufrufe zu Ausnahmen. Aus irgendeinem Grund werden Ablaufverfolgungsmeldungen, die System.Diagnostics.Trace (OutputDebugString-API) verwenden, nicht ausgegeben. Sie werden weder im Visual Studio .NET-Ausgabefenster noch bei Verwendung eines externen Verfolgungstools angezeigt.
Wenn Sie mit externen Assemblys arbeiten, haben Sie zumindest zwei Optionen für das Debuggen:
-
Ausgeben von Ablaufverfolgungsmeldungen.
-
Verwenden des Visual Studio .NET-Debuggers, um den benutzerdefinierten Code schrittweise abzuarbeiten.
Ablaufverfolgung
Als Beispiel gebe ich in der AwRsLibrary.GetForecastedSet-Methode Ablaufverfolgungsmeldungen aus. Dabei wird System.Diagnostics.Trace.WriteLine verwendet, um die beobachteten und prognostizierten Werte anzuzeigen. Um diese Meldungen anzuzeigen, wenn der Bericht in Visual Studio .NET oder dem Berichtsserver ausgeführt wird, können Sie das erstklassige Tool DebugView von Mark Russinovich verwenden, das in Abbildung 7 gezeigt wird.
Abbildung 7. Ausgeben von Ablaufverfolgungsmeldungen von externen Assemblys in DebugView.
Debuggen von benutzerdefiniertem Code
Sie können den benutzerdefinierten Assemblycode auch schrittweise mit dem Visual Studio .NET-Debugger durchgehen, indem Sie diesen an den Prozess im Berichts-Designer anhängen. Dazu führen Sie Folgendes aus:
-
Öffnen Sie die benutzerdefinierte Assembly, die Sie debuggen möchten, in einer neuen Instanz von Visual Studio .NET. Legen Sie wie gewohnt Haltepunkte im Code fest.
-
Wählen Sie in den Projekteigenschaften der benutzerdefinierten Assembly Configuration Properties->Debugging (Konfigurationseigenschaften->Debuggen) aus und für den Debug Mode (Debugmodus) die Option Wait to Attach to an External Process (Auf das Anhängen an einen externen Prozess warten).
-
Öffnen Sie in einer neuen Instanz von Visual Studio .NET Ihr Business Intelligence-Projekt.
-
Kehren Sie zum Projekt der benutzerdefinierten Assembly zurück. Klicken Sie auf das Menü Debug (Debuggen) und anschließend auf Processes... (Prozesse...). Suchen Sie den Prozess devevn, der das Business Intelligence-Projekt enthält, und hängen Sie an diesen Prozess an. Stellen Sie im Dialogfeld Attach To Process (An den Prozess anhängen) sicher, dass das Kontrollkästchen Common Language Runtime aktiviert ist, und klicken Sie auf Attach (Anfügen). An dieser Stelle sollte Ihr Dialogfeld Processes (Prozesse) so aussehen wie in Abbildung 8.
Abbildung 8. Um benutzerdefinierte Assemblys zu debuggen, hängen Sie an die Instanz von Visual Studio an, die Ihr Business Intelligence-Projekt enthält.
Im vorliegenden Fall möchte ich den Code in der AwRsLibrary-Assembly debuggen, wenn sie vom Bericht "Sales by Product Category" (Absatz nach Produktkategorie) aufgerufen wird. Daher hänge ich im AwRsLibrary-Projekt an den AWReporter-Prozess, devenv, an.
-
Sie können im Business Intelligence-Projekt eine Vorschau für den Bericht anzeigen, der die benutzerdefinierte Assembly aufruft. Wenn Sie bereits eine Vorschau des Berichts angezeigt haben, klicken Sie im Preview Panel (Vorschaubereich) auf die Schaltfläche Refresh Report (Bericht aktualisieren). An diesem Punkt sollten Ihre Haltepunkte vom Visual Studio .NET-Debugger erkannt werden.
Wie Sie bald herausfinden werden, wird folgende Ausnahme ausgelöst, wenn Sie den Code ändern müssen, die benutzerdefinierte Assembly erneut kompilieren und sie im Berichts-Designer erneut bereitstellen möchten:
Cannot copy <assembly name>: It is being used by another person or program.
Das Problem besteht darin, dass die Visual Studio .NET-IDE einen Verweis auf die benutzerdefinierte Assembly enthält. Sie müssen Visual Studio .NET beenden und anschließend die neue Assembly erneut bereitstellen. Um dies zu vermeiden, können Sie den Report Host (Fenster Vorschau) verwenden, um den benutzerdefinierten Assemblycode zu debuggen. Führen Sie dazu folgende Schritte aus:
-
Fügen Sie die benutzerdefinierte Assembly der Visual Studio .NET-Projektmappe hinzu, die Ihr Business Intelligence-Projekt enthält.
-
Ändern Sie das Startelement des Business Intelligence-Projekts auf den Bericht, der den benutzerdefinierten Code aufruft, wie in Abbildung 9 dargestellt.
Abbildung 9. Verwenden der Debugoption von Report Host, um das Sperren von Assemblys zu vermeiden.
-
Drücken Sie F5, um den Bericht in der Vorschau auszuführen. Wenn der Bericht den benutzerdefinierten Code aufruft, werden die Haltepunkte erkannt.
Wenn Sie diesen Ansatz verwenden, werden die benutzerdefinierten Assemblys nicht von Visual Studio .NET gesperrt. Daher können Sie den Speicherort zum Erstellen Ihrer Assembly auf den Ordner des Berichts-Designers ändern, damit er immer die aktuellste Kopie enthält, wenn Sie die Assembly erneut erstellen. Das Ausführen Ihrer Projekte in der Vorschau ist Gegenstand der Sicherheitsrichtlinieneinstellungen für den Codezugriff, die in der Konfigurationsdatei des Berichts-Designers (rspreviewpolicy.config) festgelegt sind.
Zusammenfassung
In diesem Artikel wurde die Integration von Berichten und benutzerdefiniertem Code, den Sie oder ein Dritter geschrieben haben, erläutert.
Verwenden Sie Visual Basic .NET-Code für einfache, berichtsspezifische Programmierlogik. Wenn der Code komplexer wird oder Sie andere Programmiersprachen bevorzugen, können Sie externe Assemblys für Ihren Code verwenden.
Die Verwendung von benutzerdefiniertem Code ist nur eine der Möglichkeiten, mit denen Entwickler Reporting Services erweitern können. Weitere Informationen zur Erweiterbarkeit von Reporting Services finden Sie im Abschnitt "Extending Reporting Services" (Erweitern von Reporting Services) in der Reporting Services-Onlinedokumentation.
Weitere Informationen:
www.microsoft.com/germany/sql/
Verwandte Literatur:
Microsoft Reporting Services in Action
„Die Inhalte der hier eingestellten Artikel stammen möglicherweise nicht von Microsoft, sondern von Dritten und werden Ihnen kostenlos zur Verfügung gestellt. Microsoft kann daher für die Richtigkeit und Vollständigkeit der Inhalte keine Haftung übernehmen.“