Inside ASP.NET Databinding - Teil 1
Alexander Jung
Die Bindung eines ASP.NET-DataGrids an eine Datenquelle (Databinding, Datenbindung) ist sehr leistungsfähig, insbesondere im Zusammenhang mit DataSets. Allerdings ist es auf den ersten Blick alles andere als offensichtlich, wie die Komponente mit der Datenquelle zusammenarbeitet oder wie der Entwickler eingreifen kann. Dieser Artikel untersucht die grundlegende Arbeitsweise des Databinding und stellt die Eingriffsmöglichkeiten vor.
Teil 2: ASP.NET-Datenbindung für eigene Objekte
Die Theorie: Die Datenbindung (Databinding) in ASP.NET ist eine feine Sache. DataGrid anlegen, DataSource auf ein DataSet setzen, fertig. Liegen die Daten in einem XML-Dokument vor, dann liest man sie vorher in ein DataSet ein. Selbst eine Objektstruktur kann man in XML serialisieren und diese Daten wiederum in ein DataSet packen.
Die Praxis: Die Datenbindung in ASP.NET ist immer noch eine feine Sache. Aber wie arbeitet Databinding nun eigentlich? Was kann ein ASP.NET-DataGrid noch verarbeiten außer DataSets? Und wie kann der Entwickler eingreifen wenn ein DataSet mal nicht die adäquate Lösung ist?
Teil 1 dieses Artikels geht diesen Fragen nach.
Teil 2 setzt dann exemplarisch ein eigenes Databinding für XML ohne den Umweg über
DataSets um.
Sehen wir zunächst, was ein DataGrid (System.Web.UI.WebControls.DataGrid) kann und danach, wie das ganze funktioniert.
Die Datenbindung eines ASP.NET-DataGrids bietet folgende Features:
BoundColumn und TemplateColumn sind sicher sehr mächtige Ansätze um ein DataGrid nach eigenen Anforderungen anzupassen, für die folgenden Betrachtungen sind sie jedoch nachrangig. Vielmehr soll es im Folgenden darum gehen, was sich hinter dem "simplen" Setzen des DataSource Properties verbirgt und wie die automatische Erzeugung vonstatten geht.
Über die Online-Hilfe und die diversen verfügbaren Beispiele findet man sehr schnell heraus, dass diese Dinge sehr gut mit DataSets funktionieren, ebenso für flache Listen (z.B. Arrays), sowohl solche mit Datentypen wie int oder string als auch solche mit komplexeren Objekten. In letzterem Fall werden dann die Properties der Objekte an die Spalten angebunden.
Gegeben ist folgender Codeausschnitt aus einer ASP.NET-Seite:
DataGrid1.DataSource= XYZ; DataGrid1.DataBind();
Laut Online-Hilfe (beim Property DataSource des DataGrids) muß XYZ das Interface IEnumerable (System.Collections.IEnumerable) implementieren, was auf nahezu alle Container (Arrays, Collections, Hashtables, etc.) zutrifft. Für die erste Betrachtung nehmen wir ein einfaches Array von anbindbaren Typen. Anbindbar bedeutet nichts anderes, als dass die Methode IsBindableType() der Klasse BaseDataList (System.Web.UI.WebControls.BaseDataList) - der Basisklasse des DataGrids - true liefert, was neben den üblichen primitiven Datentypen (int, byte, etc.) auch bei String, DateTime und Decimal der Fall ist.
Aber Vorsicht: Nicht anbindbar im Sinne dieser Methode bedeutet nicht, daß das DataGrid mit diesem Objekt nichts anfangen kann. Es kann nur eben nicht direkt in eine Zelle eines DataGrids eingetragen werden. Nur das wird durch IsBindableType() geprüft.
object[] ol= new object[]{ 'c', "text", null, 4711, DateTime.Now,
new Customer("AM-4711", "Anton", "Meier") };
DataGrid1.DataSource= ol;
Customer ist eine recht triviale Klasse, die uns als Beispiel in unterschiedlichen Formen begleiten wird. Für den Moment können wir annehmen, dass sie aus nichts weiter als einem passenden Konstruktor und der Methode ToString() besteht:
public class Customer
{
private string id;
private string firstName;
private string lastName;
public Customer(string id, string firstName, string lastName)
{
this.id= id;
this.firstName= firstName;
this.lastName= lastName;
}
public string ID
{
get { return this.id; }
}
public string FirstName
{
get { return this.firstName; }
set { this.firstName = value; }
}
public string LastName
{
get { return this.lastName; }
set { this.lastName = value; }
}
public override string ToString()
{
return id;
}
}
Und hier ist das Ergebnis. Das DataGrid wurde ohne besondere Formatierung erstellt und in größer-als- und kleiner-als-Zeichen eingefaßt (der Grund dafür wird deutlich, wenn wir uns leere Listen anschauen):

Abb. 1: Databinding an ein einfaches Array
Die über den Enumerator des Arrays gelieferten Listeneinträge stellen beim Befüllen die eigentlichen Daten bereit. Im vorliegenden Fall wird einfach die Methode ToString() des aktuellen Eintrags aufgerufen.
Bevor das DataGrid mit den Datenzeilen befüllt wird muss die Spalte allerdings erst einmal erzeugt werden. Auch diese Information stammt nicht etwa vom Container, im Beispiel also dem Array, sondern wird über das erste Element des Containers gewonnen. Bei anbindbaren Typen wird das immer eine Spalte vom Typ BoundColumn (System.Web.UI.WebControls.BoundColumn) mit dem Titel "Item" sein.
Die Entscheidung über die zu erzeugenden Spalten findet anhand des ersten Elementes im Container statt. Das hat durchaus Konsequenzen. Bei einem leeren Container etwa erhält man nichts. Deutlicher gesagt, es fehlen nicht nur die Datenzeilen, es gibt noch nicht einmal die Spaltenüberschriften. Die HTML-Ausgabe des DataGrids ist tatsächlich leer:
object[] ol= new object[]{};
DataGrid1.DataSource= ol;

Abb. 2: Databinding an ein leeres Array
Aber es gibt noch mehr Probleme. Schauen wir noch mal kurz auf das Array im ersten Beispiel. Es enthält einen null-Wert sowie eine Instanz von Customer. Solange diese Elemente irgendwo in der Liste stehen ist die Ausgabe eine leere Zeile bzw. das Ergebnis des ToString()-Aufrufs.
Erscheint der null-Wert jedoch als erstes Element, so ist das Ergebnis eine Exception - genauer eine System.Web.HttpException - weil für die Spaltenerzeugung benötigte Informationen nicht vorhanden sind. Falls das Customer-Objekt am Anfang der Liste steht kommt es ebenfalls zu einer Exception, jedoch ist dafür gar nicht das Customer-Objekt selbst, sondern eines der folgenden Listenelemente verantwortlich.
Dieser Fall führt uns aber bereits zum nächsten Abschnitt - vielleicht ist Ihnen aufgefallen, dass IsBindableType() für Customer false liefert -, dort wird auch die eigentliche Fehlerursache deutlich werden. Tatsächlich wurde das Customer-Objekt hier nur aufgeführt, um den Aufruf der Methode ToString() zu demonstrieren.
Der nächste Schritt ist die Anbindung an einen Container mit komplexeren Objekten, solche für die IsBindableType() false liefert. Für die Anbindung nehmen wir nun ein Array von Customer-Objekten:
object[] ol= new object[]{
new Customer("AM-4711", "Anton", "Meier"),
null,
new Customer("WM-0815", "Willhelm", "Müller")};
DataGrid1.DataSource= ol;
Führt man im DataGrid ein Databinding an diesen Container durch, dann erhält man eine Tabelle mit den Inhalten der öffentlichen (public) Properties der Objekte in den Spalten, als Spaltenüberschrift dient der Name des Properties. Es ist leicht nachvollziehbar, wie sich das DataGrid diese Informationen über Reflection besorgt (eine Einführung zum Thema Reflection findet sich z.B. im .NET Readiness Kit, http://www.microsoft.com/GERMANY/ms/msdnbiblio/dotnetrk/themen.htm).
Insgesamt folgt das DataGrid diesem Kochrezept:
Und hier ist das Ergebnis:

Abb. 3: Databinding an ein Array von Objekten
Während im vorigen Abschnitt die Listenelemente selbst anbindbar sein mussten gilt hier die Forderung, dass dies für die Properties der Listenelemente gelten muss. Solche Properties, auf die das nicht zutrifft, werden ignoriert. Und da wiederum das erste Element die benötigten Informationen liefert, hat man entsprechende Konsequenzen im Bezug auf leere Container oder Objekte unterschiedlichen Typs im Container.
Hier kommt auch endlich die noch ausstehende Erklärung für die Exception im vorangegangen Teil: Das DataGrid verlangt von allen Elementen, dass sie die Properties bereitstellen, die es beim ersten Element gefunden hat. Im obigen Beispiel war das jedoch nicht gegeben. Die Folge war eine System.Reflection.TargetException beim Auslesen der nicht vorhandenen Properties.
Insbesondere sollte man dies beachten, wenn man unterschiedliche Ableitungen einer gemeinsamen Basisklasse anbinden will. Die Bildung von Klassenhierarchien ist ein zentrales Konzept der Objektorientierung, man benötigt nicht sehr viel Phantasie um sich zu Customer als Basisklasse eine Ableitung RegularCustomer oder BusinessCustomer vorzustellen. Sobald diese Klassen eigene Properties mitbringen und in einer Liste gemischt auftreten können, kann diese Liste nicht mehr problemlos angebunden werden.
Die Art und Weise der Ausgabe der Objekte als Liste ist zwar genau das, was zu erwarten war, allerdings ist die Form nicht unbedingt ansprechend. Das Problem dabei ist, dass die eigentlichen Properties zwangsweise der C#-Syntax folgen. Eine Spaltenüberschrift "first Name" ist aber sicher anwendergerechter, als FirstName.
Natürlich ließe sich das durch explizite Angabe der Spaltenüberschriften im DataGrid erreichen, aber dann müssten wir den Weg der automatischen Spaltenerzeugung verlassen.
An dieser Stelle gibt es also bereits Grund den ersten Eingriff vornehmen. Die Frage ist nur wie. Nun, das DataGrid liest nicht einfach die Properties aus, es fragt vorher höflich beim Customer-Objekt nach, ob man ihm die Properties nicht freiwillig geben möchte. Dies kann man tun, indem man ICustomTypeDescriptor (System.ComponentModel.ICustomTypeDescriptor) implementiert und die nach eigenen Ansprüchen aufgebauten Properties bereitstellt.
Nahezu alle Methoden von ICustomTypeDescriptor können auf triviale Art und Weise implementiert werden, einfach indem man die Aufrufe an TypeDescriptor (System.ComponentModel.TypeDescriptor) delegiert - eine Hilfsklasse in der .NET Framework Class Library (FCL) zum Auslesen von Typinformationen per Reflection.
Lediglich die Methode GetProperties() ist für unser Problem von Interesse. Eine Beschreibung eines Properties ist vom Typ PropertyDescriptor (System.ComponentModel.PropertyDescriptor), für die Liste gibt es einen speziellen Container PropertyDescriptorCollection, den GetProperties() als Ergebnis liefern muss. Für eigene "Pseudo-Properties" leitet man eine Klasse von PropertyDescriptor ab - im Beispiel CustomerPropertyDescriptor - und implementiert sie entsprechend.
Der Name des Properties wird bereits durch die Klasse PropertyDescriptor verwaltet und muss dieser nur im Konstruktor übergeben werden. Da dies der für die Spaltenüberschriften verwendete Bezeichner ist, übergibt GetProperties() hier die Bezeichnungen im Klartext (also "First Name" statt "FirstName") - für eine ansprechendere Titelzeile sollte damit gesorgt sein.
Hier sind die bisher angefallen relevanten Ergänzungen:
class CustomerPropertyDescriptor: PropertyDescriptor
{
public CustomerPropertyDescriptor(string Name )
: base(Name, null)
{
}
[...]
}
public class Customer : ICustomTypeDescriptor
{
[...]
public PropertyDescriptorCollection GetProperties()
{
PropertyDescriptor[] props= new CustomerPropertyDescriptor[3];
props[0]= new CustomerPropertyDescriptor("Identifier");
props[1]= new CustomerPropertyDescriptor("First Name");
props[2]= new CustomerPropertyDescriptor("Last Name");
PropertyDescriptorCollection pdc=
new PropertyDescriptorCollection(props);
return pdc;
}
[...]
}
Das ist bereits ausreichend um die Spalten zu generieren. Damit ist das DataGrid aber noch nicht gefüllt. Da es keine echten Properties dieser Namen gibt, muss CustomerPropertyDescriptor während des Füllens des DataGrids auch die passenden Daten liefern. Dies passiert, indem die Methode GetValue() überschrieben wird. In diesem einfachen Beispiel wird lediglich anhand des Namens entschieden, welches echte Property aufzurufen ist. Die Prüfung auf null ist wegen des null-Wertes im Array notwendig (dieses Thema wird uns später noch beschäftigen).
class CustomerPropertyDescriptor: PropertyDescriptor
{
[...]
public override object GetValue(object component)
{
if (component==null)
return null;
switch (Name) // Name ist Property von PropertyDescriptor
{
case "Identifier": return ((Customer)component).ID;
case "First Name": return ((Customer)component).FirstName;
case "Last Name": return ((Customer)component).LastName;
}
return "";
}
[...]
}
Das Ergebnis ist wie erwartet:

Abb. 4: Databinding an ein Array von Objekten mit eigener Infrastruktur
Natürlich kann man noch weitergehend eingreifen und muss sich nicht auf einfaches "Umbenennen" von Properties beschränken. Man kann sich z.B. die Liste der Properties per Reflection holen (mit Hilfe von TypeDescriptor) und dort gezielt einzelne Properties unterdrücken, bzw. deren Anzeige von einem eigenen Attribut oder den Rechten des Anwenders abhängig machen.
Genauso denkbar sind berechnete künstliche Properties, die als solche in der Klasse selbst nicht vorhanden sind, z.B. kumulierte Werte oder eine laufende Nummer.
Gerade der triviale Fall des Umbenennens lässt sich sehr einfach zu einer wiederverwendbaren Klasse ausbauen (indem man den Property-Namen als string mitgibt und es per Reflection ausliest). Niemand verlangt schließlich, dass alle PropertyDescriptor-Objekte - abgesehen von der Basisklasse - vom gleichen Typ sind. Auf diese Weise lassen sich auch kontextabhängige Dinge, Berechtigungen des Anwenders oder anderes abdecken.
Einige dieser Beispiele sind so allgemeiner Natur, dass man hier mit wiederverwendbaren PropertyDescriptor-Klassen arbeiten kann (z.B. das Umbenennen eines echten Properties). Sobald man diesen Ansatz aber exzessiver ausnutzt läuft man in die Falle, dass Wissen um die Zusammenhänge, also Geschäftslogik, sowohl in der eigentlichen Klasse Customer, als auch in den einzelnen PropertyDescriptor-Klassen verteilt ist. Hier wird dann ein weitergehendes objektorientiertes Design notwendig, um dieses Wissen in der Klasse Customer vorzuhalten - wo es hingehört - und aus den PropertyDescriptor-Klassen lediglich darauf zurückzugreifen.
Wir haben erreicht, dass das Databinding auf Ebene der Datenklassen - und nicht etwa des DataGrids - den eigenen Bedürfnissen angepasst werden kann. Das kommt auf jeden Fall einer strikteren Trennung zwischen Oberfläche und Geschäftslogik entgegen.
Einer wasserdichten Lösung stehen jedoch die zwei bereits angesprochenen Probleme im Weg: unterschiedliche Typen und leere Listen.
Sicherzustellen, dass Container mit Objekten unterschiedlichen Typs sauber arbeiten, liegt in der Verantwortung des Entwicklers. Aber welche Möglichkeit gibt es, eine leere Liste auch als solche mit korrekten Überschriften darzustellen und in diesen Fällen das DataGrid nicht einfach verschwinden zu lassen? Das erste Element steht nicht zur Verfügung, und wenn wir wegen eines Sonderfalls auf das DataGrid zurückgreifen zu müssten hätten wir uns die bisherigen Verrenkungen auch gleich sparen können. Ergo muss der Container selbst diese Informationen bereitstellen und das DataGrid muss diese Informationen nutzen.
Glücklicherweise ist die dafür notwendige Infrastruktur in der FCL nicht vergessen worden. Bevor das DataGrid bei der Erzeugung der Spalten beim ersten Element nachfragt, stellt es die gleiche Frage an den Container. Dieser hat die Möglichkeit ITypedList (System.ComponentModel.ITypedList) zu implementieren - dieses Interface existiert alleine zu diesem Zweck - und in der Methode GetItemProperties() die gleichen Ergebnisse zu liefern, die auch die Klasse Zeilen-Objekte in der Methode ICustomTypeDescriptor.GetProperties() bereitstellt.
Im Beispiel war es lediglich notwendig, die Implementierung der oben beschriebenen Methode Customer.GetProperties() in eine statische Methode zu verschieben und aus beiden Interface-Methoden - ICustomTypeDescriptor.GetProperties() in Customer und ITypedList.GetItemProperties() im Container - aufzurufen. Die möglicherweise gravierendere Konsequenz ist, dass man nun eine eigene Klasse für den Container (zur Implementierung des Interfaces) benötigt, wo vorher ein einfaches Array ausreichend war. Hier ist der Code der Container-Klasse:
public class CustomersList : IEnumerable, ITypedList
{
ArrayList m_alData= new ArrayList();
public ArrayList Data
{
get { return m_alData; }
}
// Enumerator bereitstellen
System.Collections.IEnumerator IEnumerable.GetEnumerator()
{
return m_alData.GetEnumerator();
}
// Properties bereitstellen
System.ComponentModel.PropertyDescriptorCollection
ITypedList.GetItemProperties(
System.ComponentModel.PropertyDescriptor[] listAccessors)
{
return Customer.GetPropertiesImpl();
}
string ITypedList.GetListName(
System.ComponentModel.PropertyDescriptor[] listAccessors)
{
return null;
}
}
Und hier die verbesserte Ausgabe:

Abb. 5: Databinding an einen leeren Container der ITypedList implementiert
Ein kleines Loch gibt es aber noch, dass sich leider auch nicht schließen lässt: null-Werte. Solange sie irgendwo in der Liste stehen tun sie nicht weh, aber als erstes Element verursachen sie nach wie vor eine Exception. Grund dafür ist, dass die Spalten des DataGrids sich beim Füllen den PropertyDescriptor des Properties holen wollen. (Die von ITypedList gelieferten Informationen werden hier nicht wiederverwendet.)
Einmal gefunden wird der PropertyDescriptor gesichert, so dass das Problem bei nachfolgenden Zeilen nicht mehr auftritt. Um null-Werte für diese Fälle abzudecken muss lediglich GetValue() mit einem null-Wert im übergebenen Argument zurechtkommen, was ja im obigen Beispiel geschehen ist. Im Nachhinein betrachtet keine so gute Idee, denn die erhaltene Sicherheit ist trügerisch. Vielmehr sollte man streng darauf achten, null-Werte zu vermeiden und ggf. durch eine spezielle Instanz seiner Klasse abzudecken.
Beim DataSet (System.Data.DataSet) kommt nun zum bisher Gesagten noch etwas mehr hinzu. Das DataSet implementiert IEnumerable nicht. Trotzdem kann man es anbinden, was eigentlich der Online-Hilfe beim Property DataSource widerspricht. Ein Interface, das von IEnumerable ableitet, ist IList (System.Collections.IList). Für dieses existiert eine Indirektion, das Interface IListSource (System.ComponentModel.IListSource), dessen einzige Methode ein IList zurückliefert. Und genau an dieser Stelle kommen DataGrid und DataSet, bzw. auch DataTable (System.Data.DataTable), zusammen, denn IListSource wird von beiden implementiert.
Über diesen Mechanismus erhält das DataGrid eine Referenz auf ein DataView (System.Data.DataView). DataView ist eine der Klassen, die ITypedList implementieren, auf diesem Weg erhält das DataGrid also seine Spalteninformationen. Die einzelnen Datenzeilen werden über DataRowView-Objekte (System.Data.DataRowView) angesprochen, die u.a. die Aufgabe übernehmen, ICustomTypeDescriptor zu implementieren.
Ergo: Trotz alle Komplexität eines DataGrids und der diversen Klassen in diesem Umfeld wird von der FCL zur Anbindung keinerlei Infrastruktur verwendet, die nicht auch vollständig dem Entwickler zur Verfügung stünde.
Bevor das DataGrid mit den Datenzeilen befüllt wird, müssen die Spalten erzeugt werden. Hierzu gibt es verschiedene Varianten, die in folgender Reihenfolge abgeprüft werden:
Wenn alle diese Punkte fehlschlagen erhält man eine Exception.
Das Befüllen der Zeilen des DataGrids erfolgt über einen Enumerator, der die eigentlichen Daten bereitstellt. Auch hier kommt wieder die gerade angesprochene Liste der Varianten zum Zug, wobei aber der erste Punkt entfällt und das Auslesen nicht mehr nur auf den ersten Eintrag beschränkt ist:
Wow. Zwei Zeilen Code zur Anbindung und dann das! Die erste gute Nachricht ist, dass Sie das alles gar nicht wissen müssen, weil die typischen Container der FCL das alles schon vorbereitet haben. Die zweite gute Nachricht ist, dass Sie an vielen Stellen eingreifen können, wenn der Bedarf dazu da ist. Aber die Mächtigkeit gepaart mit der Neuheit des .NET Frameworks hat auch zur Folge, dass ein großer Teil an Erfahrung und Wissen um die Zusammenhänge noch gar nicht vorhanden sein kann. Die Online-Hilfe ist in erster Linie eine Referenz, diese Aufgabe kann sie nicht erfüllen.
Wollen Sie die Darstellung eines DataGrids an eigene Bedürfnisse anpassen, so entsteht oft der Eindruck, das sei nur auf Ebene des DataGrids möglich. Die hier vorgestellten Mechanismen zeigen, dass Sie sehr wohl die Möglichkeit haben, Anpassungen auf Ebene der Datenquelle vorzubereiten bzw. eigene Datenstrukturen ähnlich komfortabel und direkt anzubinden, wie ein DataSet.
Alexander Jung ist Software-Architekt und arbeitet als Managing Developer bei NEW LINE Software Development GmbH. Weitere Informationen finden sich unter http://www.alexander-jung.net/.
Kontakt: info alexander-jung.net |
top | Letzte Änderung: 27.08.2005 23:31:17 |