DataTableクラス

DataTable table = new DataTable("sample");
table.Columns.Add("col1");
table.Columns.Add("col2");

DataRow row = table.NewRow();
row["col1"] = "val1";
row["col2"] = "val2";

table.Rows.Add(row);

table.WriteXml("sample.xml");
table.WriteXmlSchema("sample.xsd");

これらは次のように出力されます。

<?xml version="1.0" standalone="yes"?>
<DocumentElement>
  <sample>
    <col1>val1</col1>
    <col2>val2</col2>
  </sample>
</DocumentElement>
<?xml version="1.0" standalone="yes"?>
<xs:schema id="NewDataSet" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
  <xs:element name="NewDataSet" msdata:IsDataSet="true" msdata:MainDataTable="sample" msdata:UseCurrentLocale="true">
    <xs:complexType>
      <xs:choice minOccurs="0" maxOccurs="unbounded">
        <xs:element name="sample">
          <xs:complexType>
            <xs:sequence>
              <xs:element name="col1" type="xs:string" minOccurs="0" />
              <xs:element name="col2" type="xs:string" minOccurs="0" />
            </xs:sequence>
          </xs:complexType>
        </xs:element>
      </xs:choice>
    </xs:complexType>
  </xs:element>
</xs:schema>

このときスキーマ (schema / 概要) のid属性の"NewDataSet"は、引数なしのコンストラクタでDataSetを作成したときの既定の名前であり、これは名前を設定したDataSetにDataTableを含めることで変更できます。DataSet() - DataSet Constructor (System.Data) | Microsoft Learn

DataSet dataSet = new DataSet("MyDataSet");
dataSet.Tables.Add(table);

プロパティ

プロパティ 内容
string TableName テーブルの名前。これは親のDataSetからテーブルを特定するときや、WriteXml()でシリアル化するときに用いられる
DataColumnCollection Columns このテーブルに属する列 (DataColumn) のコレクション
DataRowCollection Rows このテーブルに属する行 (DataRow) のコレクション
int MinimumCapacity 初期状態での行数。データの取得前にこれを設定することで、パフォーマンスを最適化できる。既定値は50
DataColumn[] PrimaryKey テーブルの主キーの列の配列。既定は空の配列
DataView DefaultView テーブルのカスタマイズされたビュー
DataSet DataSet このテーブルが属するDataSet
DataRelationCollection ChildRelations 子Relationのコレクション。Relationが存在しない場合は、空のコレクション
DataRelationCollection ParentRelations 親Relationのコレクション。Relationが存在しない場合は、空のコレクション
ConstraintCollection Constraints このテーブルの制約のコレクション
bool HasErrors このテーブルが属するDataSetのいずれかのテーブルのいずれかの行にエラーがあるならば、 このテーブルのいずれかの行にエラーがあるならば、true
bool CaseSensitive trueならば、DataTableでの文字列比較で大文字/小文字を区別する
CultureInfo Locale CaseSensitiveと同様に、並べ替えなどに影響する。既定では、DataSetに属していないならばシステムのCultureInfoで、それに属すとそのLocaleと同じとなる
     
Properties - DataTable Class (System.Data) | Microsoft Learn

PrimaryKey

DataColumnをPrimaryKeyに設定すると、そのUniqueプロパティはtrueとなり、AllowDBNullはfalseとなります。

主キーはConstraintsプロパティを介して、ConstraintCollection.Add()からも設定できます。

DefaultView

内部ではIndex.InitRecords()で列挙子が用いられているため、このプロパティから取得するときに別スレッドから行を操作すると、「コレクションが修正されました。列挙操作が実行されない可能性があります。(Collection was modified; enumeration operation might not execute.)」としてInvalidOperationExceptionが投げられます。DefaultView - DataTable.cs

HasErrors

ドキュメントでは、DataSetのいずれかのテーブルのいずれかの行にエラーがある (there are errors in any of the rows in any of the tables of the DataSet) ことを判定するとなっています。Definition - DataTable.HasErrors Property (System.Data) | Microsoft Learn

しかし実際はテーブル内の行しか対象となりません。HasErrors - DataTable.cs

DataSet dataSet = new DataSet();
DataTable table1 = dataSet.Tables.Add();
DataTable table2 = dataSet.Tables.Add();

DataRow row1 = table1.Rows.Add();
DataRow row2 = table1.Rows.Add();

bool a1 = table1.HasErrors; // false
bool a2 = table2.HasErrors; // false

row1.RowError = "error";

bool b1 = table1.HasErrors; // true
bool b2 = table2.HasErrors; // false

CaseSensitive

trueならば、DataTableでの文字列比較で大文字/小文字が区別されます。これは並べ替え (sorting)、検索 (searching) やフィルタ (filtering) に影響します。Remarks - DataTable.CaseSensitive Property (System.Data) | Microsoft Learn

DataSetのCaseSensitiveを設定するとそれと同じ値になりますが、DataTableを作成する前に設定されていた値は反映されません。

メソッド

メソッド 機能
NewRow() テーブルと同一スキーマのDataRowを作成できる
AcceptChanges() 最後にAcceptChanges()を呼んだ後の変更をコミットできる
LoadDataRow(Object[], LoadOption) 主キーが一致する行を更新する。一致する行がなければ、与えられた値で新しい行を作成する
BeginLoadData() 通知、インデックスの保守、制約を停止する。これはLoadDataRow()での更新時に利用するとあるが、ReadXml()でも有用
EndLoadData() データ読み込み後の、通知、インデックスの保守、制約を開始する
Select(String) フィルタに適合するDataRowの配列を取得できる。つまり条件を指定して検索できる
WriteXmlSchema(String) テーブルの現在のデータ構造を、指定ファイルにXMLスキーマとして書き込める
WriteXml(String) テーブルの現在の内容を、指定ファイルにXMLとして書き込める
ReadXmlSchema(String) XMLスキーマを、指定ファイルからテーブルへ読み込める
ReadXml(String) XMLデータとスキーマを、指定ファイルからテーブルへ読み込める
Copy() DataTableの構造とデータの両方をコピーする
Clone() DataTableの構造をコピーする
Clear() すべてのデータを消去できる。対象となるのは行だけであり、列は消去されない
GetErrors() エラーを含むDataRowの配列を取得できる
AsDataView(DataTable) LINQで使用できるDataViewを得られる
AsEnumerable(DataTable) LINQで使用できるIEnumerable<DataRow>を得られる AsEnumerable - DataTableExtensions.cs
   
Methods - DataTable Class (System.Data) | Microsoft Learn

Select()

フィルタの基準に一致する、すべてのDataRowの配列を取得できます。

  • 主キーが設定されたDataRowは、DataRowCollection.Find()で得られる
  • DataRelationが設定された親や子のDataRowは、DataRow.GetParentRow()やDataRow.GetChildRows()で得られる
public System.Data.DataRow[] Select (string filterExpression);
Select(String) - DataTable.Select Method (System.Data) | Microsoft Learn

filterExpressionの式は、RowFilterに従います。

DataTable table = new DataTable();
table.Columns.Add("col1");

table.Rows.Add("1");
table.Rows.Add("2");
table.Rows.Add("3");

DataRow[] rows1 = table.Select("col1 > 1"); // rows1.Length は2
DataRow[] rows2 = table.Select("col1 < 1"); // rows2.Length は0

DataViewRowStateでもフィルタできます。

public System.Data.DataRow[] Select (
    string filterExpression,
    string sort,
    System.Data.DataViewRowState recordStates
    );

これらの引数を省いた場合は、Select("", "", DataViewRowState.CurrentRows)と指定するのと同じです。Select() - DataTable.cs

DataTable.CaseSensitiveがfalseならば、式で大文字/小文字が区別されません。これは既定でfalseです。一致するDataRowがなければ、空の配列が返されます。

WriteXmlSchema()

テーブルのデータ構造を、XMLスキーマとして保存できます。

DataTable.WriteXmlSchema()は個々のテーブルの情報しか含まないため、Relationも含めた全体の構造を保存するにはDataSet.WriteXmlSchema()を用います。

WriteXml()

テーブルの内容を、XMLとして保存できます。そこにシリアル化に対応しないクラスのデータが含まれていると、「'***' は IXmlSerializable インターフェイスを実装しないため、シリアル化を実行できません。」としてInvalidOperationExceptionが投げられます。

public void WriteXml (string fileName, System.Data.XmlWriteMode mode, bool writeHierarchy);
WriteXml(String, XmlWriteMode, Boolean) - DataTable.WriteXml Method (System.Data) | Microsoft Learn

個々の列が出力される形式は、DataColumn.ColumnMappingで指定します。保存が不要な列は、そこでMappingType.Hiddenとします。

fileNameがすでに存在していると作成日時が維持されたまま更新日時が更新され、既存のファイルが書き替えられます。

XMLにデータ構造も含めたいならば、modeにXmlWriteMode.WriteSchemaを指定します。ただし同時に初期値やエラーも記録したいならば、WriteXmlSchema()で別ファイルに記録します。これを省略するとXmlWriteMode.IgnoreSchemaが指定されます。

table.WriteXmlSchema("sample.xsd");
table.WriteXml("sample.xml", XmlWriteMode.DiffGram);

DiffGram

初期値やエラーを記録するには、modeにXmlWriteMode.DiffGramを指定します。DiffGrams - ADO.NET | Microsoft Learn

たとえば次のようなデータをファイルに書き込むと、

DataTable table = new DataTable("sample");
table.Columns.Add("col1");
table.Columns.Add("col2");

DataRow row1 = table.Rows.Add("1a", "1b");
DataRow row2 = table.Rows.Add("2a", "2b");
DataRow row3 = table.Rows.Add("3a", "3b");
DataRow row4 = table.Rows.Add("4a", "4b");

row1.RowError = "E";
row2.SetColumnError(0, "e1");
row2.SetColumnError(1, "e2");

row2.AcceptChanges();
row3.AcceptChanges();
row4.AcceptChanges();

row3[0] = "**";
row4.Delete();

table.WriteXml("sample.xml", XmlWriteMode.DiffGram);

次のように出力されます。

<?xml version="1.0" standalone="yes"?>
<diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">
  <DocumentElement>
    <sample diffgr:id="sample1" msdata:rowOrder="0" diffgr:hasChanges="inserted" diffgr:hasErrors="true">
      <col1>1a</col1>
      <col2>1b</col2>
    </sample>
    <sample diffgr:id="sample2" msdata:rowOrder="1" diffgr:hasErrors="true">
      <col1>2a</col1>
      <col2>2b</col2>
    </sample>
    <sample diffgr:id="sample3" msdata:rowOrder="2" diffgr:hasChanges="modified">
      <col1>**</col1>
      <col2>3b</col2>
    </sample>
  </DocumentElement>
  <diffgr:before>
    <sample diffgr:id="sample3" msdata:rowOrder="2">
      <col1>3a</col1>
      <col2>3b</col2>
    </sample>
    <sample diffgr:id="sample4" msdata:rowOrder="3">
      <col1>4a</col1>
      <col2>4b</col2>
    </sample>
  </diffgr:before>
  <diffgr:errors>
    <sample diffgr:id="sample1" diffgr:Error="E" />
    <sample diffgr:id="sample2">
      <col1 diffgr:Error="e1" />
      <col2 diffgr:Error="e2" />
    </sample>
  </diffgr:errors>
</diffgr:diffgram>

このデータを読み込むとき<diffgr:errors>要素にあるdiffgr:idに一致する行が<DocumentElement>要素に存在しないと、XMLDiffLoader.ProcessErrors()で「オブジェクト参照がオブジェクト インスタンスに設定されていません。」としてNullReferenceExceptionが投げられます。

なおXmlWriteMode.IgnoreSchemaとしてスキーマを書き込まないと、次のように出力されます。

<?xml version="1.0" standalone="yes"?>
<DocumentElement>
  <sample>
    <col1>1a</col1>
    <col2>1b</col2>
  </sample>
  <sample>
    <col1>2a</col1>
    <col2>2b</col2>
  </sample>
  <sample>
    <col1>**</col1>
    <col2>3b</col2>
  </sample>
</DocumentElement>

バイナリ

BinaryFormatterを用いることで、テーブルの内容をバイナリで保存できます。C# DataTable Binary Serialization - Stack Overflow

こうすることでファイルサイズは小さくできますが、処理時間はさほど変わりません。

table.WriteXmlSchema("sample.xsd");

object[][] items = new object[table.Rows.Count][];
for (int i = 0; i < table.Rows.Count; i++)
{
    items[i] = table.Rows[i].ItemArray;
}

using (FileStream fileStream = new FileStream("sample.bin", FileMode.Create))
{
    BinaryFormatter formatter = new BinaryFormatter();
    formatter.Serialize(fileStream, items);
}
DataTable table = new DataTable();
table.ReadXmlSchema("sample.xsd");

object[][] items;
using (FileStream fileStream = new FileStream("sample.bin", FileMode.Open))
{
    BinaryFormatter formatter = new BinaryFormatter();
    items = (object[][])formatter.Deserialize(fileStream);
}

foreach (object[] item in items)
{
    table.Rows.Add(item);
}

ReadXmlSchema()

XMLスキーマをテーブルへ読み込めます。

public void ReadXmlSchema (string fileName);
ReadXmlSchema(String) - DataTable.ReadXmlSchema Method (System.Data) | Microsoft Learn

読み込み前にColumnsが設定されていると、その情報は読み込まれません。

DataTable tableA = new DataTable("A");
tableA.Columns.Add("col1");
tableA.Columns.Add("col2");
tableA.WriteXmlSchema("sample.xsd");

DataTable tableB = new DataTable();
tableB.ReadXmlSchema("sample.xsd");
// tableB.Columns: {col1}, {col2} ... Columnsが読み込まれている

DataTable tableC = new DataTable();
tableC.Columns.Add("col3");
tableC.ReadXmlSchema("sample.xsd");
// tableC.Columns: {col3} ... Columnsが読み込まれていない

ReadXml()

XMLスキーマとデータをテーブルへ読み込めます。

public System.Data.XmlReadMode ReadXml (string fileName);
ReadXml(String) - DataTable.ReadXml Method (System.Data) | Microsoft Learn

読み込み元をstringで指定すると、内部ではXmlTextReaderで読み込まれます。ReadXml - DataTable.cs

DiffGram形式で保存されていないとき、読み込まれた個々のDataRowのRowStateは、Addedになります。

読み込み時に投げられる例外には、それぞれ次のように対処します。

  • InvalidOperationException「DataTable は Xml のスキーマの推論をサポートしていません。
    • 読み込み元のXMLに、スキーマを含める
    • 先にReadXmlSchema()で、スキーマを読み込む
    • 読み込む先のDataTableに、あらかじめColumnを設定しておく
  • ArgumentExceptionが「DataTable 'TableName' は、ソースの DataTable のどれにも一致しません。
    • 読み込む先のDataTableのTableNameをソースと同一にする。もしくは設定しない

Merge()

現在のテーブルへ、指定のテーブルをマージできます。

public void Merge (System.Data.DataTable table);
Merge(DataTable) - DataTable.Merge Method (System.Data) | Microsoft Learn
DataTable tableA = new DataTable();
tableA.Columns.Add("col1");
tableA.Columns.Add("col2");

DataTable tableB = new DataTable();
tableB.Columns.Add("col2");
tableB.Columns.Add("col3");

tableA.Rows.Add("1", "2");
tableB.Rows.Add("2", "3");

tableA.Merge(tableB);

マージ後、tableAは次のようになります。

  col1 col2 col3
tableA.Rows[0] "1" "2" DBNull
tableA.Rows[1] DBNull "2" "3"

このとき、

tableA.PrimaryKey = new[] { tableA.Columns["col2"] };

のように主キーを設定しておくと、

  col1 col2 col3
tableA.Rows[0] "1" "2" "3"

のようにマージされます。

CreateDataReader()

DataTableのデータを取得するための、DataTableReaderを取得できます。

public System.Data.DataTableReader CreateDataReader ();
DataTable.CreateDataReader Method (System.Data) | Microsoft Learn
DataTableReader reader = dataTable.CreateDataReader();
while (reader.Read())
{
    for (int i = 0; i < reader.FieldCount; i++)
    {
        Console.Write(reader[i]);
    }
}

GetChanges()

public System.Data.DataTable GetChanges ();
GetChanges() - DataTable.GetChanges Method (System.Data) | Microsoft Learn

変更されたデータのコピーが返されます。変更がないならば、nullが返されます。このとき返されるのは新しいインスタンスであり、参照のコピーではありません。

public System.Data.DataTable GetChanges (System.Data.DataRowState rowStates);
GetChanges(DataRowState) - DataTable.GetChanges Method (System.Data) | Microsoft Learn

引数にDataRowStateをとる形式では、それによってフィルタされた変更されたデータが返されるとドキュメントにあります。しかし実際には変更の有無は考慮されておらず、指定のrowStatesの状態のデータのコピーが返されます。GetChanges - DataTable.cs

DataTable table = new DataTable();
table.Columns.Add();
table.Rows.Add(1);
table.Rows.Add(2);
table.Rows.Add(3);

DataTable table1a = table.GetChanges();
DataTable table1b = table.GetChanges(DataRowState.Unchanged);

int count1a = table1a.Rows.Count; // 3
int count1b = table1b.Rows.Count; // NullReferenceException

table.AcceptChanges();

DataTable table2a = table.GetChanges();
DataTable table2b = table.GetChanges(DataRowState.Unchanged);
int count2a = table2a.Rows.Count; // NullReferenceException
int count2b = table2b.Rows.Count; // 3

table.Rows[0][0] = 10; // 1つの行を変更する

DataTable table3a = table.GetChanges();
DataTable table3b = table.GetChanges(DataRowState.Unchanged);
int count3a = table3a.Rows.Count; // 1 … 変更された1つの行が返される
int count3b = table3b.Rows.Count; // 2 … Unchangedの行、2つが返される

Clone()

構造をコピーできます。ただしスキーマと制約はコピーされますがDataRelationはコピーされず、ForeignKeyConstraintの制約は失われます。

DataSet dataSet = new DataSet();
DataTable tableA = dataSet.Tables.Add();
DataTable tableB = dataSet.Tables.Add();

DataColumn columnA = tableA.Columns.Add();
DataColumn columnB = tableB.Columns.Add();

DataRelation relation = dataSet.Relations.Add(columnA, columnB);

ConstraintCollection cA = tableA.Constraints; // UniqueConstraint
ConstraintCollection cB = tableB.Constraints; // ForeignKeyConstraint
DataRelationCollection rB = tableB.ParentRelations; // DataRelation


DataTable tableAc = tableA.Clone();
DataTable tableBc = tableB.Clone();

ConstraintCollection cAc = tableAc.Constraints; // UniqueConstraint
ConstraintCollection cBc = tableBc.Constraints; // Empty
DataRelationCollection rBc = tableBc.ParentRelations; // Empty

DataRelationもコピーしたいならば、DataSet.Clone()を用います。Clone() - DataSet.cs

DataSet dataSetc = dataSet.Clone();
DataRelationCollection c = dataSetc.Tables[tableB.TableName].ParentRelations; // DataRelation

Copy()

構造とデータの両方をコピーできます。

public System.Data.DataTable Copy ();
DataTable.Copy Method (System.Data) | Microsoft Learn

返されるDataTableは、同じ構造 (スキーマと制約) とデータです。ただし構造はClone()によってコピーされるためコピーされる内容もそれと同一で、ForeignKeyConstraintの制約はコピーされません。Copy - DataTable.cs

Clear()

public void Clear ();
DataTable.Clear Method (System.Data) | Microsoft Learn

このテーブルを消去することで子テーブルの行が孤立する場合には、InvalidConstraintExceptionが投げられ失敗します。このような場合には子テーブルのデータを先に消去するか、これらのテーブルが属するDataSetのClear()で消去します。

GetErrors()

public System.Data.DataRow[] GetErrors ();
DataTable.GetErrors Method (System.Data) | Microsoft Learn

このメソッドの処理はHasErrorsがtrueであるRowsの要素を確認しているだけのため、戻り値にはRowStateがDeletedの行なども含まれます。GetErrors - DataTable.cs

イベント

区分 イベント 発生タイミング
Table Initialized Occurs after the DataTable is initialized.
TableNewRow 新しいDataRowが挿入されたとき
TableClearing Occurs when a DataTable is cleared.
TableCleared Occurs after a DataTable is cleared.
Column ColumnChanging DataRowの指定のDataColumnの値が変更されるとき
ColumnChanged DataRowの指定のDataColumnの値が変更されたとき。ただしDataColumnを指定してイベントを登録することはできないため、ハンドラでイベントを発生させたDataColumnを判定する
Row RowChanging DataRow内の値またはRowStateが、変更されるとき (同じ値を設定しても発生するため、変更されていない場合もある)
RowChanged DataRow内の値またはRowStateが、正しく変更されたとき (同じ値を設定しても発生するため、変更されていない場合もある)
RowDeleting テーブル内の行が削除され、RowStateがDeletedに変更される前 (Delete()やDataRowCollection.Remove()で、Detachedになるときにも発生する)
RowDeleted テーブル内の行が削除され、RowStateがDeletedに変更された後 (同上)
  Disposed Adds an event handler to listen to the Disposed event on the component. (Inherited from MarshalByValueComponent)
Events - DataTable Class (System.Data) | Microsoft Learn

行のカスタムエラー (DataRow.RowError) や列のエラーが変更されてもRowStateは変更されず、どのイベントも発生しません。

行に対して実行された動作

実行された動作 (Action) は、DataRowChangeEventArgs.Actionで得られます。このDataRowAction列挙型は行に対して実行された動作を表すものであり、行の状態を表すDataRowState列挙型とは異なります。

DataRowAction 列挙型
列挙子 数値 内容
Nothing 0 The row has not changed.
Delete 1 行はテーブルから削除された (DataRowStateがDeletedまたはDetachedとなった後)
Change 2 The row has changed.
Rollback 4 The most recent change to the row has been rolled back.
Commit 8 行への変更がコミットされた ※1
Add 16 The row has been added to the table.
ChangeOriginal 32 The original version of the row has been changed.
ChangeCurrentAndOriginal 64 The original and the current versions of the row have been changed.
DataRowAction Enum (System.Data) | Microsoft Learn

※1 RowStateがAddedかDetachedではない行に対してDataRowCollection.Remove()を呼んだときも、内部でDataRow.AcceptChanges()が呼ばれるためこの状態になる。

次のようにハンドラを登録し、どのような状況でイベントが発生するかを検証します。

DataTable table = new DataTable();
table.Columns.Add();

table.RowChanging += (object sender, DataRowChangeEventArgs e) => { Console.WriteLine($"RowChanging {e.Action} {e.Row.RowState}"); };
table.RowChanged += (object sender, DataRowChangeEventArgs e) => { Console.WriteLine($"RowChanged {e.Action} {e.Row.RowState}"); };
table.RowDeleting += (object sender, DataRowChangeEventArgs e) => { Console.WriteLine($"RowDeleting {e.Action} {e.Row.RowState}"); };
table.RowDeleted += (object sender, DataRowChangeEventArgs e) => { Console.WriteLine($"RowDeleted {e.Action} {e.Row.RowState}"); };
追加、修正、コミット、修正、ロールバック
DataRow row = table.Rows.Add();
// イベント名 動作 行の状態
// RowChanging Add Detached
// RowChanged  Add Added

row[0] = 1;
// RowChanging Change Added
// RowChanged  Change Added

row.AcceptChanges();
// RowChanging Commit Added
// RowChanged  Commit Unchanged

row.AcceptChanges(); // 何も変更せずコミット
// RowChanging Commit Unchanged
// RowChanged  Commit Unchanged


row[0] = 2;
// RowChanging Change Unchanged
// RowChanged  Change Modified

row[0] = 2; // 同じ値を設定
// RowChanging Change Modified
// RowChanged  Change Modified

row.RejectChanges();
// RowChanging Rollback Modified
// RowChanged  Rollback Unchanged

row.RejectChanges(); // 何も変更せずロールバック
// イベントは発生しない
  • ActionがRollbackならば値が確定している。
  • ActionがCommitならば値が確定しているが、Row.RowStateがDeletedならば削除、Detachedならば除去されている。
  • Row.RowStateがUnchangedのとき、RowChangingイベントでは値が確定しているが、RowChangedイベントでは前の状態を表しているため不定
DataRowCollection.Remove()による除去
DataRow row1 = table.Rows.Add();

table.Rows.Remove(row1); // DataRowState.Added の状態で実行
// RowDeleting Delete Added
// RowDeleted  Delete Detached


DataRow row2 = table.Rows.Add();
row2.AcceptChanges();

table.Rows.Remove(row2); // DataRowState.Unchanged の状態で実行
// RowDeleting Delete Unchanged
// RowDeleted  Delete Deleted
// RowChanging Commit Deleted
// RowChanged  Commit Detached

row2.AcceptChanges(); // RowNotInTableException
DataRow.Delete()による削除
DataRow row1 = table.Rows.Add();

row1.Delete(); // DataRowState.Added の状態で実行
// RowDeleting Delete Added
// RowDeleted  Delete Detached


DataRow row2 = table.Rows.Add();
row2.AcceptChanges();

row2.Delete(); // DataRowState.Unchanged の状態で実行
// RowDeleting Delete Unchanged
// RowDeleted  Delete Deleted

row2.AcceptChanges();
// RowChanging Commit Deleted
// RowChanged  Commit Detached

各イベント発生時のDataRowVersionごとの値の比較

DataRowVersionごとの値は、RowChangingやRowChangedの発生時点で異なります。たとえば次のように操作した場合を検証します。

DataTable table = new DataTable();
table.Columns.Add();

DataRow row = table.Rows.Add(); // Action: Add
row[0] = 1;          // Action: Change (1st)
row.AcceptChanges(); // Action: Commit (1st)

row[0] = 2;          // Action: Change (2nd)
row.AcceptChanges(); // Action: Commit (2nd)

row.Delete(); // Action: Delete
行の変更イベント
table.RowChanging += (object sender, DataRowChangeEventArgs e) =>
{
    object current, original, proposed;
    switch (e.Action)
    {
        case DataRowAction.Add:
            current  = e.Row[0, DataRowVersion.Current];  // VersionNotFoundException
            original = e.Row[0, DataRowVersion.Original]; // VersionNotFoundException
            proposed = e.Row[0, DataRowVersion.Proposed]; // DBNull
            break;

        case DataRowAction.Change:
            current  = e.Row[0, DataRowVersion.Current];  // 1st:DBNul, 2nd:"1"
            original = e.Row[0, DataRowVersion.Original]; // 1st:VersionNotFoundException, 2nd:"1"
            proposed = e.Row[0, DataRowVersion.Proposed]; // 1st:"1", 2nd:"2"
            break;

        case DataRowAction.Commit:
            current  = e.Row[0, DataRowVersion.Current];  // 1st:"1", 2nd:"2"
            original = e.Row[0, DataRowVersion.Original]; // 1st:VersionNotFoundException, 2nd:"1"
            proposed = e.Row[0, DataRowVersion.Proposed]; // 1st, 2nd:VersionNotFoundException
            break;
    }
};

table.RowChanged += (object sender, DataRowChangeEventArgs e) =>
{
    object current, original, proposed;
    switch (e.Action)
    {
        case DataRowAction.Add:
            current  = e.Row[0, DataRowVersion.Current];  // DBNull
            original = e.Row[0, DataRowVersion.Original]; // VersionNotFoundException
            proposed = e.Row[0, DataRowVersion.Proposed]; // VersionNotFoundException
            break;

        case DataRowAction.Change:
            current  = e.Row[0, DataRowVersion.Current];  // 1st:"1", 2nd:"2"
            original = e.Row[0, DataRowVersion.Original]; // 1st:VersionNotFoundException, 2nd:"1"
            proposed = e.Row[0, DataRowVersion.Proposed]; // 1st, 2nd:VersionNotFoundException
            break;

        case DataRowAction.Commit:
            current  = e.Row[0, DataRowVersion.Current];  // 1st:"1", 2nd:"2"
            original = e.Row[0, DataRowVersion.Original]; // 1st:"1", 2nd:"2"
            proposed = e.Row[0, DataRowVersion.Proposed]; // 1st, 2nd:VersionNotFoundException
            break;
    }
};
行の削除イベント
table.RowDeleting += (object sender, DataRowChangeEventArgs e) =>
{
    object current, original, proposed;
    current  = e.Row[0, DataRowVersion.Current];  // "2"
    original = e.Row[0, DataRowVersion.Original]; // "2"
    proposed = e.Row[0, DataRowVersion.Proposed]; // VersionNotFoundException
};

table.RowDeleted += (object sender, DataRowChangeEventArgs e) =>
{
    object current, original, proposed;
    current  = e.Row[0, DataRowVersion.Current];  // VersionNotFoundException
    original = e.Row[0, DataRowVersion.Original]; // "2"
    proposed = e.Row[0, DataRowVersion.Proposed]; // VersionNotFoundException
};

TableNewRow

DataTable.NewRow()で作成されたときのみ発生し、DataTable.Rows.Add()では発生しません。Add()ではRowChangedが発生し、そのActionがAddであることで新しい行が追加されたことを検知できます。

DataTable table = new DataTable();
table.TableNewRow += (object sender, DataTableNewRowEventArgs e) => { };
table.RowChanged += (object sender, DataRowChangeEventArgs e) => { };

DataRow row1 = table.NewRow(); // TableNewRow
table.Rows.Add(row1);          // RowChanged: Add

DataRow row2 = table.Rows.Add(); // RowChanged: Add

ColumnChanged

DataRowの値が変更されたときに、DataColumnごとに発生します。DataTable.ColumnChanged Event (System.Data) | Microsoft Learn

行の追加や削除では、このイベントは発生しません。Handling DataTable Events - ADO.NET | Microsoft Learn

が追加や削除されたことは、DataColumnCollection.CollectionChangedで検知できます。

DataTable table = new DataTable();
table.ColumnChanged += (object sender, DataColumnChangeEventArgs e) =>
{
    Debug.WriteLine(e.Column);
};

DataColumn col1 = table.Columns.Add(); // イベントは発生しない
DataColumn col2 = table.Columns.Add(); // イベントは発生しない

DataRow row = table.Rows.Add(1); // イベントは発生しない
row[col1] = "a"; // e.Column が Column1 でイベント発生
row[col2] = "b"; // e.Column が Column2 でイベント発生
row[col2] = "b"; // e.Column が Column2 でイベント発生 (値が変更されていなくても発生)
row.ItemArray = new object[] { "c", "d" }; // e.Column が Column1 と Column2 でイベント発生

row.Delete(); // イベントは発生しない

RowChanged

行内の値の変更が、成功したときに発生します。またRowStateが変更されたときにも発生しますが、Deletedに変更されたときは発生せず、その状態はRowDeletedイベントで処理できます。

同じ値を設定しても発生するため、実際には変更されていない場合もあります。

制約のAcceptRejectRuleがCascadeだとコミットが関連する行にも波及するため、そのテーブルの行の値やRowStateが変更されていなくても、このイベントが発生することがあります。そして親行の変更によってこのイベントが発生するときは、その呼び出し元 (source) は子行自体となります。

public event System.Data.DataRowChangeEventHandler RowChanged;
DataTable.RowChanged Event (System.Data) | Microsoft Learn
Microsoft Learnから検索