DataViews als DataTable persistieren
Geschrieben von: Christoph Wille Datenzugriff ist mit ADO.NET sehr einfach - Daten werden mit Hilfe eines DataAdpaters vom Server geholt und in ein DataSet gespeichert, und dann kann man Daten aus den DataTables auslesen. Und braucht man eine andere Sortierung oder Filterung, wozu zum Server gehen, mit Hilfe der DataView Klasse kann man das Client-seitig erledigen. Einziger Schönheitsfehler in .NET 1.x an dieser Sache ist, daß man aus der eben erzeugten Sicht (DataView mit .Sort oder .RowFilter Kriterien) nicht einfach einen neuen DataTable machen kann. Stellen wir uns also folgendes Szenario vor: Sie haben aus der berühmten Northwind Datenbank alle Kunden geholt. Nun verwenden Sie eine DataView, um nur Kunden anzuzeigen, die in Buenos Aires ihren Firmensitz haben. Alles kein Problem - außer Sie möchten jetzt diese Kunden (und nur diese!) als DataTable weiterreichen. Es gibt auf der DataView keine Methode, die Ihnen diese Arbeit erledigt. Daher: man schreibt sich die notwendigen Methoden selbst, um diese Arbeit zu erledigen. Fragt sich nur, wie macht man das am besten? Eine neue Klasse? Statische oder Instanzmethoden? Was soll die Methode können? Da ich "nebenbei" mit .NET 2.0 arbeite, das bereits - weil genug Kunden mehr als laut geschrien haben - diese Funktionalität anbietet, was liegt also näher, als die 2.0 Funktionalität zurückzuportieren, um später bei der Umstellung möglichst wenig Ärger zu haben? Klar ist das die beste Lösung, und zwar wird man sinnvollerweise die normale DataView Klasse in der neuen kapseln, und nur die notwendigen Methoden hinzufügen: public class DataViewEx : DataView { public DataTable ToTable(); public DataTable ToTable(bool isDistinct, string[] columnNames); } Die erste ToTable Implementierung ruft die zweite mit false, null Parametern auf, sie hält selbst keine Funktionalität. Wir müssen uns also nur die zweite ansehen, was ihre Aufgabe sein soll. Der erste Parameter dient dazu, um doppelte Zeilen im DataTable auszuschließen, was speziell dann zur Anwendung kommt, wenn im Ziel-DataTable keine Indexspalten mehr dabei sind. Der zweite Parameter ist auch nützlich: damit kann man angeben, daß nur eine Untermenge der orginalen Spalten in den Ziel-DataTable übernommen werden sollen. Beginnen wir am Anfang - der Klasse DataViewEx und ihren Konstruktoren. Sinnvollerweise sind die Konstruktorensignaturen identisch mit denen der DataView Klasse, und leiten an deren Implementierungen weiter: using System; using System.Data; using System.Text; using System.Collections; namespace DotNetGerman { public class DataViewEx : DataView { #region Mapped Constructors public DataViewEx() : base() { } public DataViewEx(DataTable table) : base(table) { } public DataViewEx(DataTable table, string RowFilter, string Sort, DataViewRowState RowState) : base(table, RowFilter, Sort, RowState) { } Damit haben wir die Anforderungen erfüllt, um unsere neue Klasse anstelle der originalen DataView Klasse verwenden zu können, ohne daß der Unterschied auffallen würde. Bevor wir nun in die Implementierung der "echten" ToTable Methode eintauchen, möchte ich Ihnen der Übersicht halber eine auf das absolute Minimum abgespeckte ToTable Methode zeigen, damit Sie die verwendeten Klassen und Methoden leichter ersehen: public DataTable ToTable() { // short circuiting out here int nRowCount = this.Count; if (0 == nRowCount) return null; // #1: clone the schema DataTable tableNew = Table.Clone(); // #2: get the column count, we need it repeatedly int nColumnCount = tableNew.Columns.Count; // #3: copy the values to the new table for (int iRow = 0; iRow < nRowCount; iRow++) { DataRow rowNew = tableNew.NewRow(); for (int iColumn=0; iColumn < nColumnCount; iColumn++) { rowNew[iColumn] = this[iRow][iColumn]; } tableNew.Rows.Add(rowNew); } return tableNew; } Wenn man alle Spalten kopiert, ist die Implementierung wirklich simpel: unter #1 wird ein Schema-Klon der unter der DataView liegenden DataTable Klasse erzeugt. Dieser Klon ist bis auf die Spaltendefinitionen leer, er enthält keine Daten. Diese werden unter #3 mit Hilfe eines Loops über die Zeilen (eingeschränkt durch die .Sort und .RowFilter Kriterien) und die Spalten kopiert. Und damit sind wir auch schon fertig, und können die DataTable, die nur die gewünschten gefilterten Daten enthält, zurückgeben. Jetzt werden wir diese Methode umbauen, um unseren Anforderungen an DISTINCT Zeilen und Spalten-Subsets gerecht zu werden. Zuerst die ToTable() Methode ohne Parameter: public DataTable ToTable() { return ToTable(false, null); } Wie angekündigt verweist diese nur auf unsere parametrisierte ToTable Methode. Diese beginnt mit einigen "Informationserhebungen": public DataTable ToTable(bool isDistinct, string[] columnNames) { // short circuiting out here int nRowCount = this.Count; if (0 == nRowCount) return null; // get the column count, we need it repeatedly int nColumnCount = Table.Columns.Count; int nTargetColumnCount = nColumnCount; // if second param == null, we copy the entire table if (null != columnNames) nTargetColumnCount = columnNames.Length; string[] tableColumnNames = new string[nColumnCount]; for (int iColumn = 0; iColumn < nColumnCount; iColumn++) tableColumnNames[iColumn] = Table.Columns[iColumn].ColumnName; Was immer wieder im Code vorkommt, ist die Kontrolle, ob wir jetzt alle Spalten, oder nur bestimmte Spalten kopieren werden. Für diesen Zweck wird auch das Array mit den Spaltennamen befüllt, wir brauchen dieses später um auf nicht-kopierte Spalten zugreifen zu können. Weiter geht es: bool[] keepColumn = new bool[nColumnCount]; int[] tableColumnIndexes = new int[nTargetColumnCount]; int[] newtableColumnIndexes = new int[nTargetColumnCount]; // check to see if the selected columns actually exist & map indexes if (null != columnNames) { for (int i=0; i < columnNames.Length; i++) { if (Table.Columns.Contains(columnNames[i])) { int colIndex = Table.Columns.IndexOf(columnNames[i]); tableColumnIndexes[i] = colIndex; keepColumn[colIndex] = true; } else { throw new ArgumentException("Column does not exist in base table"); } } } else { for (int i=0; i < nColumnCount; i++) { tableColumnIndexes[i] = i; newtableColumnIndexes[i] = i; keepColumn[i] = true; } } Hier werden die zu kopierenden Spalten auf den Index gemappt, und klarerweise gecheckt, ob der Programmierer nicht unbeabsichtigt eine nicht existierende Spalte gewählt hat. Zusätzlich führen wir mit, welche Spalten nicht kopiert werden sollen. Der else Zweig behandelt den Fall, daß alle Spalten kopiert werden sollen. Damit haben wir alle Infos um die DataTable mit den entsprechenden Spalten zu erzeugen: // clone the schema and remove unnecessary columns DataTable tableNew = Table.Clone(); // now we can build the final table... all we need to do is map the // string[] to the column indexes // in the new table that was now created if (null != columnNames) { // remove columns we no longer need for (int k = 0; k < nColumnCount; k++) { if (keepColumn[k] == false) tableNew.Columns.Remove(tableColumnNames[k]); } // map column names to column indexes for (int i=0; i < columnNames.Length; i++) { int colIndex = tableNew.Columns.IndexOf(columnNames[i]); newtableColumnIndexes[i] = colIndex; } } Zugegebenermaßen habe ich das Pferd von der verkehrten Seite aufgezäumt: zuerst den DataTable geklont, und dann alles an Spalten entfernt, was der Programmierer in der Zieltabelle nicht wollte. Und was ich auch noch tun muß ist mir das Mapping der Indizes zu den verbliebenen Spalten zu holen, damit das Kopieren klappt: // both variables used for determining duplicate rows StringBuilder stb = new StringBuilder(); Hashtable ht = new Hashtable(); // copy the values to the new table for (int iRow = 0; iRow < nRowCount; iRow++) { DataRow rowNew = tableNew.NewRow(); if (isDistinct) stb.Remove(0, stb.Length); for (int iColumn=0; iColumn < tableColumnIndexes.Length; iColumn++) { object currentValue = this[iRow][tableColumnIndexes[iColumn]]; if (isDistinct && (null != currentValue)) stb.Append(currentValue.ToString()); rowNew[newtableColumnIndexes[iColumn]] = currentValue; } // do the DISTINCT checks before inserting row if (isDistinct) { string strRowKey = stb.ToString(); if (!ht.ContainsKey(strRowKey)) { ht.Add(strRowKey, null); tableNew.Rows.Add(rowNew); } } else { tableNew.Rows.Add(rowNew); } } // return the new table return tableNew; } } Der Grund warum dieser Teil so angewachsen ist liegt in der Behandlung der DISTINCT Zeilen begründet. Der einfachste Weg Zeilen zu vergleichen ist die Spaltenwerte zusammenzufügen, einen Hash zu bilden, und mit den Hashes der bereits eingefügten Zeilen zu vergleichen. Und um mir diese Arbeit zu erleichtern, verwende ich StringBuilder und Hashtable. Abgesehen davon ist der Code mit der einfachen Version von ToTable beinahe identisch, und wir sind fertig. Zum Abschluß möchte ich Ihnen noch ein Beispiel zeigen, wie die Klasse in Applikationen eingesetzt werden kann (diese ist auch im heutigen Download inkludiert): SqlConnection conn = new SqlConnection("data source=(local)\\NetSDK;" + "initial catalog=Northwind; integrated security=SSPI"); SqlDataAdapter da = new SqlDataAdapter("select * from Customers", conn); DataSet ds = new DataSet("Demo"); da.Fill(ds, "Customers"); DataViewEx demoFilter = new DataViewEx(ds.Tables[0]); demoFilter.RowFilter = "City='Buenos Aires'"; DataTable table = demoFilter.ToTable(true, new string[] {"ContactTitle", "CompanyName", "ContactName"}); // DataTable table = demoFilter.ToTable(true, new string[] {"ContactTitle"}); // DataTable table = demoFilter.ToTable(); dg.DataSource = table; Es hat sich nur die Klasse verändert, und man bekommt die ToTable Overloads - sonst ist es die altbekannte DataView geblieben. SchlußbemerkungMit etwas Programmieraufwand kann man auch die .NET Framework Klassen um Funktionalität bereichern. Speziell dieses Konvertieren einer DataView in eine DataTable ist ein immer wiederkehrendes Problem, das nun hoffentlich ein für alle Mal einer Lösung zugeführt ist. Download des CodesKlicken Sie hier, um den Download zu starten. Verwandte Artikel
Das DataTable Objekt in ADO.NET Wenn Sie jetzt Fragen haben...Wenn Sie Fragen rund um die in diesem Artikel vorgestellte Technologie haben, dann schauen Sie einfach bei uns in den Community Foren der deutschen .NET Community vorbei. Die Teilnehmer helfen Ihnen gerne, wenn Sie sich zur im Artikel vorgestellten Technologie weiterbilden möchten. Eine weitere sehr hilfreiche Resource ist das deutsche ASP.NET Wiki, das als zentrale Anlaufstelle für Tips, Tricks, Know How und alles Nützliche was man in seinem Alltag als (ASP).NET-Entwickler so braucht und entdeckt gedacht ist. Haben Sie Fragen die sich direkt auf den Inhalt des Artikels beziehen, dann schreiben Sie dem Autor! Unsere Autoren freuen sich über Feedback zu ihren Artikeln. Ein einfacher Klick auf die Autor kontaktieren Schaltfläche (weiter unten) und schon haben Sie ein für diesen Artikel personalisiertes Anfrageformular.
Und zu guter Letzt möchten wir Sie bitten, den Artikel zu bewerten. Damit helfen Sie uns, die Qualität der Artikel zu verbessern - und anderen Lesern bei der Auswahl der Artikel, die sie lesen sollten.
©2000-2006 AspHeute.com |