Alexander-Jung.NET
 Home  Veröffentlichungen Downloads Historie Site Site-Map
view my resources at 123aspx.com Directory

Databinding (for XML)

Übersicht

Das DataBinding in ASP.NET ist eine feine Sache. DataGrid anlegen, DataSource auf ein DataSet setzen, fertig. . Hat man ein XML-Dokument, dann packt man es eben vorher in ein DataSet.

Nur gibt es leider XML-Strukturen, die ein DataSet überfordern, z.B. wenn es um rekursive Strukturen geht. Natürlich könnte man solche XML-Dokumente anders strukturieren - aber was ist, wenn man eine existierende XML-Struktur anbinden muß? (Oder einfach nur stur ist :-) ?)

Dann fängt man an im Internet zu suchen und wird auch viele Beispiele zum Thema DataBinding und XML finden - leider alle nach dem Strickmuster "XML in DataSet, DataSet anbinden".

Und dann stürzt man sich in die Untiefen der .NET Framework Dokumentation . . Das folgende ist das Ergebnis dessen, was ich dabei herausgefunden habe - nicht immer auf die einfache Tour.

DataBinding

DataBinding eines ASP.NET-DataGrids bietet verschiedene Features:

Das funktioniert gut mit DataSets, ebenso für flache Listen mit Objekten, wobei dann die Properties der Objekte an die Spalten angebunden werden. Sobald aber komplexe Objektstrukturen, Anbindung von Nicht-Properties oder gar XML ins Spiel kommt, geht das nicht mehr so simpel.

Beginnen wir am Anfang...

Arbeitsweise von Databinding

Gegeben ist folgender Codeausschnitt aus einer ASP.NET-Seite:

grid1.DataSource= XYZ;
grid1.DataBind();
XYZ muß ein Interface basierend auf IEnumerable implementieren. Aus der .NET Framework Dokumentation heraus stößt man an den verschiedenen Stellen noch auf folgende Interfaces:

Die Listeneinträge liefern dann neben den eigentlichen Daten auch die Informationen über die zu erzeugenden Spalten:

Das DataGrid ruft diese Informationen mehrfach ab, während es sich aufbaut. Und über die von diesen beiden Varianten gelieferten Informationen holt es sich auch noch die Daten (wie das passiert wird weiter unten erklärt).

Binding an eine Objektliste

Gegeben ist eine Collection-Klasse (Array, CollectionBase, .) die die Objektrefrenzen hält. Binding an eine solche Objektliste ist der einfachste Fall von Binding.

Führt man ein Databinding an diese Collection durch, dann erhält man ein Grid mit den Inhalten der Properties in den Spalten, als Spaltenüberschrift dient der Property-Name. Es dürfte klar sein, wie sich das Grid diese Informationen über Reflection besorgt.

Zum Füllen geht das Grid also folgendermaßen vor:

Ein Objekt (die Collection-Klasse), das Einfluß auf diese Erzeugung nehmen will (z.B. andere Namen in den Spaltenüberschriften verwenden) kann dies tun, indem es ICustomTypeDescriptor implementiert.

ICustomTypeDescriptor.GetProperties() liefert eine PropertyDescriptorCollection zurück, die wesentliche Information der enthaltenen PropertyDescriptor-Objekte ist der Name des "Properties". Außerdem muß diese PropertyDescriptor-Klasse GetValue() so überschreiben, daß sie zu einem späteren Zeitpunkt zur übergebenen Referenz (ein Eintrag in der Liste für eine Zeile) den Wert ihres eigenen "Properties" zurückliefert.

Die angepaßte Variante des Füllens sieht also für das Grid folgendermaßen aus:


Ein einfaches Beispiel in  (das übrigens aus der .NET Framework Dokumentation stammt).

Binding an ein Dataset

Binding an ein Dataset setzt auf den gerade beschriebenen Mechanismus auf, insofern ist es nicht der einfachste Fall, kann aber auch relativ schnell behandelt werden:

Ein DataView (bzw. DataTable) implementiert IList/IListSource; die einzelnen Elemente sind vom Typ DataRowView. Da DataRowView selbst keine Properties bereitstellen kann (zumindest keine mit den gerade aus der Datenbank gelesenen Daten), implementiert es ICustomTypeDescriptor und liefert dynamisch die Feld-Informationen seines DataViews/DataTables zurück.


Ein einfaches Beispiel für Databinding an ein Dataset.

Binding an XML

Für XML ist keine dem DataView/DataRowView vergleichbare Infrastruktur aufgebaut. Führt man hier ein Databinding durch, z.B. an ein XmlNodeList (selectNodes), dann erhält man - nach dem oben vorgestelllten Schema F - die Properties der einzelnen XmlNodes (HasChildNodes, InnerText, etc.) - kaum das, was man erreichen will. Wer sich selbst ein Bild machen will kann das hier tun:


Binding an ein XML-Dokument

Ergo muß das, was für DataSets durch die FCL bereitgestellt wird, nach den Anforderungen die man an ein XML-DataBinding stellt selbst implementiert werden.

Eigenes Databinding

Zielstellung

Fangen wir mit dem an, was wir erreichen wollen (immer eine gute Herangehensweise, wenn man wiederverwendbaren Code schreiben will).

Ich habe ein XML Dokument verwendet, in dem ich eine Menüstruktur verwalte (test.xml). Es besteht aus Gruppen und Einträgen mit Name, Link und Beschreibung. Die Gruppen selbst haben ebenfalls Namen und können ineinandergeschachtelt werden,bilden also eine rekursive Struktur. (Das mag für die hier beschriebene Darstellung als flaches Grid nicht sehr sinnvoll erscheinen. Ich habe ganz einfach ein XML Dokument ausgesucht, das von einem DataSet nicht direkt verarbeiten werden kann). Nun wäre es schön, wenn der folgende Code einfach das tun würde, was man erwartet:

private void Test()
{
	XmlDocument xml= new XmlDocument();
	xml.Load(Server.MapPath("test.xml"));
	XmlDataView dv= new XmlDataView(xml, "//Entry"); 

	// doc, query
	//column auto creation: available properties: // dv.AutoAddElements=
        true; // !!! data.driven, not schema-driven // dv.AutoAddAttribs= true; // !!! data.driven,
        not schema-driven // dv.AutoAddSelf= true; 

	//explicitely adding columns: //Syntax: dv.AddColumn(Name,
        xpath); 
	dv.AddColumn("Name"); 
	dv.AddColumn("Group ", "../../@Name"); 
	dv.AddColumn("Group", "../@Name"); 
	dv.AddColumn("Description", "Description"); 
	//dv.AddColumn("ID", "@ID"); 

	grid.DataSource= dv;
} 

Natürlich ist der Code nicht übersetzbar, denn eine Klasse XmlDataView (der Name wurde in Analogie zu DataView gewählt) existiert nicht.

Umsetzung

Wie gesagt, das war die Zielstellung. Was ist notwendig um das zu erreichen?

Die Aufgaben der Klassen im einzelnen:

Implementiert ist das Ganze in XmlDataBinding.cs. Damit ist es möglich eine Sicht auf ein beliebiges XML mittels beliebigem XPath zu erzeugen. Für Spalten können ebenfalls beliebige XPath-Statements verwendet werden, bzw. können Spalten auch automatisch durch die View erzeugt werden (basierend auf den Daten beim ersten Abfragen der Informationen).

Verhalten zur Laufzeit

XmlDataView hat zwei Arbeitsmethoden die bei Bedarf aufgerufen werden: Query() und BuildProps().

Query() führt per SelectNodes() die XPath-Abfrage gegen das XML-Dokument durch. Die XmlNodeList mit dem Ergebnis wird nicht direkt gespeichert, vielmehr wird für jedes XmlNode ein XmlDataRowView (analog DataRowView) erzeugt, dieses erhält eine Referenz auf den XmlNode, danach wird es in einem Array abgelegt. Diese Liste steht für die Zeilen im Grid.

protected void Query()
{
	if (m_alData!=null)
		return ; 
	
	XmlNodeList nl= m_xml.SelectNodes(m_sQuery); 
	
	// translate XMLNode to XmlDataRowView (holds reference
        of XmlNode) m_alData= new ArrayList(nl.Count);
	for( int i= 0; i<nl.Count; ++i)
	{
		XmlNode node= nl.Item(i);
		m_alData.Add( new XmlDataRowView(this , node));
	}
} 

XmlDataRowView verwaltet eine Zeile für das Grid und implementiert folglich ICustomTypeDescriptor. ICustomTypeDescriptor bringt eine Menge Methoden mit, interessant ist aber lediglich GetProperties(). Um die Daten nicht redundant halten zu müssen baut nicht jede Zeile ihre eigene PropertyDescriptorCollection auf, vielmehr ruft die Methode eine gleichnamige Methode in XmlDataView auf. Das ist der Zeitpunkt, an dem das XmlDataView dynamisch die Spalten generiert, was in der schon erwähnten Methode BuildProps() passiert.

Für diesen technischen Prototypen wurde eine einfache Variante zur Ermittlung der Spalten gewählt. Es werden einfach alle Elemente bzw. Attribute (gesteuert über die AutoAddXY-Properties) des ersten Knotens der Ergebnisliste verwendet. Eine ausgereiftere Variante würde hier vielleicht das Schema auswerten.

BuildProps() holt es sich also den ersten Eintrag aus der Liste der XmlDataRowViews. Sie sammelt die Elemente und Attribute des zugehörigen XML-Knotens und legt für jedes eine Spalte an, wobei es die Mathode AddColumn() in Anspruch nimmt.

AddColumn() legt für die Spalte ein XmlDataViewCol-Objekt mit Bezeichnung und relativem XPath zum Auslesen - bei den automatisch generierten Elementen einfach der Bezeichner, bei Attributen mit vorangestelltem Klammeraffen - an. Der Bezeichner wird zur Spaltenüberschrift werden, das XPath wird später den Weg vom Zeilen-Knoten zum anzuzeigenden Inhalt weisen.

AddColumn() kann übrigens auch mit beliebigen XPath-Statements manuell aufgerufen werden.

BuildProps() hat noch eine letzte Aufgabe: GetProperties() muß eine PropertyDescriptorCollection zurückliefern, wir haben aber nur ein Array von Objekten, die von PropertyDescriptor abgeleitet sind. Daher wird die Liste kurzerhand kopiert und das Array zur späteren Verwendung gesichert.

protected void BuildProps()
{
	if (m_props!=null)
		return;
	
	Query();
	if (m_alData.Count==0) //
        we act datadriven !!!
		return;

	XmlDataRowView row = (XmlDataRowView)m_alData[0];
	XmlNode node= row.Node;
	if (AutoAddSelf)
	AddColumn(node.Name, "");

	if (node is XmlElement)
	{
		if (AutoAddElements)
		{
			foreach (XmlNode elem in node.SelectNodes("*"))
				AddColumn(elem.Name);
		}
		if (AutoAddAttribs)
		{
			foreach (XmlNode attr in node.SelectNodes("@*"))
				AddColumn(attr.Name, "@"+attr.Name);
		}
	}

	// translate to PropertyDescriptorCollection PropertyDescriptor[] props= new XmlDataViewCol[m_alProps.Count];
	for( int i= 0; i<m_alProps.Count; ++i)
		props[i]= (PropertyDescriptor)m_alProps[i];
	m_props= new PropertyDescriptorCollection(props);
}

XmlDataViewCol schlußendlich ist von PropertyDescriptor abgeleitet. Der Name der Spalte wird bereits von der Basisklasse verwaltet und muß lediglich im Kontruktor übergeben werden.

Die wichtigste Methode in dieser Klasse ist GetValue(). Beim Füllen geht das DataGrid die Liste der Zeilen, also der XmlDataRowViews durch. Für die aktuelle Zeile wird dann für jede Spalte, im zugehörigen XmlDataViewCol-Objekt GetValue aufgerufen. Als Parameter wird das XmlDataRowView-Objekt übergeben. GetValue() muß nur das XmlNode aus dem XmlDataRowView holen, sein eigenes XPath-Statement darauf loslassen und das Ergebnis zurückliefern.

public override object GetValue(object component)
{
	XmlDataRowView row = (XmlDataRowView)component;
	XmlNode node = row.Node;
	XmlNode result = (m_sQuery.Length==0) ? node : node.SelectSingleNode(m_sQuery);
	return (result==null) ? "{null}" : result.InnerXml;
}

Ziel erreicht! :-)


Daß das ganze funktioniert wird hier bewiesen.

Realitäts-Check

Danach habe ich das ganze noch einem Lackmustest unterzogen. Wenn ein XML-Dokument sich auch auf dem Umweg über ein DataSet anbinden ließe, dann sollte das DataBinding an ein XML-Dokument nicht wesentlich komplizierter oder umständlicher sein - sonst würde der sture Entwickler (also ich) immer versuchen, auf ein Dataset auszuweichen. Dann hätte ich mir aber die Arbeit und eine Menge Zeit sparen können.

Also habe ich eine Web-Testanwendung mit editierbarem Datagrid auf Basis von Datasets und SQL genommen (respektive aus "Programming Data-Driven Web Application with ASP.NET", SAMS,"geklaut") und - nachdem ich ein Dataset einfach als XML serialisiert hatte, auf XML-Databinding umgestellt.

Das eigentliche Databinding lief dabei mit gleichem Aufwand:

void Bind()
{
	DataSet ds = new DataSet();
	SqlConnection con = new SqlConnection(sCon);
	SqlDataAdapter da = new SqlDataAdapter("select ProductID, ProductName, 
		QuantityPerUnit, UnitPrice from products order by productid DESC", con);
	da.Fill(ds, "Products");

	grid1.DataSource= ds.Tables["Products"];
	grid1.DataBind();
}

void Bind()
{
	XmlDocument xml= new XmlDocument();
	xml.Load(Server.MapPath("DATA.XML"));
	XmlDataView dv= new XmlDataView(xml, "//Products");

	// doc, query
	dv.AddColumn("ProductID"); 
	dv.AddColumn("ProductName"); 
	dv.AddColumn("QuantityPerUnit"); 
	dv.AddColumn("UnitPrice"); 
	
	grid1.DataSource= dv;
	grid1.DataBind();
}

Die Hauptarbeit war dabei die SQL-Statements für insert, update, etc. auf Xml-Manipulationen umzubauen. Allerdings sollte fairerweise gesagt werden, daß die Dataset-basierte Anwendung diese Dinge selbst auf SQL-Statements umsetzte und sich nicht auf Automatismen des DataSets verließ - bei ASP.NET-Anwendungen dürfte das aber andereseits wiederum der effizientere und damit üblichere Weg sein.

Offene Punkte

Databinding mit beliebigem XPath-Statement

Schön wäre es, wenn man beim DataGrid bei BoundColumn oder DataBinder.Eval() als Feld ein XPath-Statement angeben könnte. Das würde das händische Zufügen der Spalten ersparen. Dummerweise fragt das DataGrid aber nicht mit diesem XPath bei der XmlDataView nach. Es holt sich von der DataView die Property-Liste - in der das XPath natürlich nicht drin ist - um dort das Property selbst zu suchen. Ergo kann man diese Abkürzung leider nicht einschlagen.

Nebenbei: Beim DataSet stellt sich das Problem erst gar nicht, weil dort alle Meta-Informationen für die Tabelle vorhanden sind und niemand auf die Idee kommt, das Databinding an dieser Stelle auszuweiten, etwa tabellenübergreifend zu machen. Ein kleines Beispiel dafür, wie neue Features auch neue Begehrlichkeiten wecken.

Ein Lösungsansatz für die ganz Sturen könnte sein, ein DataGrids abzuleiten, und als Reaktion auf ondatabinding, ondatabindcolumn oder andere Events das XmlDataView von diesen Spalten zu informieren - aber das habe ich nun wirklich nicht mehr ausgetestet.

Interfaces

In der .NET Framework Dokumentation stolpert man über eine ganze Reihe von Interfaces und Methoden, die für das hier vorgestellte Verfahren gar nicht benötigt wurden:

Alle diese Dinge riechen nach Anbindung im Windows Forms Umfeld. Das hier vorgestellte geht nur auf Web Forms ein.

Für eine saubere Lösung müßte IDisposable implementiert werden.

Fazit

Databinding ist eine mächtige Sache, die in der Funktionalität am DataSet ausgerichtet zu sein scheint, aber über Interfaces genügend Eingriffsmöglichkeiten bietet. Pferdefuß ist, daß man dazu wissen muß, wo man wie und warum eingreifen muß. Leider ist die .NET Framework Dokumentation in erster Linie eine Referenz, Zusammenhänge werden i.d.R. nicht erklärt. Immerhin bin ich mit relativ wenig Mitteleinsatz bereits soweit gekommen, daß eine ASP.NET-Seite mit Anbindung (inkl. Editierung) an ein DataSet ohne Probleme auf eine Anbindung an ein XML-Dokument umgestellt werden konnte. Die meiste Arbeit, die dazu nötig war, betraf die Bereiche, bei denen einem .NET auch beim DataSet nicht weiterhilft: Die Editierung an die Datenbank durchzuschleifen.

Kontakt: infoalexander-jung.net top Letzte Änderung: 27.08.2005 23:31:17