JQuery-Tree mit ASP.NET-Komponenten
© Silvia Rothen, rothen ecotronics, Bern, Schweiz
Autorin: Dr. Silvia Rothen, rothen ecotronics, Bern, Schweiz
Letzte Überarbeitung: 04.09.2011
Mit dem Javascript-Framework JQuery lassen sich raffinierte Trees umsetzen. Andernorts habe ich schon detailliert gezeigt, wie man damit einen statischen, clientseitigen Baum aufbauen kann.
Auch hier geht es um einen Baum, der schon beim Laden der Seite vollständig eingelesen wird. Aber im Gegensatz zum erwähnten Artikel sollen die Äste des Baums ASP.NET-Komponenten beinhalten. Der Inhalt des Baumes stammt ausserdem aus einer Oracle-Datenbank. Mit dem Connect-By-Statement lassen sich ja Bäume beliebiger Tiefe aus einer Tabelle auslesen.
Das Beispiel verwendet CSharp als Programmiersprache und lässt sich ab ASP.NET 2.0 nachvollziehen.
Inhaltsverzeichnis
- Einleitung
- Vorbereitung
- Das praktische Beispiel
- Einbindung von JQuery
- HTML-Tree mit DIV-Tags und Hyperlinks
- CSS
- JQuery
- ASP.NET
- Beispiel und Download
Einleitung
Baumdarstellungen sind auf dem Computer allgegenwärtig. Im Web trifft man sie etwas weniger häufig, denn es gibt kein HTML-Tag, das direkt einen Baum unterstützt. Trees auf Webseiten sind meistens verschachtelte Tags, vor allem Listen oder DIV-Tags, die mit Javascript geöffnet und geschlossen werden. Kleinere Bäume werden dabei schon beim Start der Seite vollständig geladen. Ich spreche in diesem Fall von statischen Bäumen. Bei grossen Bäumen, z.B. der Navigation in Webshops mit mehreren Tausend Artikeln, führt dies zu unakzeptablen Ladezeiten. Dort werden Teile des Baums meist mit AJAX nachgeladen. Die AJAX-Version bezeichne ich als dynamisch.
Das vorliegende Beispiel ist eine Weiterentwicklung des Artikels über synchronisierbare Bäume mit JQuery. Der HTML- und JavaScript-Teil stützt sich auf diesen Artikel. Neu dazu kommen einerseits Oracle als Datenlieferant für den Baum und andererseits ASP.NET-Komponenten als Äste des Baumes. Die Schwierigkeit bei Letzterem besteht darin, dass der Baum nun wegen der Komponenten serverseitig im Code behind aufgebaut werden muss.
Vorbereitung
Bevor wir mit dem Erstellen der Seite beginnen können, müssen ein paar Vorbedingungen erfüllt sein:
- ASP.NET installiert, Version 2.0 oder neuer
- IDE für ASP.NET vorhanden, mindestens Visual Web Developer Express 2008 oder 2010
- Verbindung vom Entwicklungs-PC zu einer Oracle-Datenbank (im vorliegenden Fall handelt es sich um eine lokal installierte Instanz von Oracle XE). Falls dies fehlt, können Sie stattdessen hier eine Access-DB als Ersatz herunterladen. Sie simuliert in einer Tabelle die Daten, welche aus der Oracle-DB kommen.
- Um den Datenbank-Teil nachzuvollziehen, muss das von Oracle zu Lernzwecken bereitgestellte HR-Schema auf der DB installiert sein. Mit ein paar Anpassungen beim SQL kann man aber auch jede andere Tabelle verwenden, die einen Autojoin aufweist.
- ASP.NET-Projekt in CSharp erstellt
- Die neueste Version von JQuery (z.Z. 1.4.4)
Das praktische Beispiel
Inhaltlich besteht mein Beispiel aus einer Personalliste, wobei der Autojoin in der Tabelle die Unternehmenshierarchie wiedergibt, d.h. wer Manager von wem ist. Konkret kommt im HR-Schema von Oracle die Tabelle Employees zum Einsatz. Das Feld Manager_ID ist ein Fremdschlüssel, der eine Beziehung zur Employee_ID, dem Primärschlüssel derselben Tabelle erstellt. Zuoberst in der Hierarchie steht der Präsident (das Root-Element unseres Baumes), alle anderen Personen sind ihm auf verschiedenen Hierarchie-Stufen unterstellt.
Speziell an diesem Beispiel ist, dass die Anzahl Ebenen nicht konstant ist. Je nach Abteilung gibt es drei oder vier Hierarchiestufen. Die Zahl der Hierarchiestufen kann sich aber im Laufe der Zeit auch verändern, beispielsweise wenn das Unternehmen wächst und neue Management-Ebenen nötig werden. Dank dem datenbankseitigen Autojoin ist es möglich, jederzeit weitere Hierarchiestufen abzubilden. Ziel ist es, den Baum so zu programmieren, dass dies ohne Änderungen am Programm möglich ist.

Einbindung von JQuery
Als erstes erstellen Sie in ihrem Projekt eine neue ASPX-Seite. Das vorliegende Beispiel verwendet dazu C# und legt den Code in einer eigenen Seite ab (code behind).
Die gezeigte Lösung beruht auf JQuery allein, es ist nicht nötig, weitere JQuery-Pakete wie jquery.treeview zu verwenden. Mit folgendem Code binden Sie im Header der neu erstellten Seite JQuery ein:
<script type="text/javascript" src="../js/jquery-1.4.4.min.js"> </script>
Allerdings verwendet mein Beispiel die Icons für geöffnete oder geschlossene Äste und Endknoten aus dem jquery.treeview-Paket (siehe Abschnitt CSS). Aber Sie können drei andere geeignete Image-Dateien verwenden oder sogar auf Icons ganz verzichten.
HTML-Tree mit DIV-Tags und Hyperlinks
Das HTML für den Baum besteht aus verschachtelten DIV-Tags und Hyperlinks. Äste und Blätter werden dabei danach unterschieden, ob noch weitere DIV-Tags enthalten sind (Ast) oder nicht (Blatt). Der ganze Baum wird von einem Root-Tag umschlossen. Damit sieht das Grundgerüst des HTMLs im Browser folgendermassen aus:
<div style="border: 1px solid rgb(204, 204, 204);
padding: 10px;" class="ds_katgliederung">
<div class="ds_open" id="ds_101">
<a href="..">Kochhar Neena</a>
<div class="ds_close" id="ds_108">
<a href="..">Greenberg Nancy</a>
<div class="ds_leaf" id="ds_109">
<a href="..">Faviet Daniel</a>
</div>
<div class="ds_leaf" id="ds_110">
<a href="..">Chen John</a>
</div>
<div class="ds_leaf" id="ds_111">
<a href="..">Sciarra Ismael</a>
</div>
</div>
<div class="ds_leaf" id="ds_200">
<a href="..">Whalen Jennifer</a>
</div>
<div class="ds_leaf" id="ds_203">
<a href="">Mavris Susan</a>
</div>
...
</div>
</div>
Nicht alles, was Sie hier sehen, wird serverseitig zusammengebaut: Der Server gibt zwar jedem DIV-Tag ein Attribut "class" mit, aber dessen Wert wird clientseitig von JQuery geändert, und zwar abhängig davon, ob es sich um ein Blatt (ds_leaf) oder einen geöffneten (ds_open) bzw. geschlossenen Ast (ds_close) handelt. Die ID dagegen stammt vom Server. Idealerweise wird dabei der Primärschlüssel des angezeigten Elementes verwendet. Damit stellt man auch automatisch sicher, dass die ID eindeutig ist.
Hier zeigt sich auch, weshalb ich von einem statischen Baum spreche: Sämtliche Äste und Blätter werden bereits beim Laden der Seite geholt. Mit JavaScript wird dann im Browser geregelt, welche Information sichtbar oder ausgeblendet ist. Bei grösseren Bäumen mit Hunderten oder Tausenden von Ästen werden dagegen dynamische Verfahren angewandt, indem ein Klick auf einen geschlossenen Ast einen AJAX-Request auslöst, der die dazugehörigen Unterelemente auf dem Server nachlädt.
CSS
Den Div-Tags im oben gezeigten HTML werden mit JavaScript dynamisch Klassen zugewiesen. Es gibt drei Klassen:
- ds_close: Alle Äste, die geschlossen sind
- ds_open: Alle Äste, die geöffnet sind
- ds_leaf: Alle Blätter (Childs)
Auf diese Art und Weise muss im JavaScript-Code nur eine Klasse zugewiesen werden, für die Anzeigesteuerung, d.h. das Ein- und Ausblenden von Ästen, ist das CSS verantwortlich.
<style type="text/css">
/* Style für das Root-Element des Baumes */
div.ds_katgliederung div {
padding-left:16px;
}
div.ds_katgliederung div.ds_close {
cursor:pointer !important;
background: transparent
url(../js/jquerytreeview/folder-closed.gif)
no-repeat top left;
}
div.ds_katgliederung div.ds_open {
cursor:pointer !important;
background:transparent
url(../js/jquerytreeview/folder.gif)
no-repeat top left;
}
/* Alle Div-Tags innerhalb eines geschlossenen Astes
werden nicht angezeigt */
div.ds_katgliederung div.ds_close div {
display:none;
}
div.ds_katgliederung div.ds_leaf {
cursor:default;
background: transparent
url(../js/jquerytreeview/file.gif)
no-repeat top left;
}
</style>
JQuery
Nun fehlt uns nur noch der JavaScript-Code. Er besteht aus zwei Funktionen und einem Event-Handler. Als erstes der vollständige Code, dann die Erklärungen zu den einzelnen Teilen.
<script language="JavaScript">
<!--
//Diese Funktion wird beim Start der Seite
//und bei jedem Click zuerst aufgerufen
function ds_init() {
//setzt für alle Blätter bzw. Childs die Klasse ds_leaf
$('div.ds_katgliederung div').not('div:has(div)')
.attr("class", "ds_leaf");
//setzt alle Äste auf Klasse ds_close
//-> im Anfangszustand sind alle Äste geschlossen
$('div.ds_katgliederung div:has(div)')
.attr("class", "ds_close");
return true;
}
function ds_toggleNavigation(id) {
var tag = $("#" + id);
var tagclass = tag.attr("class");
ds_init();
tag.parents("div.ds_close").attr("class", "ds_open");
if (tagclass == "ds_close") {
tag.attr("class", "ds_open");
}
return true;
}
$(document).ready(function() {
//Meine ds_katgliederung
ds_init();
$('div.ds_katgliederung div')
.live('click', function(evt) {
var tag = $(this);
//alert(tag.attr("id"));
ds_toggleNavigation(tag.attr("id"));
evt.stopImmediatePropagation();
if (tag.children("div").length > 0) {
return false;
}
return true;
});
});
//-->
</script>
Die Idee ist, dass man bei jedem Klick zuerst einmal bestimmt, welche Div-Tags Äste und welche Blätter sind, die entsprechenden Klassen anhängt, und anschliessend alle Äste schliesst. Zuständig dafür ist die Funktion ds_init().
Dieses brachiale Vorgehen, alle DIV-Tags bei jedem Klick neu zu initialisieren, ist notwendig, weil es sich ja um einen dynamischen Baum handelt, der mit JavaScript und AJAX verändert werden kann. Da somit aus einem Blatt plötzlich ein Ast werden kann, muss man die Klassen bei jedem Klick neu bestimmen.
Die zweite Funktion namens ds_toggleNavigation öffnet und schliesst die Äste. Als Parameter übergibt man ihr die eindeutige id des Elementes, das geöffnet oder geschlossen werden soll. Diese Funktion ruft zuerst ds_init() auf, öffnet dann alle Elternelemente des übergebenen Tags und öffnet schliesslich noch das Element selbst, falls es geschlossen ist. Die Umkehrung, das Schliessen eines geöffneten Elementes, ist nicht nötig, das wird bereits in ds_init erledigt.
Nun benötigen wir noch den Event-Handler, der auf das OnClick-Ereignis reagiert. Dieser wird ebenso wie die erste Initialisierung innerhalb von (document).ready platziert. Beim Eventhandler gilt es zu beachten, dass live(..) verwendet wird, damit auch auf dynamisch zum Baum hinzugefügte Ereignisse reagiert werden kann. Für das vorliegende Beispiel ist dies gar nicht nötig, aber für die synchronisierbaren Bäume, auf die ich schon in der Einleitung hingewiesen habe.
ASP.NET
Bis hierher hat sich dieser Artikel kaum unterschieden von meinem früheren über synchronisierbare Bäume. Nun geht es allerdings um das Kernstück des vorliegenden Artikels, nämlich um das Vorgehen, um den Baum serverseitig zusammenzubauen. Im Gegensatz zu früheren Beispielen erfolgt dies nicht auf der HTML-Seite, sondern im Code dahinter, d.h. in der cs-Datei.
Die Aspx-Datei
Der aspx-Teil der Seite enthält deshalb ausser dem oben vorgestellten JavaScript und CSS nur wenige Elemente:
- eine SqlDataSource für die Verbindung zur Datenbank und das SQL-Statement
- ein DIV-Tag als Root für den ganzen Baum
- innerhalb des DIV-Tags eine PlaceHolder-Komponente, die später mit dem serverseitig zusammengesetzten Baum ersetzt wird.
<form id="form1" runat="server">
<div>
<asp:SqlDataSource ID="dsEmployees" runat="server"
ConnectionString="<%$ ConnectionStrings:conXeHr
%>"
ProviderName="<%$
ConnectionStrings:conXeHr.ProviderName %>"
SelectCommand="
SELECT
emp.employee_id
, LEVEL*1 AS ebene
, emp.LAST_NAME || ' ' ||
emp.first_name fullname
, emp.manager_id managerid
, emp.email
FROM
employees emp
START WITH emp.manager_id IS NULL
CONNECT BY PRIOR emp.employee_id =
emp.manager_id"
>
</asp:SqlDataSource>
<div class="ds_katgliederung"
style="border: 1px solid rgb(204, 204, 204);
padding: 10px;" >
<asp:PlaceHolder ID="TreePlaceHolder"
runat="server"></asp:PlaceHolder>
</div>
</div>
</form>
Innerhalb des SQL-Statements ist einerseits die letzte Zeile interessant, weil hier der Autojoin umgesetzt wird: Das Feld Manager_Id ist für einen Datensatz der Fremdschlüssel mit der Beziehung zur nächsthöheren Managementebene. Der Baum beginnt mit jenem Element, bei dem dieser Schlüssel leer ist, d.h. bei jener Person, die keinen Chef mehr hat. In dieser Form liefert das oracle-spezifische SQL-Statement alle Datensätze der Tabelle von oben nach unten.
Praktisch ist auch das oracle-spezifische Feld Level, es liefert die Ebene in der Hierarchie zurück, wobei die oberste Ebene mit 1 beginnt. Die Formulierung LEVEL*1 ist übrigens ein Trick, damit man als Datentyp einen Integer zurückerhält. Diese Information ist essentiell, damit später der Baum wieder aufgebaut werden kann.
Verständlicherweise sollte man diese Art von SQL nur bei Tabellen verwenden, die einige Dutzend Datensätze haben (im konkreten Fall sind es 107). Wer das gleiche mit einem Katalog versucht, an dem mehrere Zehntausend Artikel hängen, legt unter Umständen die Datenbank lahm.
MyPage.cs
Als nächstes macht es Sinn, eine allgemein verwendbare Funktion zu schreiben, die einen Connection-String und ein SQL-Statement als Parameter entgegennimmt und eine DataTable zurückgibt. Bei mir steht diese Funktion nicht im Code hinter der oben erwähnten Seite, sondern in einer Seite MyPage.cs mit allgemeinen Routinen, von der die erwähnte Seite erbt. Damit kann ich die Funktion von verschiedenen Seiten aus aufrufen. Konkret habe ich die folgenden zwei Klassendefinitionen:
public partial class TreeJQueryServerAddsControls : MyPage {
public class MyPage : System.Web.UI.Page {
Diese Vorgehensweise, via Vererbung eine eigene Klasse zwischen die eigentlichen Seiten einer Applikation und die Klassen des zugrundeliegenden Frameworks zu legen, hat sich übrigens nicht nur im Zusammenhang mit ASP.Net bewährt. Bei der Dynasoft AG wenden wir diese Vorgehensweise auch in Java-Applikationen mit Spring oder bei den ADF Business Components an.
Hier nun die Funktion für Oracle:
public System.Data.DataTable getDataTableFromOracle (
String parConn, String parSql) {
System.Data.DataTable result = null;
try {
System.Data.DataSet ds = null;
OracleDataAdapter da = null;
ds = new System.Data.DataSet();
OracleConnection conn =
new OracleConnection(parConn);
conn.Open();
da = new OracleDataAdapter(parSql, conn);
da.Fill(ds, "mytable");
result = ds.Tables["mytable"];
} catch (Exception exc) {
...
}
return result;
}
Code behind
Nach all diesen Vorbereitungen kommen wir nun zur eigentlichen CS-Seite (Code behind), welche den Baum zusammenbaut:
public partial class TreeJQueryServerAddsControls : MyPage
{
protected void Page_Load(object sender, EventArgs e){
DataTable dataTable = this.getDataTableFromOracle(
this.dsEmployees.ConnectionString,
dsEmployees.SelectCommand);
Int32 prev = 0;
Control parent = TreePlaceHolder;
if (dataTable != null && dataTable.Rows != null) {
foreach (System.Data.DataRow row in
dataTable.Rows) {
Int32 ebene =
Convert.ToInt32(row["ebene"]);
Int32 differenz = prev - ebene + 1;
while (differenz > 0) {
differenz--;
parent = parent.Parent;
}
HtmlGenericControl div =
new
HtmlGenericControl("DIV");
div.ID = "ds_" + row["id"];
div.Attributes.Add("class",
"filetree
treeview-famfamfam");
parent.Controls.Add(div);
LinkButton link = new LinkButton();
link.Text = row["fullname"].ToString();
div.Controls.Add(link);
parent = div;
prev =
Convert.ToInt32(row["ebene"]);
}
}
}
}
Beim Laden der Seite wird zuerst mit der Funktion aus MyPage.cs eine DataTable aus den Datenbankzeilen erstellt. Dann wird der Baum mit HtmlGenericControl aus DIV-Tags zusammengesetzt.
Das Kernstück der Routine ist aber der Umgang mit den Ebenen: Das Feld Ebene sagt uns, ob der aktuelle Datensatz in der Hierarchie unterhalb, auf der gleichen Ebene oder oberhalb des vorangehenden Datensatzes steht. Der erste Fall ist einfach: Befindet sich der Datensatz unterhalb, dann wird er an das Element parent angehängt. Für die anderen zwei Fälle kommt die While-Schleife und die Zeile darüber zum Zuge: Zuerst wird mit "prev - ebene + 1" berechnet, um wie viele Ebenen man den Parent nach oben verschieben muss. Und in der While-Schleife wird dies dann gemacht.
Innerhalb des DIV-Tags kann man weitere Komponenten platzieren. Im vorliegenden Beispiel ist dies ein LinkButton. Allerdings wird das Auf- und Zuklappen nicht über diesen Link ausgelöst, sondern durch einen Klick auf das den Link umhüllende DIV-Tag. Insofern könnte hier auch eine ganz andere Komponente vorkommen.
Beispiel und Download
Dieser Link führt zu einer funktionsfähigen Demonstrationsseite. Allerdings habe ich auf diesem Website keinen Zugang zu einer Oracle-Datenbank, deshalb wird stattdessen die Mockversion in Access verwendet, die sich auch Download unten findet. Ausserdem habe ich auf die Seite MyPage.cs verzichtet und die Funktion getDataTable direkt in den Code der Seite integriert.
Die ASPX-Seite, die Access-DB, die Javascript-Dateien und der in web.config einzufügenden Connection-String finden Sie in der gezippten Datei, die Sie hier herunterladen können. Die Dateien kommen in folgende Verzeichnisse:
- Access-DB tree.mdb: Verzeichnis App_Data
- Connection-String: in web.config in den Abschnitt connectionStrings kopieren
- JavaScript-Dateien: Verzeichnis js, Unterverzeichnis jquerytreeview übernehmen
- ASPX- und CS-Datei TreeJQueryServerAddsControls: irgendein Unterverzeichnis unterhalb von Root, z.B. webdesign
Damit das Beispiel funktioniert, muss der Webserver IIS .. in Pfadangaben zulassen. Manchmal wird dies aus Sicherheitsgründen unterbunden. Auf 64-Bit Systemen muss die Applikation im IIS ausserdem für 32-Bit konfiguriert werden, damit der Zugriff auf die Access-Datenbank klappt. Wie man das macht, habe ich in meinem Blog bereits beschrieben.
Und wer sich mit dem Autojoin in einer Oracle-Datenbank versuchen möchte (es soll ja Masochisten geben ;-), der findet bei Oracle die SQL-Scripts für das HR-Schema und unter diesem Link noch Installationsanweisungen dazu; kurz, prägnant und benutzerfreundlich, wie bei Oracle üblich.
Diese Webseite wurde am 12.01.02 um 10:02 von rothen ecotronics, Bern, erstellt oder überarbeitet. Falls Sie für Ihre eigenen Webseiten Unterstützung benötigen, finden Sie diese unter dem folgenden Link: rothen ecotronics
Wenn Sie uns ein EMail senden wollen, klicken Sie hier!