Extreme ASP.NET
Diagrammerstellung mit ASP.NET und LINQ
K. Scott Allen
Codedownload verfügbar in der MSDN-Codegalerie
Code online durchsuchen
Inhalt
Erste Schritte
Diagrammerstellung
Einfügen von Daten
Von Diagrammen zu Dashboards
Ein Blick in die Zukunft
Microsoft hat kürzlich ein neues Diagrammsteuerelement für ASP.NET 3.5 SP1 veröffentlicht. Das Diagrammsteuerelement ist leicht zu verwenden und ermöglich das Hinzufügen attraktiver Datenvisualisierungen zu Ihren ASP.NET-Webanwendungen. Das Steuerelement unterstützt alle Standarddiagrammtypen wie Zeilendiagramme und Kreisdiagramme sowie erweiterte Visualisierungen wie Trichter und Pyramidendiagramme. In diesem Artikel wird das Diagrammsteuerelement erläutert. Außerdem werden einige Daten mithilfe von Abfragen generiert, die für LINQ to Objects geschrieben wurden. Der vollständige Quellcode für diesen Artikel ist in der MSDN-Codegalerie verfügbar.
Abbildung 1 Visual Studio-Tools
Erste Schritte
Der erste Schritt ist das Herunterladen des Diagrammsteuerelements vom Microsoft-Downloadcenter. Bei diesem Download werden die wichtigen Laufzeitkomponenten installiert, die für die Diagrammerstellung erforderlich sind, einschließlich Platzierung der System.Web.DataVisualization-Assembly im globalen Assemblycache.
Außerdem müssen die Visual Studio 2008-Toolunterstützung und die Beispielwebsite für die Diagrammerstellung herunterladen werden. Die Toolunterstützung bietet Ihnen Toolboxintegration und IntelliSense-Unterstützung zur Entwurfszeit, während die Beispielwebsite Hunderte von Beispielen enthält, die als Anregung beim Erstellen des gewünschten Diagramms dienen. Beachten Sie, dass Service Pack 1 sowohl für die Microsoft .NET Framework 3.5-Laufzeit als auch für Visual Studio 2008 installiert sein muss.
Nach Abschluss der Installationen können Sie ein neues ASP.NET-Projekt in Visual Studio erstellen, wobei das Diagrammsteuerelement im Toolbox-Fenster sichtbar ist (siehe Abbildung 1). Sie können das Diagramm mithilfe der Entwurfsansicht (die dazu einige erforderliche Konfigurationsänderungen an Ihrer web.config-Datei vornimmt) in eine ASPX-Datei ziehen oder direkt mit dem Steuerelement in der Quellansicht einer ASPX-Datei arbeiten (in diesem Fall müssen die Konfigurationsänderungen, die gleich beschrieben werden, manuell durchgeführt werden).
Wenn Sie ein Diagramm im Web Forms-Designer platzieren, fügt Visual Studio einen neuen Eintrag im <controls>-Abschnitt der web.config-Datei ein:
<add tagPrefix="asp"
namespace="System.Web.UI.DataVisualization.Charting"
assembly="System.Web.DataVisualization,
Version=3.5.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35" />
Dieser Eintrag ermöglicht Ihnen die Verwendung des Diagrammsteuerelements mit dem vertrauten asp-Tagpräfix, das von anderen integrierten ASP.NET-Steuerelementen verwendet wird. Visual Studio fügt im IIS 7.0-<handlers>-Abschnitt auch einen neuen HTTP-Handlereintrag hinzu und einen ähnlichen Eintrag im <httpHandlers>-Abschnitt (zur Verwendung mit IIS 6.0 und dem Visual Studio-Webentwicklungsserver):
<add name="ChartImageHandler"
preCondition="integratedMode"
verb="GET,HEAD"
path="ChartImg.axd"
type="System.Web.UI.DataVisualization.Charting.Chart HttpHandler,
System.Web.DataVisualization,
Version=3.5.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35" />
Dieser HTTP-Handler ist für das Verarbeiten von Anforderungen verantwortlich, die für ChartImg.axd eingehen. Dabei handelt es sich um den Standardendpunkt, den das Diagrammsteuerelement für das Aufstellen von Diagrammen verwendet. Später wird noch näher auf den HTTP-Handler eingegangen. Der Code in Abbildung 2 zeigt die grundlegenden Elemente eines Diagramms. Jedes Diagramm enthält mindestens ein Reihenobjekt, das mit Daten aufgefüllt ist. Die ChartType-Eigenschaft jeder Reihe legt die Art des Diagramms fest, das zur grafischen Darstellung der Reihe verwendet wird (Standardtyp ist ein Säulendiagramm). Jedes Diagramm kann auch ein oder mehrere ChartArea-Objekte enthalten, in denen die grafische Darstellung erfolgt.
Abbildung 2 Ein grundlegendes Diagramm
<asp:Chart ID="Chart1" runat="server" Height="300" Width="400">
<Series>
<asp:Series BorderColor="180, 26, 59, 105">
<Points>
<asp:DataPoint YValues="45" />
<asp:DataPoint YValues="34" />
<asp:DataPoint YValues="67" />
</Points>
</asp:Series>
</Series>
<ChartAreas>
<asp:ChartArea />
</ChartAreas>
</asp:Chart>
Sie können fast jedes visuelle Element des ASP.NET-Diagrammsteuerelements anpassen, einschließlich Hintergründen, Achsen, Titeln, Legenden und Beschriftungen. Das Diagrammsteuerelement kann so hochgradig angepasst werden, dass Sie einen Plan aufstellen sollten, um ein einheitliches Aussehen von Diagrammen in Ihrer Anwendung zu gewährleisten. In diesem Artikel wird eine Generatorstrategie verwendet, um einheitliche Schriftarten und Farben auf alle Diagramme anzuwenden. Diese Generatorklasse ermöglicht auch die Verwendung des Diagrammsteuerelements außerhalb der Beschränkungen einer ASP.NET-Seite.
Diagrammerstellung
Bei der Suche nach Beispieldaten für diesen Artikel habe ich es kurz in Betracht gezogen, Verlaufsdaten vom Börsenmarkt zu verwenden, doch die Daten des letzten Jahres waren so deprimierend, dass ich mich stattdessen für die Daten des US-amerikanischen Büros für Transportstatistik (bts.gov) entschieden habe. Die Beispielanwendung für diesen Artikel enthält eine Datei mit Informationen zu jedem Inlandsflug von meinem Heimatflughafen (Baltimore Washington International oder BWI) im Januar 2008. Die Daten umfassen den Zielort, die Entfernung, die Rollzeiten sowie Verspätungen. Diese Daten sind in C# mithilfe der in Abbildung 3 verwendeten Klasse dargestellt.
Abbildung 3 Flugdaten
public class Flight {
public DateTime Date { get; set; }
public string Airline { get; set; }
public string Origin { get; set; }
public string Destination { get; set; }
public int TaxiOut { get; set; }
public int TaxiIn { get; set; }
public bool Cancelled { get; set; }
public int ArrivalDelay { get; set; }
public int AirTime { get; set; }
public int Distance { get; set; }
public int CarrierDelay { get; set; }
public int WeatherDelay { get; set; }
}
Das erste anhand dieser Daten erstellte Diagramm zeigt die beliebtesten Ziele für Flüge vom Flughafen Baltimore aus (siehe Abbildung 4). Dieses Diagramm wurde mit sehr wenig Code in der ASPX-Datei oder der zugeordneten CodeBehind-Datei erstellt. Die ASPX-Datei enthält ein Diagrammsteuerelement auf der Seite, legt jedoch nur die Width- und Height-Eigenschaften fest, wie Sie hier sehen:
<form id="form1" runat="server">
<div>
<asp:Chart runat="server" Width="800" Height="600" ID="_chart">
</asp:Chart>
</div>
</form>
Abbildung 4 Die beliebtesten Zielorte (zum Vergrößern auf das Bild klicken)
Der CodeBehind delegiert die gesamte Arbeit bei der Page_Load-Ereignisverarbeitung an eine TopDestinationsChartBuilder-Klasse, wie hier dargestellt:
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
var builder = new TopDestinationsChartBuilder(_chart);
builder.BuildChart();
}
}
Die TopDestinationsChartBuilder-Klasse erbt von einer ChartBuilder-Klasse. Diese ChartBuilder-Basisklasse verwendet ein Vorlagenmethodenentwurfsmuster, um die Teile eines Diagramms zusammenzustellen. Die Vorlagenmethode gibt den grundlegenden Algorithmus an, der zum Erstellen eines ästhetisch einheitlichen und funktionalen Diagramms erforderlich ist, bietet jedoch Hooks für eine Unterklasse, um die zusammengestellten Teile anzupassen. Die Vorlagenmethode heißt BuildChart und ist in Abbildung 5 dargestellt.
Abbildung 5 BuildChart
public void BuildChart() {
_chart.ChartAreas.Add(BuildChartArea());
_chart.Titles.Add(BuildChartTitle());
if (_numberOfSeries > 1) {
_chart.Legends.Add(BuildChartLegend());
}
foreach (var series in BuildChartSeries()) {
_chart.Series.Add(series);
}
}
Jeder Build-Methode in der ChartBuilder-Klasse ist eine Customize-Methode zugeordnet. Wenn die Build-Methode ihr Teil des Diagramms erstellt hat, ruft sie die Customize-Methode auf. Eine abgeleitete Klasse kann die Anpassungsmethode außer Kraft setzen, um diagrammspezifische Einstellungen anzuwenden. Ein solches Beispiel ist die BuildChartTitle-Methode, die in Abbildung 6 dargestellt ist. Die TopDestinationsChartBuilder-Klasse setzt die CustomizeChartTitle-Klasse außer Kraft, um den spezifischen Text für einen Titel anzuwenden:
protected override void CustomizeChartTitle(Title title)
{
title.Text = "Top Destinations";
}
Abbildung 6 BuildChartTitle
private Title BuildChartTitle() {
Title title = new Title() {
Docking = Docking.Top,
Font = new Font("Trebuchet MS", 18.0f, FontStyle.Bold),
};
CustomizeChartTitle(title);
return title;
}
protected virtual void CustomizeChartTitle(Title title) { }
Der größte Teil der Arbeit für die TopDestinationsChartBuilder-Klasse besteht im Anpassen der Diagrammreihe, wobei unter anderem alle Datenpunkte für die Anzeige hinzugefügt werden. Glücklicherweise ist es mithilfe von LINQ to Objects äußerst einfach, die beliebtesten fünf Ziele in einer Sammlung von Flugobjekten zu finden, wie in Abbildung 7 zu sehen ist. Der Code wendet zuerst den GroupBy-Operator von LINQ an, um die Flüge nach Zielort zu gruppieren. Der OrderByDescending-Operator sortiert dann die Sequenz der Gruppierungen nach der Anzahl von Flügen in jeder Gruppe. Schließlich verwendet der Code den Take-Operator zum Identifizieren der beliebtesten fünf Ziele der Sequenz.
Abbildung 7 Abrufen der fünf beliebtesten Ziele
protected override void CustomizeChartSeries(IList<Series> seriesList) {
var repository = FlightRepositoryFactory.CreateRepository();
var query = repository.Flights
.GroupBy(flight => flight.Destination)
.OrderByDescending(group => group.Count())
.Take(5);
Series cities = seriesList.Single();
cities.Name = "Cities";
foreach (var record in query) {
cities.Points.AddXY(record.Key, record.Count());
}
}
Die LINQ-Abfrage erstellt eine Sequenz von Gruppierungen. Sie können diese Gruppierungen durchlaufen, um dem Diagramm Informationen hinzuzufügen. Die Key-Eigenschaft jeder Gruppe repräsentiert den Wert des Schlüssels, der im GroupBy-Operator ausgewählt wurde (in diesem Fall ist es der Wert des Zielorts). Der Code verwendet das Ziel als X-Wert für jeden Datenpunkt. Jede Gruppe enthält auch eine aufzählbare Sequenz der Flugobjekte, die unter diesem Zielort gruppiert wurden. Sie müssen nur den Count-Operator verwenden, um die Gesamtzahl der Flüge an die einzelnen Zielorte abzurufen, und die Zahl als Y-Wert für jeden Datenpunkt hinzufügen.
Einfügen von Daten
Statt einer Diagrammreihe Daten Punkt für Punkt hinzuzufügen, können Sie eine DataBindXY-Methode in der DataPointCollection des Diagramms verwenden, um eine Sequenz von Datenpunkten ohne Verwendung einer Schleife einzufügen. Dies geschieht im DelaysByDayChartBuilder, der die Summe aller Verspätungen (in Minuten) für jeden Tag im Monat berechnet. Dieser Generator erstellt auch eine zweite Datenreihe, die alle wetterbezogenen Verspätungen anzeigt. Der Generator beginnt mit einer LINQ-Abfrage, die die Flüge nach dem Monatstag gruppiert. Die Key-Eigenschaft für jede Gruppe stellt nun den Tag des Monats dar (vom 1. bis zum 31. Januar):
var query = repository.Flights
.GroupBy(flight => flight.Date.Day)
.OrderBy(group => group.Key)
.ToList();
Im Code in Abbildung 8 wird die DataBindXY-Methode verwendet. Zuerst werden alle X-Werte mithilfe des Select-Operators in einer Liste gesammelt, um den Key-Wert jeder Gruppe abzurufen. Als Nächstes wird die anfängliche Gruppenabfrage weiter verarbeitet, um die Werte ArrivalDelay und WeatherDelay innerhalb jeder Gruppe zu summieren.
Abbildung 8 Einfügen von Werten ohne Schleifendurchlauf
var xValues = query.Select(group => group.Key).ToList();
totalDelaySeries.Points.DataBindXY(
xValues,
query.Select(
group => group.Sum(
flight => flight.ArrivalDelay)).ToList());
weatherDelaySeries.Points.DataBindXY(
xValues,
query.Select(
group => group.Sum(
flight => flight.WeatherDelay)).ToList());
Wie Sie sehen, ist es mithilfe der Standard-LINQ-Operatoren einfach, die Daten für Berichte und Diagramme zu analysieren und zu ändern. Ein weiteres hervorragendes Beispiel ist im TaxiTimeChartBuilder enthalten. Diese Klasse stellt ein Radardiagramm zusammen, um die gesamte Rollzeit von Flugzeugen vor dem Abflug für jeden Wochentag anzuzeigen. Dabei handelt es sich um die Zeitdauer, die ein Flugzeug vom Verlassen des Flugsteigs bis zum Abheben von der Startbahn braucht. An überfüllten Flughäfen kann sich diese Zeit stark erhöhen, wenn Flugzeuge in einer Warteschlange stehen und auf eine freie Startbahn warten. Der TaxiTimeChartBuilder hebt die Datenpunkte hervor, in der diese Zeitdauer einen Schwellenwert überschreitet. Diese Aufgabe kann mithilfe einer LINQ-Abfrage der Datenpunkte in einer Reihe höchst einfach gelöst werden:
var overThresholdPoints =
taxiOutSeries.Points
.Where(p => p.YValues.First() > _taxiThreshold);
foreach (var point in overThresholdPoints)
{
point.Color = Color.Red;
}
Hier wird die Farbe jedes Datenpunkts, der den angegebenen Schwellenwert überschreitet, in Rot umgeändert. Bei diesem Verhalten denken Sie möglicherweise an einen Flughafendashboardbericht, und daher geht es als Nächstes um Dashboards.
Von Diagrammen zu Dashboards
In Business Intelligence-Kreisen werden Dashboards verwendet, um wichtige Leistungsdaten zu einem Unternehmen grafisch darzustellen. Diese Leistungsdaten können aus verschiedenen Quellen stammen und mit einer Reihe von Diagrammen und Widgets visualisiert werden. Der Benutzer sollte nach einem kurzen Blick auf die Dashboardanzeige schnell Anomalien identifizieren können, die sich auf das Geschäft auswirken könnten.
Eine Herausforderung beim Erstellen einer Dashboardanzeige ist die Leistung. Das Zusammenstellen aller Informationen für ein Dashboard kann zu einer Vielzahl von Abfragen bei relationalen wie auch mehrdimensionalen Datenbanken führen. Selbst wenn die Rohdaten für ein Dashboard zwischengespeichert werden, kann die große Zahl von Diagrammen und Widgets auf einem Dashboard zur Belastung des Servers führen.
In Abbildung 9 ist ein einfaches Dashboard für einen Flughafen aufgeführt, das die beliebtesten Ziele, die Rollzeit nach Wochentag, die Gesamtzahl der Flüge für jeden Wochentag und die gesamte Verspätungszeit für jeden Tag des Monats anzeigt. Ein Ansatz beim Erstellen dieses Dashboards besteht darin, alle vier Diagrammsteuerelemente auf einer einzelnen Webseite zu platzieren und die Diagrammgeneratorklassen zu verwenden, die zum Zusammenstellen der Diagramme erstellt wurden. Stellen Sie sich jedoch vor, dass für das Erstellen jedes Diagramms zwei Sekunden erforderlich sind. Normalerweise sind zwei Sekunden Wartezeit bei einer komplizierten Abfrage nicht lang, doch da auf der Seite insgesamt vier Diagramme vorhanden sind (eine für ein Dashboard geringe Zahl), muss der Benutzer mindestens acht Sekunden warten, bis die erste Grafik angezeigt wird.
Abbildung 9 Dashboard mit Flughafenaktivitäten
Ein anderer Ansatz beim Erstellen dieser Dashboardseite besteht darin, Platzhalter für jedes Diagramm auf der Seite zu definieren und die Diagrammbilder asynchron zu erstellen. Beim asynchronen Ansatz sieht der Benutzer das erste Ergebnis, sobald es verfügbar ist. Bei dieser Lösung können Windows Communication Foundation (WCF)-Proxys in JavaScript genutzt werden, um die Diagramme gleich nach Verfügbarkeit anzuzeigen.
Glücklicherweise erleichtern die für Diagramme definierten Generatorklassen es, die Generierungslogik für die Diagramme hinter WCF-Webdienste zu verschieben. Der Code in Abbildung 10 zeigt eine WCF-Dienstmethode, die zuerst ein leeres Diagramm erstellt und dann eine der vom ChartBuilder abgeleiteten Klassen erstellt, die im Projekt implementiert wurden. Der Generator wird als Parameter für die Methode angegeben, und der Code prüft diesen Parameter anhand eines Schemas bekannter Generatoren, um den Generatortyp zu finden, der instanziiert werden soll (dazu wird Activator.CreateInstance verwendet).
Abbildung 10 Instanziieren eines Generators
[OperationContract]
public string GenerateChart(string builderName) {
Chart chart = new Chart() {
Width = 500, Height = 300
};
ChartBuilder builder =
Activator.CreateInstance(_typeMap[builderName], chart)
as ChartBuilder;
builder.BuildChart();
return SaveChart(builderName, chart);
}
Die Verwendung eines Schemas bekannter Generatorentypen bedeutet, dass der Methode kein eigentlicher Typname übergeben werden muss. Zudem wird dadurch eine Isolierungsschicht vor schädlichen Eingaben über das Internet bereitgestellt. Sie instanziieren nur Typen, die der Dienst kennt.
Das Erstellen des Diagrammbilds im Webdienst ist nicht einfach. Wie bereits erwähnt, arbeitet das Diagramm mit einem HTTP-Handler, um Diagramme auf dem Client zu rendern. Insbesondere übergibt das Diagrammsteuerelement der ChartHttpHandler-Klasse die Bytes, die das fertige Abbild des Diagramms repräsentieren. Der ChartHttpHandler antwortet mit einem eindeutigen Bezeichner. Beim Rendern mithilfe des Diagrammsteuerelements wird ein reguläres HTML-<img>-Tag mit dem src-Attribut erstellt, das auf ChartImg.axd verweist und den eindeutigen Bezeichner in der Abfragezeichenfolge enthält. Wenn diese Bildanforderung den HTTP-Handler erreicht, kann der Handler das richtige Diagramm für die Anzeige suchen. Weitere Einzelheiten zu diesem Prozess, einschließlich aller Konfigurationsoptionen, sind im Blog von Delian Tchoparino enthalten.
Leider sind die APIs für den HTTP-Handler nicht öffentlich und stehen daher in einem Webdienst nicht zur Verfügung. Stattdessen verwendet die SaveChart-Methode, die vom Dienstcode in Abbildung 10 aufgerufen wird, die SaveImage-Methode des Diagrammsteuerelements, um das Diagrammbild im PNG-Format in das Dateisystem zu schreiben. Der Dienst gibt dann den Namen der Datei an den Client zurück. Durch Generieren physischer Dateien können Sie auch eine Zwischenspeicherungsstrategie einführen und Abfragen und Abbildgenerierung bei starker Auslastung vermeiden.
Im Code in Abbildung 11 wird der WCF-Dienst aus JavaScript verwendet, um das src-Attribut des Abbildplatzhalters für alle Diagramme festzulegen. (Der Artikel Clientbasierte Webdienstaufrufe mit AJAX Extensions von Fritz Onion erläutert, wie JavaScript-Proxys generiert und Webdienste mit JavaScript aufgerufen werden.) Der Bezeichner und die Generatorklasse des Dokumentobjektmodells (Document Object Model, DOM) für jedes Diagramm sind in einem JavaScript-Array definiert, das Teil eines „Kontext“-Objekts ist. Dieser Kontext wird durch aufeinander folgende Webdienstaufrufe im userState-Parameter der Webdienstproxymethoden getunnelt. JavaScript verwendet das Kontextobjekt, um den Fortschritt beim Aktualisieren der Dashboarddiagramme zu verfolgen. Bei dynamischen Dashboardseiten könnte der Server das Array dynamisch generieren.
Abbildung 11 Aktualisieren des Diagramms
/// <reference name="MicrosoftAjax.js" />
function pageLoad() {
var context = {
index: 0,
client: new ChartingService(),
charts:
[
{ id: "topDestinations", builder: "TopDestinations" },
{ id: "taxiTime", builder: "TaxiTime" },
{ id: "dayOfWeek", builder: "DayOfWeek" },
{ id: "delaysByDay", builder: "DelaysByDay" }
]
};
context.client.GenerateChart(
context.charts[context.index].builder,
updateChart,
displayError,
context);
}
function updateChart(result, context) {
var img = $get(context.charts[context.index].id);
img.src = result;
context.index++;
if (context.index < context.charts.length) {
context.client.GenerateChart(
context.charts[context.index].builder,
updateChart, displayError, context);
}
}
function displayError() {
alert("There was an error creating the dashboard charts");
}
Ein Blick in die Zukunft
Hier wurde ziemlich viel Technologie wie Vorlagenmethodenentwurfsmuster, LINQ-Operatoren und JavaScript-kompatible WCF-Webdienste abgedeckt. Da in diesem Artikel nur ein Bruchteil der Features vorgestellt wurde, die im Diagrammsteuerelement zur Verfügung stehen, sollten Sie sich unbedingt die Fülle an Features auf der Beispielwebsite ansehen. Die Verbindung dieses hervorragenden Visualisierungstools mit der Flexibilität und Ausdruckskraft von LINQ bedeutet, dass Sie Ihre zukünftigen Diagrammanwendungen flexibler, effektiver und nützlicher gestalten können.
K. Scott Allen ist technischer Mitarbeiter bei Pluralsight und Gründer von OdeToCode. Sie können Scott Allen unter scott@OdeToCode.com erreichen oder seinen Blog unter odetocode.com/blogs/scott lesen.