## 28.2 Anatomie eines typisierten DataSet Werfen wir einen Blick in die Klassendatei des typisierten DataSet. Öffnen Sie dazu die Datei NWDataSet.Designer.cs im Editor. Die Datei enthält mehrere Klassendefinitionen:
    public partial class NWDataSet : DataSet
    public partial class ProductsTableAdapter : Component
    public partial class CategoriesTableAdapter : Component
Die Klassendefinition, die in direktem Zusammenhang mit dem typisierten DataSet steht, ist die Klasse NWDataSet. Diese Klasse ist von DataSet abgeleitet. Des Weiteren enthält die Datei für jede der beiden Tabellen eine TableAdapter-Klassendefinition: ProductsTableAdapter und CategoriesTableAdapter. Die Letztgenannten sind direkt von System.ComponentModel.Component abgeleitet. Darauf werde ich später noch eingehen. Die Klasse NWDataSet ist prall mit automatisch generiertem Code gefüllt. Sie enthält unter anderem auch die beiden Klassen CategoriesDataTable und ProductsDataTable. Beide sind von DataTable abgeleitet. Um die Datenzeilen der beiden Tabellen zugänglich zu machen, sind im typisierten DataSet auch noch die beiden Klassen CategoriesRow und ProductsRow spezifiziert, die die gemeinsame Basisklasse DataRow haben. ### 28.2.1 Datenzeilen einer Tabelle ausgeben Damit Sie sehen, wie mit einem typisierten DataSet gearbeitet wird, möchte ich Ihnen zunächst ein ganz einfaches Beispiel vorstellen. Es basiert auf dem eben erzeugten typisierten DataSet, das mit NWDataSet bezeichnet worden ist. Dieses DataSet bildet die Grundlage, um uns alle Produktnamen an der Konsole auszugeben. Hier ist zuerst der dazu notwendige Code:
    // ---------------------------------------------------------
    // Beispiel: ...\Kapitel 28\TypisiertesDataSet
    // ---------------------------------------------------------
    using System;
    using System.Collections.Generic;
    using System.Text;
    using TypisiertesDataSet.NWDataSetTableAdapters;
    namespace TypisiertesDataSet {
      class Program {
        static void Main(string[] args) {
          NWDataSet ds = new NWDataSet();
          ProductsTableAdapter tblAd = new ProductsTableAdapter();
          tblAd.Fill(ds.Products);
          foreach(NWDataSet.ProductsRow row in ds.Products) {
            Console.WriteLine("{0}", row.ProductName);
          }
          Console.ReadLine();
        }
      }
    }
Wir benötigen weder ein SqlConnection-Objekt noch ein SqlCommand-Objekt. Alle Informationen, die wir bisher diesen Objekten mitgeteilt haben, stecken nun im typisierten DataSet. Wie Sie weiter oben gesehen haben, lautet dessen Klassenbezeichner NWDataSet. Diese Klasse wird als Erstes instanziiert. In der Datei NWDataSet.Designer.cs ist nicht nur NWDataSet definiert, sondern darüber hinaus auch die beiden Klassen ProductsTableAdapter und CategoriesTableAdapter. Ein TableAdapter ist ein Objekt, das zum Laden und Speichern einer Tabelle im typisierten DataSet dient und zusammen mit der Klasse des typisierten DataSet automatisch erzeugt wird. Schauen Sie sich noch einmal den Code in der Datei NWDataSet.Designer.cs an. Sie werden feststellen, dass die beiden TableAdapter-Klassen dem Namespace TypisiertesDataSet.NWDataSetTableAdapters angehören, einem dem Stamm-Namespace untergeordneten Namespace also. Aus diesem Grund ist der Namespace der Klasse TableAdapter mit using bekannt gegeben worden. Ähnlich wie ein DataAdapter muss auch ein TableAdapter instanziiert werden. Gefüllt wird das DataSet durch den Aufruf von Fill des TableAdapters. Allerdings ist diese Methode typisiert, ihr muss also ein ganz bestimmter Datentyp übergeben werden. Wenn Sie sich die Definition der Methode Fill in der Klasse ProductsTableAdapter ansehen, wissen Sie, welche Tabelle mit Daten gefüllt werden kann:
    public virtual int Fill(NWDataSet.ProductsDataTable dataTable)
Die Fill-Methode, die auf der Referenz eines ProductsTableAdapters aufgerufen wird, kann demnach nur ein Objekt vom Typ NWDataSet.ProductsDataTable füllen. (Beachten Sie, dass die Klasse ProductsDataTable eine innere Klasse der Klasse NWDataSet ist. Öffentliche innere Klassen können instanziiert werden, allerdings beinhaltet der Klassenbezeichner immer die Nennung der äußeren Klasse. Sie wird, durch einen Punkt getrennt, vor den Namen der inneren Klasse gesetzt. Doch was ist nun als Argument der Methode Fill zu übergeben? Die Antwort liefert wieder ein Blick in die Klasse NWDataSet. Das folgende Codefragment zeigt den in diesem Zusammenhang wichtigen Ausschnitt:
    public partial class NWDataSet : DataSet {
      private ProductsDataTable tableProducts;
      private CategoriesDataTable tableCategories;
      private void InitClass() {
        ...
        this.tableProducts = new ProductsDataTable();
        ...
        this.tableCategories = new CategoriesDataTable();
      }
      public ProductsDataTable Products {
        get {
          return this.tableProducts;
        }
    }
Auf Klassenebene sind die beiden Variablen tableProducts und tableCategories deklariert. In der Methode InitClass, die intern bei der Instanziierung des typisierten DataSet aufgerufen wird, erfolgt die Initialisierung der beiden Variablen. Genau genommen beherbergt das typisierte DataSet also die beiden Tabellen Products und Categories. Die Referenz auf die beiden Tabellen wird über schreibgeschützte Eigenschaftsmethoden offengelegt, von denen im Codefragment nur die Eigenschaftsmethode der Products-Tabelle angegeben ist. Daher muss das typisierte DataSet NWDataSet mit
    tblAd.Fill(ds.Products);
gefüllt werden. Es gilt nun, die Datenzeilen der Products-Tabelle zu durchlaufen. In der Klasse NWDataSet sind dazu die beiden inneren Klassen ProductsRow und CategoriesRow definiert. Sie ersetzen im typisierten DataSet die generische Klasse DataRow. Doch wie kommt man an ein Array oder eine Auflistung aller Datenzeilen einer Tabelle? Jetzt wird das Klassenkonzept interessant. Das ProductsDataTable-Objekt repräsentiert nämlich genau genommen nicht nur die Tabelle Products, sondern gleichzeitig auch alle darin enthaltenen Datenzeilen. Sie sehen das bestätigt, wenn Sie sich die Klassendefinition von ProductsDataTable ansehen. Hier finden Sie die Überladung des this-Operators, der als Parameter einen Integer erwartet, der als Index einer bestimmten Datenzeile interpretiert wird.
    public ProductsRow this[int index] {
      get {
        return ((ProductsRow)(this.Rows[index]));
      }
    }
Mit dieser Erkenntnis kann nun auch die foreach-Schleife konstruiert werden:
    foreach(NWDataSet.ProductsRow row in ds.Products)
      ...
Sie haben nun gesehen, wie Sie sich mit einem typisierten DataSet die Datenzeilen einer der beiden Tabellen ausgeben lassen können. Wahrscheinlich bleibt bei Ihnen ein flaues Gefühl im Magen zurück. Um dasselbe Resultat zu erzielen, hätten Sie vermutlich effektiver mit den generischen Klassen von ADO.NET gearbeitet. Ganz unrecht kann ich Ihnen nicht geben. Auch mir ging es am Anfang nicht anders, als ich mich zum ersten Mal mit den typisierten DataSets auseinandergesetzt habe. Aber wir sind mit der Thematik auch noch nicht am Ende angelangt, denn mit einem typisierten DataSet werden noch viele andere Dinge möglich – und vielleicht sogar einfacher. Zugegeben, ein wenig abschreckend und auch unübersichtlich sind die automatisch vom Designer erzeugten Klassendefinitionen schon. Um Ihnen ein wenig die Hemmung davor zu nehmen, bin ich in diesem Abschnitt etwas intensiver auf deren Code eingegangen – zumindest im Zusammenhang mit der selbst auferlegten Aufgabenstellung. Wenn Sie mit typisierten DataSets arbeiten, werden Sie nicht umhinkommen, den Code eventuell an die eigenen Anforderungen und Bedürfnisse anzupassen. Werfen Sie daher ruhig auch einen Blick in die Klassendefinitionen, wenn ich Ihnen in den folgenden Abschnitten weitere Möglichkeiten des Einsatzes der typisierten DataSets vorstellen werde. ### 28.2.2 Datenzeilen hinzufügen Um eine Datenzeile zu einer typisierten Tabelle hinzuzufügen, wird in der typisierten Tabelle eine Methode mit der folgenden Namenskonvention bereitgestellt:
    New<Tabellenname>Row
Diese Methode ersetzt die Methode NewRow in einem untypisierten DataSet. Ferner finden wir auch eine Methode, um die neue Datenzeile zur Auflistung der Datenzeilen hinzuzufügen:
    Add<Tabellenname>Row
Es stellt sich nun nur noch die Frage, wie man die Werte in die neue Datenzeile eintragen kann. Das ist sehr einfach, weil die Spaltenbezeichner der zugrunde liegenden Abfrage als gleichnamige Eigenschaften der typisierten DataRows abgebildet werden. Im folgenden Codefragment wird das deutlich:
    NWDataSet ds = new NWDataSet();
    ProductsTableAdapter tblAd = new ProductsTableAdapter();
    // Tabelle füllen
    tblAd.Fill(ds.Products);
    // neue Datenzeile
    NWDataSet.ProductsRow row = ds.Products.NewProductsRow();
    row.ProductName = "Gewürzgurke";
    row.CategoryID = 2;
    row.UnitPrice = 100;
    ds.Products.AddProductsRow(row);
Sie werden mir zustimmen, dass dieser Code recht intuitiv aussieht und einfach zu lesen ist. Sie werden auch feststellen, dass Sie bei der Codierung durch die IntelliSense-Hilfe eine hervorragende Unterstützung erfahren. Zumindest haben Sie gut lesbaren Programmcode, ohne dass Sie explizit die Spaltenbezeichner angeben müssen (falls Sie keine Indizes verwenden). Wie die Add-Methode der DataRowCollection ist auch die Methode Add<Tabellenname>Row der typisierten DataTable überladen. Rufen Sie diese Überladung auf, wird intern automatisch eine temporäre Datenzeile mit allen Spalten angelegt, und die Spalten werden mit den Daten aus der Argumentenliste gefüllt. Anschließend wird die neue Datenzeile an die Auflistung der Datenzeilen angehängt.
    ds.Products.AddProductsRow("Gewürzgurke",1, 100);
### 28.2.3 Datenzeilen bearbeiten Das Ändern von Datenzeilen ähnelt dem Vorgehen in einem nicht typisierten DataSet. Es stehen Ihnen auch in einem typisierten DataSet die Methoden BeginEdit, EndEdit und CancelEdit zur Verfügung.
    // zu ändernde Datenzeile referenzieren
    NWDataSet.ProductsRow row = ds.Products[0];
    // gewünschte Spalte ändern
    row.BeginEdit();
    row.ProductName = "Hartkäse";
    row.EndEdit();
Alternativ dürfen Sie auch ohne vorherigen Aufruf von BeginEdit sofort den Wert in die betreffende Spalte schreiben. Die Möglichkeit, die Änderung zurückzunehmen, haben Sie dann allerdings nicht. ### 28.2.4 Datenzeilen suchen Eine Datenzeile in einer typisierten DataTable anhand ihres Primärschlüssels zu finden ist eine ganz simple Angelegenheit. Jede typisierte Tabelle veröffentlicht dazu eine typisierte Find-Methode. In der typisierten Products-Tabelle lautet sie FindByProductID, in der typisierten Categories-Tabelle FindByCategoryID.
    NWDataSet.ProductsRow row = ds.Products.FindByProductID(5);
Handelt es sich um einen kombinierten Primärschlüssel aus mehreren Spalten, wie beispielsweise in der Tabelle Order Details, werden in den Bezeichner der Find-Methode die Namen der Spalten übernommen, die den Primärschlüssel bilden. In dieser Reihenfolge geben Sie auch die Werte für die Spalten in der Argumentenliste an. ### 28.2.5 NULL-Werte im typisierten DataSet Jede typisierte DataRow, die NULL-Werte enthalten kann, bietet zwei Methoden an, um mit den NULL-Werten zu arbeiten. Die erste Methode dient dazu, Werte einer Spalte auf NULL zu setzen, die zweite Methode dient dazu, den Wert einer Spalte auf NULL zu untersuchen. In der Tabelle Products ist die Spalte UnitPrice eine der Spalten, die NULL-Werte erlaubt. Die beiden Methoden, die im Zusammenhang mit den NULL-Werten stehen, heißen SetUnitPriceNull und IsUnitPriceNull.
    NWDataSet ds = new NWDataSet();
    ProductsTableAdapter tblAd = new ProductsTableAdapter();
    // Tabelle füllen
    tblAd.Fill(ds.Products);
    // neue Datenzeile hinzufügen
    NWDataSet.ProductsRow newRow = ds.Products.NewProductsRow();
    newRow.ProductName = "Gewürzgurke";
    newRow.CategoryID = 2;
    // Wert der Spalte 'UnitPrice' auf NULL setzen
    newRow.SetUnitPriceNull();
    ds.Products.AddProductsRow(newRow);
    // Datenzeilenliste ausgeben
    foreach(NWDataSet.ProductsRow row in ds.Products) {
      Console.Write("{0,-4}", row.ProductID);
      Console.Write("{0,-35}", row.ProductName);
      if (!row.IsUnitPriceNull())
        Console.WriteLine("{0}", row.UnitPrice);
      else
        Console.WriteLine("NULL");
    }
### 28.2.6 Daten in einem hierarchischen DataSet In einem nicht typisierten DataSet können Sie mit den Methoden GetChildRows, GetParentRow und GetParentRows durch ein hierarchisches DataSet navigieren. Dabei müssen Sie nicht nur die DataRelation angeben, über die die Daten abgefragt werden, sondern darüber hinaus auch die beiden verknüpften Spalten aufseiten der Master- und Detailtabelle. Beim Erzeugen eines typisierten DataSet wird automatisch die Beziehung zwischen zwei oder auch mehr Tabellen erkannt. Sie konnten das im Designer erkennen. Es wird daher ein wenig einfacher, durch das typisierte DataSet zu navigieren. Dazu stellt die Klasse CategoriesRow die Methode GetProductsRows bereit, die ohne Übergabe eines Arguments aufgerufen werden kann und alle verknüpften Datenzeilen der Detailtabelle liefert. Um von einer Detailtabelle auf eine verknüpfte Datenzeile in der Mastertabelle zuzugreifen, weicht der Methodenbezeichner leider die eingeschlagene Konvention auf und verzichtet auf das Präfix Get. Hier heißt die Methode schlichtweg CategoriesRow.
    NWDataSet ds = new NWDataSet();
    ProductsTableAdapter adProducts = new ProductsTableAdapter();
    CategoriesTableAdapter adCategories = new CategoriesTableAdapter();
    // Tabelle füllen
    adProducts.Fill(ds.Products);
    adCategories.Fill(ds.Categories);
    // Datenzeilen der Tabelle 'Categories' durchlaufen
    foreach (NWDataSet.CategoriesRow rowCat in ds.Categories) {
      Console.WriteLine("Kategorie: {0}\n", rowCat.CategoryName);
      // Datenzeilen der Tabelle 'Products' durchlaufen
      foreach (NWDataSet.ProductsRow rowProduct in rowCat.GetProductsRows())
      {
        Console.WriteLine("  {0}", rowProduct.ProductName);
      }
      Console.WriteLine(new string('-',40));
    }