🔢

Windows Forms C#定石 - DataGridView - DataTable

2025/04/15に公開

はじめに

C# ソフト開発時に、決まり事として実施していた内容を記載します。

DataGridView については下記記事もあります

テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。

  • Windows Forms - .NET Framework 4.8
  • Windows Forms - .NET 8

DataTable

DataGridView に DataTable をバインドするサンプルとして、SQL Server のテーブルを一覧操作するコードを記載しようと思います。

https://learn.microsoft.com/ja-jp/dotnet/api/system.data.datatable

DataRow

https://learn.microsoft.com/ja-jp/dotnet/api/system.data.datarow

DataRow は DataTable 内のデータ行です。
DataRow.RowStatus で、AcceptChanges(RowStatusのクリア)実施後、行に対する操作を確認できます。

DataRowState 状態
Added 行追加された
Deleted 行削除された
Modified 行データが更新された
Unchanged 変更されていない
Detached DataRow を新規作成して、DataTable に未追加の状態

SQL Server

SQL Server 2022 Express をインスタンス名:Hoge、SQL Server 認証モードでインストールして、AdventureWorks サンプル データベース - SQL Server を導入します。
サンプルとして手頃なテーブルが見つけられなかったので、Northwindサンプル の Shippers(ShipperID の IDENTITY 指定は除外)を作成することにします。

USE AdventureWorks2022
GO
CREATE SCHEMA Northwind
GO
CREATE TABLE Northwind.Shippers (
  [ShipperID] int NOT NULL ,
  [CompanyName] nvarchar (40) NOT NULL ,
  [Phone] nvarchar (24) NOT NULL ,
  CONSTRAINT [PK_Shippers] PRIMARY KEY
  (
    [ShipperID]
  )
)
GO
INSERT INTO Northwind.Shippers ([ShipperID],[CompanyName],[Phone])
  VALUES(1,'Speedy Express','(503) 555-9831')
INSERT INTO Northwind.Shippers ([ShipperID],[CompanyName],[Phone])
  VALUES(2,'United Package','(503) 555-3199')
INSERT INTO Northwind.Shippers ([ShipperID],[CompanyName],[Phone])
  VALUES(3,'Federal Shipping','(503) 555-9931')
GO

SQL Server アクセスを行うので NuGet Gallery | Microsoft.Data.SqlClient を導入します。

PM> NuGet\Install-Package Microsoft.Data.SqlClient
using System.Data;
using Microsoft.Data.SqlClient;

Sql Server で DataTable ならば、SqlDataAdapter 利用が便利です。

https://learn.microsoft.com/ja-jp/dotnet/api/system.data.sqlclient.sqldataadapter

SQL Server からのデータ取得は SqlDataAdapter.Fill を利用しますが、データ更新(削除/追加を含む)は、SqlDataUdapter.Update は利用せず、削除、追加、更新の順でそれぞれを実行する形態とします。

構成

DataGridView 上での行追加、行追加は操作ミスがあり得るので、それぞれの操作用のボタンと、SQL Server への更新用ボタンを用意します。

コントール 名称 用途
DataGridView dataGridView1
Button btnAppend 行追加
Button btnDelete 行削除
Button btnUpdate 更新

行追加はサブフォームを用意して、サブフォーム入力値で更新します。

コントール 名称 用途
TextBox txtShipperID ShipperID 入力
TextBox txtCompanyName CompanyName 入力
TextBox txtPhone Phone 入力
Button btnUpdate 更新
Button btnCancel キャンセル

サンプルコード

メインフォーム - データ取得

削除行は、DataRow.RowState = DataRowState.Deleted となりますが、項目データをアクセスできないので、削除した ShipperID を管理する List<int> を用意します。

private List<int> lstDelete = new List<int>();

メインフォーム(Form1)コンストラクタで、DateGridView プロパティ設定を行います。
列定義では DataPropertyName に DataTable の項目名をセットします。
また、ShipperID は Primary Key なので ReadOnly とします。

public Form1()
{
  InitializeComponent();

  // デザイナで DataGridView dataGridView1 を配置
  dataGridView1.AutoGenerateColumns = false;
  dataGridView1.SelectionMode = DataGridViewSelectionMode.FullRowSelect;  // 行選択モード
  dataGridView1.MultiSelect = false;             // 複数選択無効
  dataGridView1.ColumnHeadersVisible = true;     // 列ヘッダ表示
  dataGridView1.RowHeadersVisible = false;       // 行ヘッダ非表示
  dataGridView1.AllowUserToDeleteRows = false;   // 削除キーで削除を無効
  dataGridView1.AllowUserToAddRows = false;      // 末尾行での行追加を無効
  dataGridView1.ScrollBars = ScrollBars.Both;
  dataGridView1.EditMode = DataGridViewEditMode.EditOnEnter;

  dataGridView1.Columns.AddRange(new DataGridViewColumn[]
  {
    new DataGridViewTextBoxColumn { Name = "ShipperID", 
      DataPropertyName = "ShipperID", Width = 100, ReadOnly = true },
    new DataGridViewTextBoxColumn { Name = "CompanyName", 
      DataPropertyName = "CompanyName", Width = 200 },
    new DataGridViewTextBoxColumn { Name = "Phone", 
      DataPropertyName = "Phone", Width = 200 }
  });
}

SQL Server 接続文字列を SqlConnectionStringBuilder で作成します。

https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.data.sqlclient.sqlconnectionstringbuilder

// 接続文字列:const string でも良いが、SqlConnectionStringBuilder 利用
private string GetConnectionString()
{
  var builder = new Microsoft.Data.SqlClient.SqlConnectionStringBuilder();
  builder.DataSource = "(local)\\Hoge";          // 自マシン、インスタンス:Hoge
  builder.InitialCatalog = "AdventureWorks2022"; // データベース
  builder.IntegratedSecurity = false;            // SQL Server 認証
  builder.UserID = "sa";
  builder.Password = "$$PASSWORD";               // TODO
  builder.Encrypt = false;
  builder.CommandTimeout = 10;                   // 10秒
  return builder.ToString();
}

SQL Server からのデータ取得は、SqlDataAdapter.Fill を利用して、SELECT 結果を DataTable にセットします。
SqlDataAdapter.Fill は、コネクションが close の状態で呼ばれた時は、コネクションを open して、データを取得後に close します。

DataGridView.DataSource に直接 DataTable をセットすることも可能ですが、BindingSource を経由して DataTable をセットすると、複数の操作(フィルタリング、ソート、選択行の追跡、イベント処理)でメリットがあるので、この形態とします。

https://learn.microsoft.com/ja-jp/dotnet/desktop/winforms/controls/sort-and-filter-ado-net-data-with-wf-bindingsource-component

// SQL Server から DataTable 生成して dataGridVieww1 にバインド
private bool DataLoad()
{
  bool bResult = false;

  // 削除行データをクリア
  lstDelete.Clear();

  // SQL Server から取得
  try
  {
    var queryString = "SELECT * FROM Northwind.Shippers";

    using (var connection = new Microsoft.Data.SqlClient.SqlConnection(GetConnectionString()))
    using (var command = new Microsoft.Data.SqlClient.SqlCommand(queryString, connection))
    using (var adapter = new Microsoft.Data.SqlClient.SqlDataAdapter())
    {
      if (adapter != null)
      {
        adapter.SelectCommand = command;
        var table = new DataTable();
        adapter.Fill(table);                          // DataTable に取得
        table.AcceptChanges();                        // RowState クリア
        dataGridView1.DataSource = new BindingSource  // データバインド
        {
          DataSource = table
        };
        bResult = true;
      }
    }
  }
  catch (Exception)
  {
    // TODO - ERROR
  }
  return bResult;
}

メインフォーム Shown イベントなどで、上記 DataLoad を呼び出すと、DataGridView に一覧表示されます。

ShipperID は Primary Key なので、重複判断を行うメソッドを用意します。

// メインフォーム:ShipperID 重複チェック
public bool IsDupulicateId(int shipperId)
{
  // DataGridView は BindingSource でフィルタリング結果表示などの可能性があるので
  // バインドしている DataTable を直接参照
  if (dataGridView1.DataSource is BindingSource bindingSource
   && bindingSource.List is DataView boundView
   && boundView.Table is DataTable dataTable)
  {
    foreach (DataRow row in dataTable.Rows)
    {
      // 削除された行のデータをアクセスすると Exception になる
      if (row.RowState != DataRowState.Deleted)
      {
        if (row["ShipperID"] is int id)
        {
          if (shipperId == id)
          {
            // 対象 ID は存在
            return true;
          }
        }
      }
    }
  }
  return false;
}

サブフォーム

行追加用のサブフォーム(DlgAppend.cs)は、更新処理で ShipperID 重複チェックを行い、問題なければ、プロパティに各項目の情報をセットします。
CompanyName については、フォーム プロパティに CompanyName が存在するので、ShipperName というプロパティとします。

// サブフォーム
public partial class DlgAppend : Form
{
  public int ShipperID { get; set; } = -1;
  public string ShipperName { get; set; } = string.Empty;
  public string Phone { get; set; } = string.Empty;

  public DlgAppend()
  {
    InitializeComponent();
  }
  // .NET Framework 時 object? の ? 不要
  private void btnUpdate_Click(object? sender, EventArgs e)
  {
    int shipperId;
    if (int.TryParse(txtShipperID.Text.Trim(), out shipperId))
    {
      if(this.Owner is Form1 form)
      {
        if (shipperId <= 0 || form.IsDupulicateId(shipperId))
        {
          MessageBox.Show("ShipperID が不正、もくしは、重複しています", "ERROR",
                          MessageBoxButtons.OK, MessageBoxIcon.Error);
          return;
        }
        ShipperID = shipperId;
        ShipperName = txtCompanyName.Text.Trim();
        Phone = txtPhone.Text.Trim();
        DialogResult = DialogResult.OK;
        this.Close();
        return;
      }
    }
    MessageBox.Show("入力値が正しくありません", "ERROR", 
                    MessageBoxButtons.OK, MessageBoxIcon.Error);
  }
  // .NET Framework 時 object? の ? 不要
  private void btnCancel_Click(object? sender, EventArgs e)
  {
    DialogResult = DialogResult.Cancel;
    this.Close();
  }
}

メインフォーム - 行追加/行削除

DataGridView に対する行追加、行削除を実装します。
下記サンプルコードには記載していませんが、必要に応じて、行追加/行削除 実施前に MessageBox で確認してください。

// DataGridView:行追加 - .NET Framework 時 object? の ? 不要
private void btnAppend_Click(object? sender, EventArgs e)
{
  // サブフォーム表示
  using (var dlg = new DlgAppend())
  {
    dlg.Owner = this;
    var result = dlg.ShowDialog();
    if (result == DialogResult.OK)
    {
      // DataGridView ではなく、バインドしている DataTable を直接参照
      if (dataGridView1.DataSource is BindingSource bindingSource
       && bindingSource.List is DataView boundView
       && boundView.Table is DataTable dataTable)
      {
        // 行追加
        dataTable.Rows.Add(dlg.ShipperID, dlg.ShipperName, dlg.Phone);
      }
    }
  }
}
// DataGridView:行削除 - .NET Framework 時 object? の ? 不要
private void buttonDelete_Click(object? sender, EventArgs e)
{
  if (dataGridView1.CurrentRow != null)
  {
    // DataGridView ではなく、バインドしている DataTable を直接参照
    if (dataGridView1.CurrentRow.DataBoundItem is DataRowView boundRow
     && boundRow.Row["ShipperID"] is int id)
    {
      // 行削除
      boundRow.Row.Delete();
      // DataRow.RowState = DataRowState.Deleted のデータは参照不可なので id を記録
      lstDelete.Add(id);
    }
  }
}

メインフォーム - データ更新

SQL Server への更新処理を実装します。

  • DELETE は List<int> lstDelete 参照
  • INSERT、UPDATE は DataTable 参照
// DataGridView:更新 - .NET Framework 時 object? の ? 不要
private void btnUpdate_Click(object? sender, EventArgs e)
{
  bool bCommit = false;
  // 削除の重複削除
  if (lstDelete.Count > 0)
  {
    lstDelete = lstDelete.Distinct().ToList();
  }
  // DataGridView ではなく、バインドしている DataTable を直接参照
  if (dataGridView1.DataSource is BindingSource bindingSource
   && bindingSource.List is DataView boundView
   && boundView.Table is DataTable dataTable)
  {
    int insert = 0;
    int update = 0;

    // 処理確認
    foreach (DataRow row in dataTable.Rows)
    {
      if (row.RowState == DataRowState.Added)
      {
        insert++;
      }
      else if (row.RowState == DataRowState.Modified)
      {
        update++;
      }
    }
    // 処理すべき行があるか確認
    if (lstDelete.Count == 0 && insert == 0 && update == 0)
    {
      MessageBox.Show("更新対象が存在しません", "ERROR",
                      MessageBoxButtons.OK, MessageBoxIcon.Error);
      return;
    }
    // データ更新処理
    using (var connection = new Microsoft.Data.SqlClient.SqlConnection(
                                GetConnectionString()))
    {
      connection.Open();
      var transaction = connection.BeginTransaction();

      try
      {
        // DELETE
        if (lstDelete.Count > 0)
        {
          var sql = "DELETE FROM Northwind.Shippers WHERE [ShipperID] = @Id";
          using (var command = new Microsoft.Data.SqlClient.SqlCommand(
                                   sql, connection, transaction))
          {
            foreach (var id in lstDelete)
            {
              command.Parameters.AddWithValue("@id", id);
              var affected = command.ExecuteNonQuery();
              if (affected == 0)
              {
                // TODO - DELETE されていない
              }
            }
          }
        }
        // INSERT
        if (insert > 0)
        {
          var sql =  "INSERT INTO Northwind.Shippers "
                   + "([ShipperID],[CompanyName],[Phone]) VALUES (@Id, @Name, @Phone)";
          using (var command = new Microsoft.Data.SqlClient.SqlCommand(
                                   sql, connection, transaction))
          {
            foreach (DataRow row in dataTable.Rows)
            {
              if (row.RowState == DataRowState.Added
               && row["ShipperID"] is int id)
              {
                var name = row["CompanyName"]?.ToString()?.Trim() ?? string.Empty;
                var phone = row["Phone"]?.ToString()?.Trim() ?? string.Empty;
                command.Parameters.AddWithValue("@id", id);
                command.Parameters.AddWithValue("@Name", name);
                command.Parameters.AddWithValue("@Phone", phone);
                var affected = command.ExecuteNonQuery();
                if (affected == 0)
                {
                  // TODO - INSERT されていない
                }
              }
            }
          }
        }
        // UPDATE
        if (update > 0)
        {
          var sql = "UPDATE Northwind.Shippers SET " 
                  + "[CompanyName] = @Name, [Phone] = @Phone WHERE [ShipperID] = @Id";
          using (var command = new Microsoft.Data.SqlClient.SqlCommand(
                                   sql, connection, transaction))
          {
            foreach (DataRow row in dataTable.Rows)
            {
              if (row.RowState == DataRowState.Modified
               && row["ShipperID"] is int id)
              {
                var name = row["CompanyName"]?.ToString()?.Trim() ?? string.Empty;
                var phone = row["Phone"]?.ToString()?.Trim() ?? string.Empty;
                command.Parameters.AddWithValue("@id", id);
                command.Parameters.AddWithValue("@Name", name);
                command.Parameters.AddWithValue("@Phone", phone);
                var affected = command.ExecuteNonQuery();
                if (affected == 0)
                {
                  // TODO - UPDATE されていない
                }
              }
            }
          }
        }
        // コミット
        transaction.Commit();
        bCommit = true;
      }
      catch (Exception)
      {
        // ロールバック
        transaction.Rollback();
        // TODO
      }
      finally
      {
        connection.Close();
      }
    }
  }
  if (bCommit)
  {
     MessageBox.Show("データ更新しました", "INFO",
                     MessageBoxButtons.OK, MessageBoxIcon.Information);
     // SQL Server からデータ再取得
     DataLoad();
  }
}

おまけ

DataGridView 表示件数が多いときには後述のような UI があると便利です。

現在行/全体件数 表示

現在行/全体件数 表示用に Label lblStatusCount を用意して、DataGridView.SelectionChange イベントを用いて表示更新します。

// .NET Framework 時 object? の ? 不要
private void DataGridView_SelectionChanged(object? sender, EventArgs e)
{
  if (sender is DataGridView dgv && dgv.CurrentRow is DataGridViewRow row)
  {
    UpdateStatusCount(dgv, row.Index);
  }
}
// 現在行/全体件数 表示更新
private void UpdateStatusCount(DataGridView dgv, int rowIndex)
{
  int rowMax = GetRealRowCount(dgv);
  if (rowMax > 0 && rowIndex < rowMax)
  {
    lblStatusCount.Text = $"{rowIndex + 1}/{rowMax} 件";
  }
  else
  {
    lblStatusCount.Text = "--/-- 件";
  }
}
// 行追加用の行を除外した件数取得
private int GetRealRowCount(DataGridView dgv)
{
  // AllowUserToAddRows が true/false どちらにも対応
  int rowMax = dgv.Rows.Count - (dgv.AllowUserToAddRows ? 1 : 0);
  return rowMax;
}

先頭行/末尾行に移動

先頭行、もしくは、末尾行を選択して、対象行を DataGridView に表示します。
(スクロールバーで表示範囲外となっているケースを考慮)

NavigateTopRow、NavigateBottomRow をボタン、メニューアイテムなどのクリックイベントで呼び出してください。

// 先頭行に移動
private void NavigateTopRow(DataGridView dgv)
{
  int rowMax = GetRealRowCount(dgv);
  if (rowMax > 0)
  {
    NavigateRowPosition(dgv, 0);
  }
}
// 末尾行に移動
private void NavigateBottomRow(DataGridView dgv)
{
  int rowMax = GetRealRowCount(dgv);
  if (rowMax > 0)
  {
    NavigateRowPosition(dgv, rowMax - 1);
  }
}
// 指定行を選択、選択行を表示
private void NavigateRowPosition(DataGridView dgv, int rowIndex)
{
  dgv.Rows[rowIndex].Cells[0].Selected = true;    // 選択行を更新
  dgv.FirstDisplayedScrollingRowIndex = rowIndex; // 対象行を表示
  dgv.Focus();

  // 現在行/全体件数 表示を実施している場合には、コメントを外す
  // UpdateStatusCount(dgv, rowIndex);
}

ソート処理時に選択行を追随

列ヘッダでソートした場合、選択されていた行ではなく、ソート後に同一位置の行が選択されます。
本記事サンプルは、ShipperID が Primary Key なので、DataGridView の CellMouseDown、ColumnHeaderMouseClick イベントを用いることで、選択行の追随が可能です。

private int CurrentRowKey = -1;
// .NET Framework 時 object? の ? 不要
private void DataGridView_CellMouseDown(object? sender, DataGridViewCellMouseEventArgs e)
{
  // 列ヘッダ (e.RowIndex:-1)  
  if (e.RowIndex < 0 && sender is DataGridView dgv
   && dgv.CurrentRow is DataGridViewRow row && !row.IsNewRow
   && row.Cells["ShipperID"]?.Value is int id)
  {
    CurrentRowKey = id;
    return;
  }
  CurrentRowKey = -1;
}
// .NET Framework 時 object? の ? 不要
private void DataGridView_ColumnHeaderMouseClick(object? sender,
                                                 DataGridViewCellMouseEventArgs e)
{
  if (sender is DataGridView dgv && CurrentRowKey >= 0)
  {
    foreach (DataGridViewRow row in dgv.Rows)
    {
      if (row.Cells["ShipperID"]?.Value is int id
       && id == CurrentRowKey)
      {
        NavigateRowPosition(dgv, row.Index);
        return;
      }
    }
  }
}

出典

本記事は、2025/04/15 Qiita 投稿記事の転載です。

Windows Forms C#定石 - DataGridView - DataTable

Discussion