## 26.3 Änderungen in einer DataTable vornehmen Sehen wir uns nun an, wie wir einer DataTable eine neue DataRow hinzufügen und eine vorhandene DataRow löschen oder editieren können. Um einen wichtigen Punkt gleich vorwegzunehmen: Jegliche Änderung betrifft zunächst nur das DataSet. Die Originaldatenbank weiß davon nichts. Erst zu einem späteren Zeitpunkt werden alle Änderungen zur Datenbank übermittelt. Wir behandeln daher in diesem Abschnitt nur die lokalen Aktualisierungen. Im Kapitel 27, »ADO.NET – Aktualisieren der Datenbank«, werden wir uns der Aktualisierung der Originaldatenquelle zuwenden. ### 26.3.1 Editieren einer DataRow Es gibt drei Möglichkeiten, eine Zeile zu aktualisieren. Im einfachsten Fall weisen Sie der betreffenden Spalte nur den neuen Inhalt zu:
    ds.Tables[0].Rows[3]["ProductName"] = "Kirschkuchen";
Die Änderung wird sofort in die angegebene Spalte der entsprechenden Datenzeile geschrieben. Die zweite Möglichkeit puffert die Änderung. Dazu wird vor Beginn der Änderung die Methode BeginEdit auf der zu ändernden Datenzeile aufgerufen und die Änderung mit EndEdit bestätigt. Sie können die eingeleitete Änderung auch zurücksetzen und anstelle von EndEdit die Methode CancelEdit aufrufen. Die Zeile wird dann in den Zustand zurückversetzt, den sie vor BeginEdit hatte.
    DataRow row = ds.Tables[0].Rows[3];
    row.BeginEdit();
    row["ProductName"] = "Kirschkuchen";
    row.EndEdit();
    // alternativ: row.cancelEdit();
Die Pufferung der Änderung ist nicht der einzige Unterschied zwischen den beiden Aktualisierungsmöglichkeiten. Die DataTable verfügt über mehrere Ereignisse, die nur im Zusammenhang mit BeginEdit und EndEdit ausgelöst werden. Es handelt sich hierbei um: RowChanging RowChanged ColumnChanging ColumnChanged Diese Ereignisse spielen eine Rolle, wenn Änderungen an einer Datenzeile oder Spalte überprüft werden müssen. Die Ereignisse werden nicht ausgelöst, wenn Sie CancelEdit aufrufen. Wenn wir uns später dem Zurückschreiben der Änderungen in die Originaldatenbank zuwenden, werden wir noch einmal auf diese Ereignisse zurückkommen. Die dritte Möglichkeit bietet uns die Eigenschaft ItemArray, die ein Object-Array beschreibt. Mit dieser Eigenschaft können Sie den Inhalt einer Datenzeile abrufen oder verändern. ItemArray arbeitet mit einem Array, in dem jedes Element einer Spalte entspricht. Mit einer Codezeile können Sie mehrere Spaltenwerte abrufen und editieren. Ist in einer Zeile nur eine Teilmenge der verfügbaren Werte zu modifizieren, verwenden Sie null, um anzuzeigen, dass der Wert dieser Spalte nicht geändert werden soll. Im folgenden Codefragment werden drei Spalten der Tabelle Products abgefragt. In der ersten Datenzeile soll mit der Eigenschaft ItemArray der Produktbezeichner modifiziert werden. Weil der Schlüsselwert nicht geändert wird, muss an der ersten Position null in das Objekt-Array geschrieben werden.
    SqlCommand cmd = new SqlCommand();
    cmd.Connection = con;
    cmd.CommandText = "SELECT ProductID, ProductName, UnitPrice FROM Products";
    DataSet ds = new DataSet();
    SqlDataAdapter da = new SqlDataAdapter(cmd);
    da.Fill(ds);
    DataRow row = ds.Tables[0].Rows[0];
    row.ItemArray = new Object[] {null, "Kirschkuchen"};
#### Spaltenwert auf »NULL« festlegen Möchten Sie den Wert einer Spalte auf NULL setzen, verwenden Sie die Klasse DBNull, die sich im Namespace System befindet. Mit der Eigenschaft Value legen Sie den Wert einer Spalte in einer DataRow auf NULL fest.
    DataRow row = ds.Tables[0].Rows[4];
    row["UnitPrice"] = DBNull.Value;
### 26.3.2 Datenzeile löschen Das Löschen einer Datenzeile ist sehr einfach: Sie rufen hierzu die Methode Delete der DataRow auf, die gelöscht werden soll.
    row.Delete();
Es ist falsch anzunehmen, dass die betreffende Datenzeile nun aus der DataTable entfernt wird. Sie ist immer noch vorhanden, allerdings kennzeichnet ADO.NET sie als gelöscht. Der Grund für die Markierung ist) dass das Löschen zunächst nur das aktuelle DataSet betrifft und zu einem späteren Zeitpunkt der Originaldatenbank mitgeteilt werden muss. Es wäre daher auch falsch, eine Datenzeile mit Remove oder RemoveAt aus der DataRowCollection der Tabelle zu entfernen, denn dann findet der Aktualisierungsprozess die Datenzeile nicht mehr. ### 26.3.3 Neue Datenzeile hinzufügen Eine Datenzeile zu einer DataTable hinzuzufügen, ist auch nicht schwierig. Allerdings stellt die Klasse DataRow keinen öffentlichen Konstruktor zur Verfügung, denn woher sollte ein auf diese Weise konstruiertes DataRow-Objekt etwas von den Spalten wissen, durch die es beschrieben wird? ADO.NET bietet Ihnen genauso wie zum Editieren einer Datenzeile drei Varianten an, um eine neue Datenzeile zu einer DataTable hinzuzufügen. Zunächst einmal sei die Methode NewRow der DataTable erwähnt. Eine so erzeugte neue Zeile enthält alle Informationen über die Spalten in der Tabelle. Werden im Schema keine Standardwerte vorgegeben, sind die Inhalte der Spalten auf NULL gesetzt. Haben Sie alle Einträge in der neuen Zeile vorgenommen, müssen Sie die neue Zeile der DataRowCollection anhängen, denn das leistet der Aufruf von NewRow nicht.
    DataTable tbl = ds.Tables[0];
    DataRow row = tbl.NewRow();
    row["ProductName"] = "Erbsensuppe";
    row["UnitPrice"] = 2;
    row["SupplierID"] = 3;
    // ...
    tbl.Rows.Add(row);
Die zweite Möglichkeit, eine neue Datenzeile hinzuzufügen, bietet eine Überladung der Methode Add der DataRowCollection. Übergeben Sie dem Methodenaufruf die Spaltenwerte in der Reihenfolge, die der Reihenfolge der Spalten in der SELECT-Abfrage entspricht. Basierend auf der Auswahlabfrage
    SELECT ProductName, Unitprice, UnitsInStock FROM Products
könnte eine neue Datenzeile wie folgt hinzugefügt werden:
    ds.Tables[0].Rows.Add("Mehl", 20, 0);
Im Gegensatz zur Methode NewRow wird die neue Datenzeile automatisch der DataRowCollection hinzugefügt. Die dritte Möglichkeit stellt die Methode LoadDataRow der DataTable dar. Diese Methode arbeitet ähnlich wie die zuvor gezeigte Add-Methode der DataRowCollection, verlangt aber die Angabe von zwei Parametern. Geben Sie im ersten Parameter ein Array von Werten an, dessen Elemente den Spalten in der Tabelle entsprechen. Tragen Sie im zweiten Parameter false ein. Der Grund dafür ist, dass die so gekennzeichnete Datenzeile als neue Datenzeile interpretiert wird. LoadDataRow eignet sich nämlich auch dazu, eine bestimmte Datenzeile zu suchen und zu modifizieren. Dann muss dem zweiten Parameter jedoch true übergeben werden.
    ds.Tables[0].LoadDataRow(new object[] {"Mehl", 20, 0}, false);
### 26.3.4 Der Sonderfall: Autoinkrementspalten Viele Tabellen in Datenbanken beschreiben das Primärschlüsselfeld mit Autoinkrementwerten. Das ist vorteilhaft, weil eine zentrale Logik immer eindeutige Ganzzahlen erzeugt. Fügen wir jedoch eine neue Datenzeile zu einer DataTable hinzu, die ein solches Schlüsselfeld definiert, haben wir keine Verbindung zur Originaldatenbank. Mit anderen Worten: Wir kennen den neuen Wert des Schlüsselfeldes nicht. Den erfahren wir erst, wenn wir die Datenbank aktualisiert haben und eine entsprechende Abfrage starten. ADO.NET unterstützt uns mit drei Eigenschaften der DataColumn, um auch diese scheinbare Problematik zu lösen: AutoIncrement AutoIncrementSeed AutoIncrementStep Um von ADO.NET in einer DataTable Autoinkrementwerte generieren zu lassen, muss die Eigenschaft AutoIncrement der betreffenden Spalte auf true gesetzt werden. Mit AutoIncrementSeed und AutoIncrementStep werden die von ADO.NET erzeugten Werte gesteuert. AutoIncrementSeed beschreibt dabei den Startwert der Autoinkrementspalte für die erste neu hinzugefügte Datenzeile. AutoIncrementStep gibt die Schrittweite an, mit der neue Schlüsselwerte generiert werden. Legen Sie für eine Autoinkrementspalte beispielsweise AutoIncrementSeed=1 und AutoIncrementStep=2 fest, lauten die Werte für die drei nachfolgend hinzugefügten Datenzeilen 1, 3 und 5. Die Werte, die ADO.NET erzeugt, müssen Sie als Platzhalter verstehen. Sie werden später bei der Aktualisierung der Originaldatenbank nicht mit zurückgeschrieben. Die tatsächlichen Schlüsselwerte erzeugt die Datenbank selbst. Doch welche Werte sollten Sie in der DataTable vergeben? Eigentlich müssen Sie nur sicherstellen, dass neue Schlüsselwerte nicht mit den Schlüsselwerten in Konflikt geraten, die bereits in der DataTable enthalten sind. Sie können auch davon ausgehen, dass negative Werte in der Datenbank nicht verwendet werden. Empfehlenswert ist daher, die beiden Eigenschaften AutoIncrementSeed und AutoIncrementStep auf jeweils –1 festzulegen. Zudem sollten diese Einstellungen erfolgen, ehe das DataSet mit den Daten gefüllt wird. Sehen wir uns dazu nun ein Beispiel an.
    // ------------------------------------------------------------------
    // Beispiel: ...\Kapitel 26\AutoIncrementSample
    // ------------------------------------------------------------------
    class Program {
      static void Main(string[] args) {
        SqlConnection con = new SqlConnection();
        con.ConnectionString = "...";
        SqlCommand cmd = new SqlCommand();
        cmd.Connection = con;
        cmd.CommandText = "SELECT ProductID, ProductName FROM Products";
        DataSet ds = new DataSet();
        SqlDataAdapter da = new SqlDataAdapter(cmd);
        // Schema-Informationen abrufen
        da.FillSchema(ds, SchemaType.Source);
        // festlegen, wie die neuen Schlüsselwerte erzeugt werden
        ds.Tables[0].Columns[0].AutoIncrementSeed = -1;
        ds.Tables[0].Columns[0].AutoIncrementStep = -1;
        // DataSet füllen
        da.Fill(ds);
        // neue Datenzeilen hinzufügen
        DataRow row = ds.Tables[0].NewRow();
        row["ProductName"] = "Kaffee";
        ds.Tables[0].Rows.Add(row);
        row = ds.Tables[0].NewRow();
        row["ProductName"] = "Milch";
        ds.Tables[0].Rows.Add(row);
        row = ds.Tables[0].NewRow();
        row["ProductName"] = "Zucker";
        ds.Tables[0].Rows.Add(row);
        // Ausgabe des DataSets
        foreach (DataRow tempRow in ds.Tables[0].Rows)
          Console.WriteLine("{0,-6}{1}",tempRow[0],tempRow[1]);
        Console.ReadLine();
      }
    }
Damit der Code überschaubar bleibt, werden aus der Datenbank nur zwei Spalten der Tabelle Products abgefragt. Die Primärschlüsselspalte ProductID ist als Autoinkrementspalte definiert. Mit FillSchema werden die Metadaten der Tabelle bezogen. In der Praxis würde man diese Methode in einer Anwendung wohl aus den weiter oben angeführten Gründen nicht benutzen, aber für ein Beispielprogramm ist sie durchaus geeignet. Da FillSchema auch AutoIncrement=true für die Spalte ProductID setzt, muss diese Eigenschaft der DataColumn nicht mehr gesetzt werden. Später werden der DataTable drei Datenzeilen hinzugefügt. Der temporäre Schlüsselwert der ersten ist auf –1 festgelegt. Alle weiteren neuen Schlüsselwerte werden mit der Schrittweite –1 generiert, sodass der Schlüsselwert der zweiten neuen Datenzeile –2 ist, der der dritten neuen Datenzeile –3. Beachten Sie, dass die Autoinkrementeigenschaften vor dem Füllen des DataSets gesetzt werden müssen. Ansonsten wirken sich die Eigenschaftswerte nicht auf die Autoinkrementwerte aus, die die DataTable generiert. Zum Abschluss unserer Betrachtungen zu den Autoinkrementwerten noch eine Anmerkung: Vergessen Sie nicht, dass die generierten Schlüsselwerte nur Platzhalter innerhalb der DataTable darstellen. Erst nach der Übermittlung zur Originaldatenbank werden die tatsächlichen und endgültigen Schlüsselwerte von der Datenbank erzeugt. Sie sollten daher vermeiden, die temporären Schlüsselwerte dem Anwender anzuzeigen. Es könnte zu unabsehbaren Folgen führen, wenn der Anwender sich eine ADO.NET-Schlüsselnummer notiert, die später nach der Aktualisierung nicht mehr existiert. ### 26.3.5 Was passiert bei der Änderung einer Datenzeile? #### Die Eigenschaft »RowState« Ein DataSet ist im lokalen Cache der Anwendung abgelegt. Während des Löschens, Änderns und Hinzufügens von Datenzeilen besteht zu der Originaldatenbank keine Verbindung. Wenn der Benutzer die geänderten Daten später an die Datenbank übermitteln möchte, muss sich das DataSet daran erinnern können, welche Zeilen von einer Änderung betroffen sind, und natürlich auch daran, welcher Natur diese Änderung ist. Haben Sie beispielsweise eine Datenzeile gelöscht, muss für die betreffende Datenzeile ein DELETE-SQL-Statement zur Datenbank geschickt werden, das das Löschen in der Originaltabelle bewirkt. Haben Sie eine Datenzeile geändert, brauchen Sie noch ein passend formuliertes UPDATE-Statement. Wie die Aktualisierungsabfragen erzeugt werden, erläutere ich an dieser Stelle noch nicht. Das werden wir uns im Detail später noch ansehen. Aber Sie sollten an dieser Stelle erkennen, wie wichtig es ist, dass jede Datenzeile ihren eigenen Aktualisierungszustand beschreiben kann. ADO.NET speichert die notwendigen Zustandsinformationen in der Eigenschaft RowState jeder Datenzeile. Die Eigenschaft wird durch die Enumeration DataRowState beschrieben, wie in Tabelle 26.3 aufgelistet.

Tabelle 26.3 Mitglieder der Enumeration »DataRowState«

Member | Beschreibung
Added | Die Zeile wurde einer DataRowCollection hinzugefügt.
Deleted | Die Zeile wurde mit der Delete-Methode der DataRow gelöscht.
Detached | Die Zeile wurde erstellt, ist jedoch nicht Teil einer DataRowCollection. Eine DataRow befindet sich in diesem Zustand, wenn sie unmittelbar nach ihrer Erstellung noch keiner Auflistung hinzugefügt wurde oder wenn sie aus einer Auflistung entfernt wurde.
Modified | Die Zeile wurde geändert.
Unchanged | Die Zeile wurde nicht geändert.
#### Ursprünglicher und aktualisierter Inhalt einer Datenzeile Sie wissen nun, dass eine Datenzeile beschreibt, ob und wie sie modifiziert wurde. Um später die Änderung zur Datenbank zu übermitteln, reicht das aber noch nicht aus: Es fehlen noch dringend notwendige Informationen. Stellen Sie sich dazu nur vor, Sie würden den Artikelbezeichner einer Datenzeile der Tabelle Products ändern und die Änderung mit einem UPDATE-Statement der Datenbank mitteilen. Das SQL-Statement könnte wie folgt lauten:
    UPDATE Products
    SET ProductName = @Param1
    WHERE ProductID = @Param2 AND ProductName = @Param3
Im Parameter @Param1 wird der geänderte, also neue Wert übermittelt, in @Param2 der Schlüsselwert der Datenzeile und in @Param3 der ursprüngliche Wert der Spalte ProductID. Wenn Sie ein solches Statement absetzen, darf natürlich zwischen dem Abrufen der Dateninformationen und der Aktualisierung kein zweiter Benutzer den Produktnamen geändert haben. Die Folge wäre eine Konfliktsituation, weil die anstehende Änderung nicht in die Datenbank geschrieben werden kann. Dieser (scheinbaren) Problematik wollen wir an dieser Stelle noch nicht weiter nachgehen. Sie sollten Folgendes erkennen: Um das UPDATE-Statement erfolgreich absetzen zu können, brauchen Sie außer den geänderten Werten auch den Originalwert, um die Datenzeile in der Datenbank zu identifizieren. Für diesen Zweck ist der Indexer einer DataRow überladen. Anstatt mit
    row["Productname"]
den aktuellen, also möglicherweise geänderten Wert der Spalte ProductName abzurufen, können Sie auch mit
    row["Productname", DataRowVersion.Original]
auf den von der Datenbank bezogenen Originalwert zurückgreifen. DataRowVersion ist eine Aufzählung, mit der die gewünschte Version der betreffenden Spalte in der Datenzeile angegeben werden kann.

Tabelle 26.4 Werte der Enumeration »DataRowVersion«

Member | Beschreibung
Current | Die Zeile enthält aktuelle Werte.
Default | Die Zeile enthält einen vorgeschlagenen Wert.
Original | Die Standardversion der Zeile, dem aktuellen DataRowState entsprechend
Proposed | Die Zeile enthält ihre ursprünglichen Werte.
Sie können sich jetzt sicher vorstellen, dass es von jeder DataRow immer zwei Versionen gibt: Zunächst einmal DataRowVersion.Original für die Werte, die aus der Datenbank bezogen worden sind, und unter DataRowVersion.Current die aktuellen und möglicherweise geänderten Werte. Jetzt wird auch verständlich, warum es nach der Einleitung einer Änderung mit BeginEdit mittels CancelEdit möglich ist, den ursprünglichen Zustand einer DataRow wiederherzustellen. Rufen Sie mit
    row["ProductName"]
den Inhalt einer Spalte ab, wird immer DataRowVersion.Current ausgewertet. Das ist wichtig zu wissen, denn sollten Sie die DataRowCollection in einer Schleife durchlaufen, innerhalb der zum Beispiel auf Spalten aller geänderten Zeilen zugegriffen wird, dürfen Sie von einer gelöschten Zeile nicht DataRowVersion.Current abrufen. Sie können aber sehr wohl DataRowVersion.Original auswerten, weil eine als gelöscht markierte Datenzeile nicht aus der DataRowCollection entfernt wird. Das nächste Beispiel zeigt Ihnen die prinzipielle Vorgehensweise: Nachdem das DataSet aus der Artikeltabelle mit Daten gefüllt ist, wird zuerst ein weiterer Datensatz hinzugefügt. Anschließend wird in der Tabelle nach einem bestimmten Artikel gesucht (Tofu). Hierzu wird die Methode Select der DataTable aufgerufen, die mehrere Überladungen aufweist. Benutzt wird in diesem Beispiel die einfach parametrisierte Version, der ein Suchkriterium als Zeichenfolge übergeben wird. Die Zeichenfolge entspricht der WHERE-Klausel in einer SELECT-Abfrage ohne die Angabe von WHERE. Zum Schluss wird auch noch die fünfte Datenzeile aus der Liste »gelöscht«. An der Konsole werden abschließend nur die Datenzeilen angezeigt, die in irgendeiner Form gegenüber dem Original eine Änderung erfahren haben.
    // ------------------------------------------------------------------
    // Beispiel: ...\Kapitel 26\AusgabeModifizierterDaten
    // ------------------------------------------------------------------
    class Program {
      static void Main(string[] args) {
        SqlConnection con = new SqlConnection();
        con.ConnectionString = "...";
        SqlCommand cmd = new SqlCommand();
        cmd.Connection = con;
        cmd.CommandText = "SELECT ProductID, ProductName, UnitsInStock " +
                          "FROM Products";
        DataSet ds = new DataSet();
        SqlDataAdapter da = new SqlDataAdapter(cmd);
        da.FillSchema(ds, SchemaType.Source);
        ds.Tables[0].Columns["ProductID"].AutoIncrementSeed = -1;
        ds.Tables[0].Columns["ProductID"].AutoIncrementStep = -1;
        da.Fill(ds);
        // neue Datenzeile hinzufügen
        DataRow newRow = ds.Tables[0].NewRow();
        newRow["ProductName"] = "Camembert";
        newRow["UnitsInStock"] = 100;
        ds.Tables[0].Rows.Add(newRow);
        // Datenzeile ändern
        DataRow[] editRow = ds.Tables[0].Select("ProductName='Tofu'");
        if (editRow.Length == 1) {
          editRow[0].BeginEdit();
          editRow[0]["UnitsInStock"] = 1000;
          editRow[0].EndEdit();
        }
        else
          Console.WriteLine("Datenzeile 'Tofu' nicht gefunden.");
        // Datenzeile löschen
        ds.Tables[0].Rows[4].Delete();
        // Ausgabe
        foreach (DataRow tempRow in ds.Tables[0].Rows) {
          if (tempRow.RowState == DataRowState.Added)
            Console.WriteLine("Neue Datenzeile: {0}", tempRow["Productname"]);
          else if (tempRow.RowState == DataRowState.Modified) {
            Console.WriteLine("Modifiziert: {0}", tempRow["Productname"]);
            Console.WriteLine("Alter Wert: {0}",
                       tempRow["UnitsInStock", DataRowVersion.Original]);
            Console.WriteLine("Neuer Wert: {0}", tempRow["UnitsInStock"]);
          }
          else if (tempRow.RowState == DataRowState.Deleted)
            Console.WriteLine("Gelöscht: {0}",
                 tempRow["ProductName", DataRowVersion.Original]);
          else
            continue;
          Console.WriteLine(new string('-', 40));
        }
        Console.ReadLine();
      }
    }
### 26.3.6 Manuelles Steuern der Eigenschaft »DataRowState« Die Eigenschaft RowState ist für jede Datenzeile nach dem Füllen des DataSets auf Unchanged gesetzt. Je nachdem, ob Sie eine Datenzeile ändern, löschen oder hinzufügen, wird ihr Zustand automatisch auf Modified, Deleted oder Added gesetzt. Mit zwei Methoden können Sie den RowState per Code beeinflussen: AcceptChanges und RejectChanges. Es handelt sich hierbei um Methoden, die Sie auf dem DataSet, der DataTable oder einer bestimmten DataRow aufrufen können. #### Die Methode »AcceptChanges« AcceptChanges setzt den RowState einer Datenzeile von Added oder Modified auf Unchanged. Dabei wird der Inhalt von DataRowVersion.Original durch den von DataRowVersion.Current beschriebenen Inhalt ersetzt. Trifft die Methode auf eine gelöschte Datenzeile, wird die Datenzeile aus der DataRowCollection entfernt und RowState auf DataRowState.Detached gesetzt. Rufen Sie AcceptChanges auf die Referenz des DataSets auf, wird mit allen Datenzeilenänderungen in sämtlichen Tabellen so verfahren. Der Aufruf auf eine bestimmte Tabelle im DataSet wirkt sich dementsprechend nur auf die betreffenden Datenzeilen der Tabelle aus. Analog können Sie auch den Zustand einer bestimmten Datenzeile ändern. #### Die Methode »RejectChanges« Mit RejectChanges verwerfen Sie alle Änderungen. Die Methode setzt die aktuellen Werte der DataRow auf ihre ursprünglichen Werte zurück. Dabei werden die in der DataRow enthaltenen Änderungen verworfen, also:
    DataRowVersion.Current = DataRowVersion.Original
Der RowState hängt nach dem Aufruf von RejectChanges vom anfänglichen RowState ab. Der Zustand Deleted oder Modified wird zu Unchanged, eine hinzugefügte Datenzeile wird zu Detached. #### Die Methoden »SetAdded« und »SetModified« SetAdded ändert den Zustand einer Datenzeile in Added und kann nur für eine DataRow aufgerufen werden, deren RowState den Wert Unchanged oder Added hat. Ist der Ausgangszustand ein anderer, wird die Ausnahme InvalidOperationException ausgelöst.