Windows Formsを活用したCRUDの操作（作成、読み取り、更新、削除）

## 概要
C#とWindows Forms（WinForms）を使用して、基本的なCRUD（作成・読み取り・更新・削除）操作を実装する方法について解説します。DataGridView を活用し、ユーザーが直感的にデータを操作できるように設計されています。

また、一つ「実行」ボタンを1回クリックするだけで、すべてのCRUD処理（作成・更新・削除）が実行される 仕組みを採用しています。

## 開発環境
* Visual Studio 2022
* .NET Framework 4.8
* SQL Server

## 機能一覧
* 作成（Create）: 新しいファイルを追加
* 読み取り（Read）: 既存のファイル一覧を表示
* 更新（Update）: 備考欄を編集し、更新
* 削除（Delete）: 選択した行を削除
* 実行（Execute）: 追加・更新・削除を一括で適用

## 実装の流れ
### 1. UIの構築
DataGridView を用いてファイル情報を一覧表示し、ボタンを配置してCRUD操作を実装します。

```FileCRUD.cs
private void InitializeUI()
{
    this.Text = "ファイル管理";
    this.Size = new Size(932, 480);

    SetupButton(btnExecute, "実行", new Point(712, 37), btnExecute_Click);
    SetupButton(btnAddRow, "行追加", new Point(712, 66), btnAddRow_Click);
    SetupButton(btnDeleteRow, "行削除", new Point(712, 99), btnDeleteRow_Click);

    dataGridView.Location = new Point(32, 37);
    dataGridView.Size = new Size(602, 313);
    dataGridView.RowHeadersVisible = false;
    dataGridView.AllowUserToAddRows = false;
    dataGridView.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
    dataGridView.MultiSelect = false;
    dataGridView.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; 
    this.Controls.Add(dataGridView);
}

```
### 2. データの読み取り（Read）
データベースまたはファイルサービスからデータを取得し、DataGridView に表示します。
```FileCRUD.cs
private async Task LoadData()
{
    var data = await _fileServices.GetFile(string.Empty);
    foreach (DataRow row in data.Rows)
    {
        _dt.Rows.Add(row["FileNo"], row["FileName"], row["remarks"], row["fileData"]);
    }
}

```
### 3. ファイルの作成（Create）
新しいファイルを追加する機能を実装します。
```FileCRUD.cs
private void btnAddRow_Click(object sender, EventArgs e)
{
    using (OpenFileDialog openFileDialog = new OpenFileDialog())
    {
        openFileDialog.Filter = "ファイル (*.*)|*.*";
        openFileDialog.Title = "ファイル選択";

        if (openFileDialog.ShowDialog() == DialogResult.OK)
        {
            string filePath = openFileDialog.FileName;
            byte[] fileData = File.ReadAllBytes(filePath);
            string fileName = Path.GetFileName(filePath); 
            _dt.Rows.Add("", fileName, "", fileData);
            dataGridView.Rows[dataGridView.Rows.Count - 1].Selected = true;
        }
    }
}
```
* OpenFileDialog を用いて、ユーザーがローカルファイルを選択可能にします。
* File.ReadAllBytes を使ってファイルデータを取得し、DataTable に追加します。

### 4. ファイルの削除（Delete）
選択した行を削除する処理を実装します。
```FileCRUD.cs
private void btnDeleteRow_Click(object sender, EventArgs e)
{
    if (dataGridView.SelectedRows.Count > 0)
    {
        dataGridView.Rows.RemoveAt(dataGridView.SelectedRows[0].Index);
    }
}

```
* dataGridView.SelectedRows を確認し、選択された行を削除します。

### 5. すべての処理を一括実行（Execute）
「実行」ボタンをクリックすることで、作成（Insert）、更新（Update）、削除（Delete）を一括で適用 します。
```FileCRUD.cs
private async void btnExecute_Click(object sender, EventArgs e)
{
    foreach (DataRow row in _dt.Rows)
    {
        switch (row.RowState)
        {
            case DataRowState.Added:
                await _fileServices.InsertFile(row["fileName"].ToString(), (byte[])row["fileData"], row["remarks"].ToString());
                break;
            case DataRowState.Modified:
                await _fileServices.UpdateFile(int.Parse(row["fileNo"].ToString()), row["remarks"].ToString());
                break;
            case DataRowState.Deleted:
                await _fileServices.DeleteFile(int.Parse(row["fileNo", DataRowVersion.Original].ToString()));
                break;
        }
    }

    _dt.Rows.Clear();
    await LoadData();
    _dt.AcceptChanges();
}
```
* RowState を判定し、追加（Added）、更新（Modified）、削除（Deleted）を分類。
* 変更があったデータのみ適用するため、効率的に処理。
* LoadData() を再実行し、データを最新の状態に更新。

### FileServicesクラスの実装
```FileServices.cs
using System;
using System.Collections.Generic;
using System.Data;
using System.Text;
using System.Threading.Tasks;
using WinFormsDemo.Common;

namespace WinFormsDemo.Services
{
    /// <summary>
    /// ファイル操作に関連するサービスクラスです。
    /// </summary>
    public class FileServices
    {
        /// <summary>
        /// ファイル情報を取得します。
        /// </summary>
        /// <param name="where">SQLのWHERE句に追加する条件</param>
        /// <returns>ファイル情報を含むDataTable</returns>
        public async Task<DataTable> GetFile(string where)
        {
            try
            {
                // DB接続をラップするDBWrapperクラスを使用
                using (var dbWrapper = new DBWrapper())
                {
                    StringBuilder sqlCommand = new StringBuilder();

                    // SQLクエリを作成
                    sqlCommand.Append("SELECT * FROM FileMgmt ");
                    if (!string.IsNullOrEmpty(where))
                    {
                        sqlCommand.Append(where); // where句を条件に追加
                    }

                    // SQLクエリを実行して結果を返す
                    return await dbWrapper.ExecuteQuery(sqlCommand.ToString());
                }
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }
        }

        /// <summary>
        /// 新しいファイル情報をデータベースに挿入します。
        /// </summary>
        /// <param name="fileName">ファイル名</param>
        /// <param name="fileData">ファイルデータ</param>
        /// <param name="remarks">備考</param>
        /// <returns>非同期タスク</returns>
        public async Task InsertFile(string fileName, byte[] fileData, string remarks)
        {
            using (var dbWrapper = new DBWrapper())
            {
                try
                {
                    await dbWrapper.BeginTransaction(); // トランザクション開始

                    StringBuilder sqlCommand = new StringBuilder();
                    sqlCommand.Append("INSERT INTO FileMgmt (fileData, fileName, remarks) VALUES (@fileData, @fileName, @remarks)");

                    // パラメータを辞書で作成
                    var parameters = new Dictionary<string, object>
                    {
                        {"@fileData", fileData},
                        {"@fileName", fileName},
                        {"@remarks", remarks},
                    };

                    // SQLコマンドを実行
                    await dbWrapper.ExecuteNonQuery(sqlCommand.ToString(), parameters);
                    await dbWrapper.CommitTransaction(); // コミット
                }
                catch (Exception ex)
                {
                    // エラー処理
                    Console.WriteLine(ex.Message);
                    await dbWrapper.RollbackTransaction(); // ロールバック
                }
            }
        }

        /// <summary>
        /// ファイル情報を更新します。
        /// </summary>
        /// <param name="fileNo">ファイル番号</param>
        /// <param name="remarks">新しい備考</param>
        /// <returns>非同期タスク</returns>
        public async Task UpdateFile(int fileNo, string remarks)
        {
            using (var dbWrapper = new DBWrapper())
            {
                try
                {
                    await dbWrapper.BeginTransaction(); // トランザクション開始

                    StringBuilder sqlCommand = new StringBuilder();
                    sqlCommand.Append("UPDATE FileMgmt SET remarks = @remarks WHERE fileNo = @fileNo");

                    // パラメータを辞書で作成
                    var parameters = new Dictionary<string, object>
                    {
                        {"@fileNo", fileNo},
                        {"@remarks", remarks},
                    };

                    // SQLコマンドを実行
                    await dbWrapper.ExecuteNonQuery(sqlCommand.ToString(), parameters);
                    await dbWrapper.CommitTransaction(); // コミット
                }
                catch (Exception ex)
                {
                    // エラー処理
                    Console.WriteLine(ex.Message);
                    await dbWrapper.RollbackTransaction(); // ロールバック
                }
            }
        }

        /// <summary>
        /// 指定したファイル情報を削除します。
        /// </summary>
        /// <param name="fileNo">削除するファイル番号</param>
        /// <returns>非同期タスク</returns>
        public async Task DeleteFile(int fileNo)
        {
            using (var dbWrapper = new DBWrapper())
            {
                try
                {
                    await dbWrapper.BeginTransaction(); // トランザクション開始

                    StringBuilder sqlCommand = new StringBuilder();
                    sqlCommand.Append("DELETE FROM FileMgmt WHERE fileNo = @fileNo");

                    // パラメータを辞書で作成
                    var parameters = new Dictionary<string, object>
                    {
                        {"@fileNo", fileNo},
                    };

                    // SQLコマンドを実行
                    await dbWrapper.ExecuteNonQuery(sqlCommand.ToString(), parameters);
                    await dbWrapper.CommitTransaction(); // コミット
                }
                catch (Exception ex)
                {
                    // エラー処理
                    Console.WriteLine(ex.Message);
                    await dbWrapper.RollbackTransaction(); // ロールバック
                }
            }
        }
    }
}
```

### DBwrapperクラスの実装
```DBWrapper.cs
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Data;
using System.Text;
using System.Threading.Tasks;
using System.Configuration;

namespace WinFormsDemo.Common
{
    /// <summary>
    /// データベース接続をラップするクラスです。
    /// SQLクエリの実行、トランザクションの管理、接続のクローズなどを行います。
    /// </summary>
    public class DBWrapper : IDisposable
    {
        private string _connectionString;
        private readonly SqlConnection _connection;
        private SqlTransaction _transaction;

        /// <summary>
        /// デフォルトの接続文字列を使用してDBWrapperのインスタンスを初期化します。
        /// </summary>
        public DBWrapper() : this(ConfigurationManager.ConnectionStrings["DBConnectionString"].ToString()) { }

        /// <summary>
        /// 指定した接続文字列を使用してDBWrapperのインスタンスを初期化します。
        /// </summary>
        /// <param name="connectionString">接続文字列</param>
        public DBWrapper(string connectionString)
        {
            _connection = new SqlConnection(connectionString);
            _connectionString = connectionString;
        }

        /// <summary>
        /// 新しいSQL接続を作成します。
        /// </summary>
        /// <returns>SqlConnectionのインスタンス</returns>
        private SqlConnection CreateConnection()
        {
            return new SqlConnection(_connectionString);
        }

        /// <summary>
        /// SQLクエリを非同期で実行し、結果をDataTableとして返します。
        /// </summary>
        /// <param name="sql">実行するSQLクエリ</param>
        /// <param name="parameters">SQLパラメータ（任意）</param>
        /// <returns>クエリ結果を含むDataTable</returns>
        public async Task<DataTable> ExecuteQuery(string sql, SqlParameter[] parameters = null)
        {
            using (var connection = CreateConnection())
            {
                using (var command = new SqlCommand(sql, connection))
                {
                    if (parameters != null)
                    {
                        command.Parameters.AddRange(parameters);
                    }

                    DataTable dataTable = new DataTable();
                    using (var adapter = new SqlDataAdapter(command))
                    {
                        adapter.Fill(dataTable); // データをDataTableに埋め込む
                    }
                    return dataTable;
                }
            }
        }

        /// <summary>
        /// SQLコマンドを非同期で実行し、影響を受けた行数を返します。
        /// </summary>
        /// <param name="sql">実行するSQLクエリ</param>
        /// <param name="parameters">SQLパラメータ（任意）</param>
        /// <returns>影響を受けた行数</returns>
        public async Task<int> ExecuteNonQuery(string sql, Dictionary<string, object> parameters = null)
        {
            using (var command = _connection.CreateCommand())
            {
                command.Transaction = _transaction;  // トランザクションを関連付け

                command.CommandText = sql;

                if (parameters != null)
                {
                    // パラメータをコマンドに追加
                    foreach (var param in parameters)
                    {
                        var dbParam = command.CreateParameter();
                        dbParam.ParameterName = param.Key;
                        dbParam.Value = param.Value ?? DBNull.Value;
                        command.Parameters.Add(dbParam);
                    }
                }

                // 非同期でSQLコマンドを実行
                return await command.ExecuteNonQueryAsync();
            }
        }

        /// <summary>
        /// トランザクションを開始します。
        /// </summary>
        /// <returns>非同期タスク</returns>
        public async Task BeginTransaction()
        {
            if (_connection.State == ConnectionState.Closed)
            {
                await _connection.OpenAsync(); // 接続を開く
            }

            if (_transaction == null)
            {
                _transaction = _connection.BeginTransaction(); // トランザクション開始
            }
        }

        /// <summary>
        /// トランザクションをコミットします。
        /// </summary>
        /// <returns>非同期タスク</returns>
        public async Task CommitTransaction()
        {
            try
            {
                await Task.Run(() => _transaction.Commit()); // トランザクションをコミット
            }
            finally
            {
                await DisposeTransactionAsync(); // トランザクションの破棄
            }
        }

        /// <summary>
        /// トランザクションをロールバックします。
        /// </summary>
        /// <returns>非同期タスク</returns>
        public async Task RollbackTransaction()
        {
            if (_transaction == null)
            {
                throw new InvalidOperationException("トランザクションが開始されていません。");
            }

            try
            {
                await Task.Run(() => _transaction.Rollback()); // トランザクションをロールバック
            }
            finally
            {
                await DisposeTransactionAsync(); // トランザクションの破棄
            }
        }

        /// <summary>
        /// トランザクションのリソースを非同期で破棄します。
        /// </summary>
        /// <returns>非同期タスク</returns>
        private async Task DisposeTransactionAsync()
        {
            if (_transaction != null)
            {
                await Task.Run(() => _transaction.Dispose()); // トランザクションを破棄
                _transaction = null;
            }
        }

        /// <summary>
        /// DBWrapperのリソースを解放します。
        /// </summary>
        public void Dispose()
        {
            try
            {
                _transaction?.Dispose(); // トランザクションを破棄
                _connection?.Dispose();  // 接続を破棄
            }
            catch (Exception ex)
            {
                Console.WriteLine("リソースの解放中にエラーが発生しました: " + ex.Message);
            }
        }
    }
}
```
### 結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951101/18113cb5-1126-47e8-83ec-0d973bf4d017.png)


## おわりに
Windows Formsを活用したCRUD操作（作成・読み取り・更新・削除）の基本的な実装方法を紹介しました。
特に、「実行ボタンをクリックするだけでCRUD処理を一括適用できる」 仕様となっており、シンプルな操作性を実現しています。
この実装を参考に、実際の業務に役立つアプリケーションを作成してみてください！
**参考：**
* DataGridView の公式ドキュメント:  [Microsoft](https://learn.microsoft.com/ja-jp/dotnet/desktop/winforms/controls/datagridview-control-windows-forms?view=netframeworkdesktop-4.8)

* OpenFileDialog の使い方: [Microsoft Docs](https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.openfiledialog?view=windowsdesktop-9.0)