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

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.

Zurück zum Inhaltsverzeichnis


Vorbereitung

Bevor wir mit dem Erstellen der Seite beginnen können, müssen ein paar Vorbedingungen erfüllt sein:

Zurück zum Inhaltsverzeichnis


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.

Die Hierarchie eines Unternehmens als Baumdarstellung

Zurück zum Inhaltsverzeichnis


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.

Zurück zum Inhaltsverzeichnis


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.

Zurück zum Inhaltsverzeichnis


CSS

Den Div-Tags im oben gezeigten HTML werden mit JavaScript dynamisch Klassen zugewiesen. Es gibt drei Klassen:

  1. ds_close: Alle Äste, die geschlossen sind
  2. ds_open: Alle Äste, die geöffnet sind
  3. 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>

Zurück zum Inhaltsverzeichnis


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.

Zurück zum Inhaltsverzeichnis


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:

<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.

Zurück zum Inhaltsverzeichnis


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:

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.

Zurück zum Inhaltsverzeichnis



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!

Zurück zum Kleiner-&-Rothen-Website