Skip to content

第25章 Windowsフォーム社員管理アプリ:編集・更新

この章では、第 23・24 章で作った社員管理アプリに 編集機能 を追加し、第 23 章から枠だけ用意していた 新規登録・削除 も完成させます。

ここまでで一覧表示(Read)と検索ができるようになっていますが、業務アプリとしての CRUD(Create / Read / Update / Delete)はまだ Read だけです。この章で残りの 3 つを実装し、Windows フォーム社員管理アプリを 業務アプリの最小完成形 にします。

操作SQL
Create(新規登録)INSERTこの章
Read(一覧・検索)SELECT第 23・24 章
Update(更新)UPDATEこの章
Delete(削除)DELETEこの章

加えて、この章で初めて 複数のフォーム間の画面遷移 を扱います。一覧画面で行を選んで「編集」を押すと 別のフォーム(編集ダイアログ) が開き、入力して保存すると一覧に戻って自動で再読み込みされる、という現場で頻出のパターンです。


この章でできるようになること

Section titled “この章でできるようになること”

この章を終えると、次のことができるようになります。

  • INSERT / UPDATE / DELETE 文をパラメータ化クエリで書ける
  • IDENTITY で採番された ID を取得できる(OUTPUT INSERTED.column_name)
  • SqlCommand.ExecuteNonQuery の戻り値(影響行数)の意味を説明できる
  • 別のフォームを ShowDialog でモーダル表示できる
  • フォーム間でデータを受け渡せる(コンストラクタ + プロパティ)
  • DialogResult で OK / キャンセルを判定できる
  • 入力チェックを書ける(必須項目、数値、日付)
  • 削除前の確認ダイアログを出せる
  • 一覧画面に戻ったときに 自動で再読み込み する処理を書ける

項目内容
開発環境Visual Studio 2022
プロジェクト種類Windows フォーム アプリ
対象フレームワーク.NET 8
ソリューション名KadaiWinFormsApp(第 23 章で作成済み・続けて使う)
プロジェクト名EmployeeApp(第 23・24 章の続き・作り直さない)
ベースとなる章第 24 章
データベースSQLServer 2022(TrainingDB)
認証方式Windows 統合認証
NuGet パッケージMicrosoft.Data.SqlClient

この章も第 23・24 章の EmployeeApp を続けて使います

新しいプロジェクトは作りません。KadaiWinFormsApp ソリューションの EmployeeApp を開き、編集ダイアログ(別フォーム)と CRUD のイベントを足していきます。

Server=localhost で接続できないとき

第 23 章と同じく、SQLServer のインスタンス名によっては Server=localhost\SQLEXPRESS 等の指定が必要です。詳しくは第 23 章「23-4 接続文字列を設定する」の補足を参照してください。


  • 第 24 章を Git に提出済み、または手元にコードがある
  • TrainingDBemployees / departments が動作している
  • 第 24 章の EmployeeRepository.Search が動くことを確認した

第 23・24 章のアプリに、次の機能を追加します。

┌──────────────────────────────────────────────────────────────┐
│ 社員一覧 [再読込]│
├──────────────────────────────────────────────────────────────┤
│ 名前:[ ] 部署:[ (すべての部署) ▼ ] [検索] [クリア]│
├──────────────────────────────────────────────────────────────┤
│ EmployeeId │ LastName │ FirstName │ Email │ Salary │
├────────────┼──────────┼───────────┼─────────────────┼────────┤
│ 1001 │ 山田 │ 二郎 │ yamada.jiro@... │ 500000 │ ← 行をダブルクリックで編集
│ ... │
├──────────────────────────────────────────────────────────────┤
│ [新規登録] [編集] [削除] │ ← 全て有効化
└──────────────────────────────────────────────────────────────┘

機能の追加点:

機能操作
新規登録「新規登録」ボタン → 編集ダイアログが空欄で開く → 保存で INSERT
編集行を選んで「編集」ボタン(または行をダブルクリック) → ダイアログに値が入って開く → 保存で UPDATE
削除行を選んで「削除」ボタン → 確認ダイアログ → OK で DELETE
保存後の更新ダイアログを閉じたら、一覧画面が自動で再読み込み

25-2 EmployeeRepository に Insert / Update / Delete を実装する

Section titled “25-2 EmployeeRepository に Insert / Update / Delete を実装する”

第 23 章で NotImplementedException を返していた InsertDelete を実装し、新しく Update を追加します。

EmployeeRepository.cs
namespace EmployeeApp;
using Microsoft.Data.SqlClient;
using System.Collections.Generic;
public class EmployeeRepository
{
private readonly string _connectionString;
public EmployeeRepository(string connectionString)
{
_connectionString = connectionString;
}
public List<Employee> GetAll()
{
const string sql = @"
SELECT
e.employee_id, e.last_name, e.first_name, e.email,
e.hire_date, e.salary, e.department_id, d.department_name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id
ORDER BY e.employee_id";
using SqlConnection connection = new SqlConnection(_connectionString);
connection.Open();
using SqlCommand command = new SqlCommand(sql, connection);
return ReadEmployees(command);
}
public List<Employee> Search(string keyword, int departmentId)
{
const string sql = @"
SELECT
e.employee_id, e.last_name, e.first_name, e.email,
e.hire_date, e.salary, e.department_id, d.department_name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id
WHERE
(@keyword = ''
OR e.last_name LIKE '%' + @keyword + '%'
OR e.first_name LIKE '%' + @keyword + '%')
AND (@departmentId = -1 OR e.department_id = @departmentId)
ORDER BY e.employee_id";
using SqlConnection connection = new SqlConnection(_connectionString);
connection.Open();
using SqlCommand command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@keyword", keyword ?? string.Empty);
command.Parameters.AddWithValue("@departmentId", departmentId);
return ReadEmployees(command);
}
public int Insert(Employee employee)
{
const string sql = @"
INSERT INTO employees (last_name, first_name, email, hire_date, salary, department_id)
OUTPUT INSERTED.employee_id
VALUES (@lastName, @firstName, @email, @hireDate, @salary, @departmentId)";
using SqlConnection connection = new SqlConnection(_connectionString);
connection.Open();
using SqlCommand command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@lastName", employee.LastName);
command.Parameters.AddWithValue("@firstName", employee.FirstName);
command.Parameters.AddWithValue("@email", string.IsNullOrEmpty(employee.Email) ? (object)DBNull.Value : employee.Email);
command.Parameters.AddWithValue("@hireDate", employee.HireDate);
command.Parameters.AddWithValue("@salary", employee.Salary);
command.Parameters.AddWithValue("@departmentId", employee.DepartmentId);
int newId = (int)command.ExecuteScalar();
return newId;
}
public int Update(Employee employee)
{
const string sql = @"
UPDATE employees
SET last_name = @lastName,
first_name = @firstName,
email = @email,
hire_date = @hireDate,
salary = @salary,
department_id = @departmentId
WHERE employee_id = @employeeId";
using SqlConnection connection = new SqlConnection(_connectionString);
connection.Open();
using SqlCommand command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@employeeId", employee.EmployeeId);
command.Parameters.AddWithValue("@lastName", employee.LastName);
command.Parameters.AddWithValue("@firstName", employee.FirstName);
command.Parameters.AddWithValue("@email", string.IsNullOrEmpty(employee.Email) ? (object)DBNull.Value : employee.Email);
command.Parameters.AddWithValue("@hireDate", employee.HireDate);
command.Parameters.AddWithValue("@salary", employee.Salary);
command.Parameters.AddWithValue("@departmentId", employee.DepartmentId);
int affected = command.ExecuteNonQuery();
return affected;
}
public int Delete(int employeeId)
{
const string sql = "DELETE FROM employees WHERE employee_id = @employeeId";
using SqlConnection connection = new SqlConnection(_connectionString);
connection.Open();
using SqlCommand command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@employeeId", employeeId);
int affected = command.ExecuteNonQuery();
return affected;
}
private static List<Employee> ReadEmployees(SqlCommand command)
{
List<Employee> list = new List<Employee>();
using SqlDataReader reader = command.ExecuteReader();
int idxId = reader.GetOrdinal("employee_id");
int idxLast = reader.GetOrdinal("last_name");
int idxFirst = reader.GetOrdinal("first_name");
int idxEmail = reader.GetOrdinal("email");
int idxHire = reader.GetOrdinal("hire_date");
int idxSalary = reader.GetOrdinal("salary");
int idxDeptId = reader.GetOrdinal("department_id");
int idxDeptName = reader.GetOrdinal("department_name");
while (reader.Read())
{
list.Add(new Employee
{
EmployeeId = reader.GetInt32(idxId),
LastName = reader.GetString(idxLast),
FirstName = reader.GetString(idxFirst),
Email = reader.IsDBNull(idxEmail) ? string.Empty : reader.GetString(idxEmail),
HireDate = reader.GetDateTime(idxHire),
Salary = reader.IsDBNull(idxSalary) ? 0m : reader.GetDecimal(idxSalary),
DepartmentId = reader.IsDBNull(idxDeptId) ? 0 : reader.GetInt32(idxDeptId),
DepartmentName = reader.IsDBNull(idxDeptName) ? string.Empty : reader.GetString(idxDeptName)
});
}
return list;
}
}
メソッド戻り値ポイント
Insert採番された employee_idOUTPUT INSERTED.employee_id で、INSERT 直後に IDENTITY 値を取得できる
Update影響行数(0 か 1)ExecuteNonQuery は INSERT/UPDATE/DELETE で使う。戻り値は処理された行数
Delete影響行数(0 か 1)WHERE employee_id = @employeeId必ず付ける(付け忘れると全件削除)

ExecuteScalar / ExecuteReader / ExecuteNonQuery の使い分け

Section titled “ExecuteScalar / ExecuteReader / ExecuteNonQuery の使い分け”

ここまでで 3 種類のメソッドが出てきました。

メソッド想定する SQL戻り値
ExecuteScalar1 行 1 列が返る SELECT(COUNTMAX 等)、OUTPUT 付き INSERT先頭セルの値(object)
ExecuteReader複数行が返る SELECTSqlDataReader
ExecuteNonQueryINSERT / UPDATE / DELETE などデータ変更系影響行数(int)

InsertExecuteScalar を使っているのは、OUTPUT INSERTED.employee_id1 つの値(採番された ID)を返してもらっているからです。

email 列は NULL を許容します。C# の stringnullAddWithValue に渡すと、内部で パラメータが省略された ように扱われてしまうことがあるため、明示的に DBNull.Value を渡します。

command.Parameters.AddWithValue("@email",
string.IsNullOrEmpty(employee.Email) ? (object)DBNull.Value : employee.Email);

(object) キャストは、三項演算子の戻り値の型を object に揃えるためです(DBNull.Valuestring を直接並べると型推論で混乱します)。


25-3 編集ダイアログ(EmployeeEditForm)を作る

Section titled “25-3 編集ダイアログ(EmployeeEditForm)を作る”

社員 1 件分の入力フォームを 別フォーム として作ります。 新規登録と編集の両方で使い回します(モードはコンストラクタで切り替え)。

ソリューションエクスプローラーで EmployeeApp プロジェクトを右クリック → 追加 → 新しい項目 → 左ペインで「Windows フォーム」を選択 → 「フォーム(Windows フォーム)」 → 名前を EmployeeEditForm.cs にして追加します。 追加すると、EmployeeEditForm.csEmployeeEditForm.Designer.csEmployeeEditForm.resx の 3 つが自動生成されます(Form1 と同じ仕組み)。

ラベルと入力欄を縦に並べ、いちばん下に「保存」「キャンセル」ボタンを置きます。完成イメージは次のとおりです。

┌─ 社員 編集 ──────────────────────────┐
│ │
│ 姓 [___________________________]│
│ 名 [___________________________]│
│ メール [___________________________]│
│ 入社日 [______________________ ▼] │
│ 給与 [_______________] │
│ 部署 [______________________ ▼] │
│ │
│ [ 保存 ] [キャンセル]│
│ │
└───────────────────────────────────────┘

ツールボックスから次のコントロールをフォームに配置し、プロパティ ウィンドウで下表のとおり設定します。Location(左上からの位置)と Size(大きさ)は座標(X, Y)で指定 すると、上のイメージのきれいに揃った配置になります。ラベルの (Name) は既定のままで構いません(表では分かりやすいように名前を付けています)。

コントロール(Name)TextLocationSizeその他のプロパティ
FormEmployeeEditForm社員 編集420, 380FormBorderStyle = FixedDialogMaximizeBox = falseMinimizeBox = falseStartPosition = CenterParent
LabellabelLastName24, 2472, 23
TextBoxtextBoxLastName(空)110, 21270, 23
LabellabelFirstName24, 6072, 23
TextBoxtextBoxFirstName(空)110, 57270, 23
LabellabelEmailメール24, 9672, 23
TextBoxtextBoxEmail(空)110, 93270, 23
LabellabelHireDate入社日24, 13272, 23
DateTimePickerdateTimePickerHireDate110, 129270, 23Format = Short
LabellabelSalary給与24, 16872, 23
NumericUpDownnumericUpDownSalary110, 165150, 23Maximum = 99999999Minimum = 0DecimalPlaces = 0ThousandsSeparator = true
LabellabelDepartment部署24, 20472, 23
ComboBoxcomboBoxDepartment110, 201270, 24DropDownStyle = DropDownList
ButtonbuttonSave保存196, 30088, 30DialogResult = OK
ButtonbuttonCancelキャンセル292, 30088, 30DialogResult = Cancel

座標はあくまで目安です。 多少ずれても動作には影響しません。まずは上の値で置いてみて、見た目が気になれば後から微調整してください。Label は文字がはみ出すようなら Size の幅を少し広げます。

buttonCancel.DialogResultCancel にしておくと、ボタンを押すだけで ShowDialog()DialogResult.Cancel を返してフォームが閉じます(コードは不要)。同様に buttonSave.DialogResultOK にしておくと、保存処理が通ったときに OK が返ります(入力チェックで止めたいときは、後述のコードで DialogResult を一時的に打ち消します)。

このフォームも、ヘルパー(自分で呼ぶ部品)を先に用意 → イベントを紐付けて中身を書く の順で組み立てます。

ソースを丸ごと貼り付けるのではなく、ステップごとに自分の手で組み立てます。完成形(答え合わせ用)は 25-7 にあります。先に見ず、まず自分で積み上げてみてください。

ステップ1 フィールドとコンストラクタ(新規/編集の切り替え)

Section titled “ステップ1 フィールドとコンストラクタ(新規/編集の切り替え)”

このダイアログは 新規登録と編集の両方 で使い回します。編集対象 _targetnull なら新規モード、そうでなければ編集モードです。

EmployeeEditForm.cs
namespace EmployeeApp;
using Microsoft.Data.SqlClient;
public partial class EmployeeEditForm : Form
{
private readonly EmployeeRepository _employeeRepository;
private readonly DepartmentRepository _departmentRepository;
// 編集対象。新規モードのときは null。
private readonly Employee _target;
// _target が null かどうかで「新規モード」を判定する
private bool IsNewMode => _target == null;
public EmployeeEditForm(
EmployeeRepository employeeRepository,
DepartmentRepository departmentRepository,
Employee target)
{
InitializeComponent();
_employeeRepository = employeeRepository;
_departmentRepository = departmentRepository;
_target = target;
}
}
書いたもの意図
_target フィールド編集対象の社員。null なら新規登録モード
IsNewMode プロパティ_target == null で新規/編集を 1 か所で判定
Repository をコンストラクタで受け取るダイアログが直接 DB を触らず、親と同じ Repository を使い回す

ビルドの状態:通ります。

ステップ2 入力チェックのヘルパー(ValidateInput)

Section titled “ステップ2 入力チェックのヘルパー(ValidateInput)”

保存前に、必須項目やメール形式をチェックする部品を先に用意します。boolout string error で「OK か」+「NG の理由」を一緒に返すパターンです。

private bool ValidateInput(out string error)
{
if (string.IsNullOrWhiteSpace(textBoxLastName.Text))
{
error = "「姓」は必須です。";
return false;
}
if (string.IsNullOrWhiteSpace(textBoxFirstName.Text))
{
error = "「名」は必須です。";
return false;
}
string email = textBoxEmail.Text.Trim();
if (!string.IsNullOrEmpty(email) && !email.Contains('@'))
{
error = "「メール」は @ を含む形式で入力してください。";
return false;
}
if (comboBoxDepartment.SelectedValue == null)
{
error = "「部署」を選択してください。";
return false;
}
error = string.Empty;
return true;
}

メール形式は @ が含まれるかだけの「ざっくり」チェックで十分です(厳密には正規表現が必要)。

ステップ3 ダイアログを開いたときの初期化(Load イベント)

Section titled “ステップ3 ダイアログを開いたときの初期化(Load イベント)”

イベントの紐付け:デザイナでフォームの空白部分をクリック → 稲妻アイコン → Load をダブルクリック。

EmployeeEditForm_Load の中で、部署プルダウンを用意し、編集モードなら既存の値を画面に流し込みます。

private void EmployeeEditForm_Load(object sender, EventArgs e)
{
Text = IsNewMode ? "社員 新規登録" : $"社員 編集 (ID:{_target.EmployeeId})";
// 部署プルダウンを初期化
List<Department> departments = _departmentRepository.GetAll();
comboBoxDepartment.DataSource = departments;
comboBoxDepartment.DisplayMember = nameof(Department.DepartmentName);
comboBoxDepartment.ValueMember = nameof(Department.DepartmentId);
// 既存値を画面に流し込む(編集モード)
if (!IsNewMode)
{
textBoxLastName.Text = _target.LastName;
textBoxFirstName.Text = _target.FirstName;
textBoxEmail.Text = _target.Email;
dateTimePickerHireDate.Value = _target.HireDate;
numericUpDownSalary.Value = _target.Salary;
comboBoxDepartment.SelectedValue = _target.DepartmentId;
}
else
{
// 新規モード:今日を入社日のデフォルトに
dateTimePickerHireDate.Value = DateTime.Today;
}
}

ステップ4 保存ボタン(buttonSave の Click)

Section titled “ステップ4 保存ボタン(buttonSave の Click)”

イベントの紐付け:デザイナで buttonSaveダブルクリックbuttonCancelコードを書きません(DialogResult プロパティを Cancel に設定するだけで閉じます)。

保存ボタンでは、①入力チェック → ②画面の値から Employee を組み立てる → ③新規なら Insert、編集なら Update を呼びます。新規と編集はこの保存処理で 分かれます。流れを図にすると次のとおりです。

入力エラーや更新失敗のときは DialogResult = None にして フォームを閉じない(入力をやり直せる)のがポイントです。Insert/UpdateSqlException が起きた場合も、catch で DB エラーを表示して DialogResult = None にします(図では省略)。

private void buttonSave_Click(object sender, EventArgs e)
{
if (!ValidateInput(out string error))
{
MessageBox.Show(error, "入力チェック", MessageBoxButtons.OK, MessageBoxIcon.Warning);
DialogResult = DialogResult.None; // フォームを閉じない
return;
}
Employee employee = new Employee
{
EmployeeId = IsNewMode ? 0 : _target.EmployeeId,
LastName = textBoxLastName.Text.Trim(),
FirstName = textBoxFirstName.Text.Trim(),
Email = textBoxEmail.Text.Trim(),
HireDate = dateTimePickerHireDate.Value.Date,
Salary = numericUpDownSalary.Value,
DepartmentId = (int)comboBoxDepartment.SelectedValue
};
try
{
if (IsNewMode)
{
int newId = _employeeRepository.Insert(employee);
MessageBox.Show($"新規登録しました(ID:{newId})。", "完了", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
else
{
int affected = _employeeRepository.Update(employee);
if (affected == 0)
{
MessageBox.Show("更新対象の社員が見つかりませんでした。", "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning);
DialogResult = DialogResult.None;
return;
}
MessageBox.Show("更新しました。", "完了", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
// 入力チェックを通り保存できたら、DialogResult = OK のままフォームが閉じる
}
catch (SqlException ex)
{
MessageBox.Show(
$"SQLServer 関連のエラーが発生しました。\nエラー番号:{ex.Number}\nメッセージ:{ex.Message}",
"DB エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
DialogResult = DialogResult.None;
}
}
ポイント説明
_target == null で新規モードを判定1 つのフォームで新規/編集を兼ねる(コードが半分で済む)
DialogResult = DialogResult.None入力エラー時にフォームを 閉じさせない テクニック
EmployeeId = IsNewMode ? 0 : _target.EmployeeId新規は 0(採番は DB 任せ)、編集は既存 ID
NumericUpDown / DateTimePicker給与・入社日は専用部品で誤入力を防ぐ

第 24 章で作った検索つきの Form1土台 にして、新規登録・編集・削除のイベントを足していきます。土台がない場合は、第 24 章の Form1.cs(24-9 の完成形)を用意してから始めてください。

ソースを丸ごと貼り付けるのではなく、ステップごとに自分の手で組み立てます。完成形(答え合わせ用)は 25-7 にあります。先に見ず、まず自分で積み上げてみてください。

追加点操作
buttonNew を有効化デザイナで Enabledtrue に変更
buttonEdit を新設Button を 1 つ追加。Name = buttonEditText = 編集Location = 580, 462Size = 90, 28Anchor = Bottom, Right(buttonNew の左隣)
buttonDelete を有効化デザイナで Enabledtrue に変更

イベントの紐付けは、次の各ステップで機能を書くのと一緒に行います。

ステップ1 選択中の社員を取り出すヘルパー(GetSelectedEmployee)

Section titled “ステップ1 選択中の社員を取り出すヘルパー(GetSelectedEmployee)”

編集・削除では「いま一覧で選ばれている社員」が必要です。これを取り出す部品を先に用意します。

private Employee GetSelectedEmployee()
{
if (dataGridViewEmployees.CurrentRow == null)
{
return null;
}
return dataGridViewEmployees.CurrentRow.DataBoundItem as Employee;
}

DataBoundItem の便利さ

CurrentRow.DataBoundItem as Employee は「選択中の行に紐づいた 元データ(Employee) を取り出す」書き方です。DataSourceList<Employee> をバインドしているので、セルを 1 つずつ読み直さなくても Employee をそのまま取り出せます。

ステップ2 新規登録ボタン(buttonNew の Click)

Section titled “ステップ2 新規登録ボタン(buttonNew の Click)”

イベントの紐付け:デザイナで buttonNewダブルクリック

編集ダイアログを 新規モード(target: null)で開き、保存(DialogResult.OK)されたら一覧を再読み込みします。

新規登録と編集は ほとんど同じ流れ です(違いは「開く前に行の選択が要るか」と、target に渡す相手だけ)。次の図でまとめて見ておきましょう。

ダイアログを開いたあとの 保存処理の中身(入力チェック → Insert / Update)は、25-3 ステップ4 の「保存ボタンの流れ」の図を参照してください。ここ(Form1 側)は「ダイアログを開く → OK なら再読み込み」までが担当です。

private void buttonNew_Click(object sender, EventArgs e)
{
using EmployeeEditForm dialog = new EmployeeEditForm(_employeeRepository, _departmentRepository, target: null);
if (dialog.ShowDialog(this) == DialogResult.OK)
{
LoadEmployees();
}
}

ShowDialog のポイント

ShowDialog(this)モーダル表示(編集中は親画面を操作できない)。using を付けると、閉じたときに自動で Dispose(リソース解放)されます。== DialogResult.OK のときだけ一覧を再読み込みします。

ステップ3 編集ボタン(buttonEdit の Click)

Section titled “ステップ3 編集ボタン(buttonEdit の Click)”

イベントの紐付け:デザイナで buttonEditダブルクリック

選択中の社員を取り出し、編集モード(target: selected)でダイアログを開きます。未選択なら案内を出して終了します。流れはステップ2の「新規登録・編集の流れ」の図の 編集側 にあたります。

private void buttonEdit_Click(object sender, EventArgs e)
{
Employee selected = GetSelectedEmployee();
if (selected == null)
{
MessageBox.Show("編集する社員を選んでください。", "情報", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
using EmployeeEditForm dialog = new EmployeeEditForm(_employeeRepository, _departmentRepository, target: selected);
if (dialog.ShowDialog(this) == DialogResult.OK)
{
LoadEmployees();
}
}

ステップ4 削除ボタン(buttonDelete の Click)

Section titled “ステップ4 削除ボタン(buttonDelete の Click)”

イベントの紐付け:デザイナで buttonDeleteダブルクリック

選択チェック → 確認ダイアログ → OK なら Delete を呼び、一覧を再読み込みします。

削除は事故が起きやすい操作なので、コードを書く前に 処理の流れ(どこで止めて、どこで分岐するか) を図で押さえておきましょう。

図の ひし形(◇)が分岐 です。「未選択なら止める」「確認でキャンセルなら止める」「WHERE 付きの DELETE」「影響行数で成否を判断」という、削除を安全に行うための要所が分岐になっています。なお Delete の呼び出しで SqlException が起きた場合は、catch でまとめて ShowError に渡します(図では省略)。

private void buttonDelete_Click(object sender, EventArgs e)
{
Employee selected = GetSelectedEmployee();
if (selected == null)
{
MessageBox.Show("削除する社員を選んでください。", "情報", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
DialogResult result = MessageBox.Show(
$"社員番号 {selected.EmployeeId}({selected.FullName})を削除します。よろしいですか?",
"削除確認",
MessageBoxButtons.OKCancel,
MessageBoxIcon.Warning);
if (result != DialogResult.OK)
{
return;
}
try
{
int affected = _employeeRepository.Delete(selected.EmployeeId);
if (affected == 0)
{
MessageBox.Show("削除対象の社員が見つかりませんでした。", "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
else
{
MessageBox.Show("削除しました。", "完了", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
LoadEmployees();
}
catch (SqlException ex)
{
ShowError(ex);
}
}

WHERE 句を絶対に忘れない

Delete の SQL(25-2)には WHERE employee_id = @employeeId が必ず付いています。これを書き忘れると 全件削除 になります。削除前に確認ダイアログを出しているのも、事故を防ぐためです。

ステップ5 行のダブルクリックでも編集(CellDoubleClick)

Section titled “ステップ5 行のダブルクリックでも編集(CellDoubleClick)”

イベントの紐付け:デザイナで dataGridViewEmployees をクリック → 稲妻アイコン → CellDoubleClick をダブルクリック。

行をダブルクリックしたら、編集ボタンと同じ処理を呼びます。列ヘッダー(e.RowIndex < 0)は無視します。

private void dataGridViewEmployees_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex < 0)
{
// 列ヘッダーがダブルクリックされた場合
return;
}
buttonEdit_Click(sender, e);
}

ここまでで CRUD がそろいます。実行(F5)して、次の 25-5 のシナリオで確かめてください


次のシナリオで動作を確認します。

シナリオ期待する結果
新規登録ボタン → 全項目入力 → 保存一覧に追加され、新規 ID が採番されて表示
新規登録ボタン → 姓を空のまま保存入力エラーで閉じられない、警告メッセージ
行をダブルクリック → 値を変更 → 保存一覧に変更が反映される
編集中にキャンセル変更されない
削除ボタン → OK一覧から消える
削除ボタン → キャンセル何も起きない
検索結果から編集 → 保存一覧が再読み込みされる(検索条件はクリア)
メールにシングルクォートを入れて保存エラーにならず保存される(パラメータ化クエリの確認)

検索条件を保ったまま再読み込みしたい場合

上の表では、編集後は LoadEmployees()(全件)で再読み込みしています。 検索条件を維持したい場合は、SearchEmployees() を呼ぶように buttonNew_Click / buttonEdit_Click / buttonDelete_Click の最後で分岐します。発展課題で扱います。


この章では扱いませんが、複数の INSERT / UPDATE / DELETE「すべて成功するか、すべて失敗するか」 という形にまとめたい場面があります。

using SqlConnection connection = new SqlConnection(_connectionString);
connection.Open();
using SqlTransaction transaction = connection.BeginTransaction();
try
{
// 複数のコマンドを実行...
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}

これは トランザクション という機能で、たとえば「社員を異動するときに、旧部署の担当を外して新部署の担当を増やす」など、複数の更新を 1 つの単位で扱う場合に使います。 現場では頻出ですが、まずは単発の CRUD を確実にできるようになることが優先です。本研修ではこの章では深入りしません。


25-7 完成形の確認(答え合わせ用)

Section titled “25-7 完成形の確認(答え合わせ用)”

まずは 25-3・25-4 のステップを自分で積み上げてから見てください

25-3(編集ダイアログ)・25-4(Form1)のステップをすべて終えたあとの完成形です。詰まったときの 答え合わせ に使ってください。

EmployeeEditForm.cs
namespace EmployeeApp;
using Microsoft.Data.SqlClient;
public partial class EmployeeEditForm : Form
{
private readonly EmployeeRepository _employeeRepository;
private readonly DepartmentRepository _departmentRepository;
// 編集対象。新規モードのときは null。
private readonly Employee _target;
// _target が null かどうかで「新規モード」を判定する
private bool IsNewMode => _target == null;
public EmployeeEditForm(
EmployeeRepository employeeRepository,
DepartmentRepository departmentRepository,
Employee target)
{
InitializeComponent();
_employeeRepository = employeeRepository;
_departmentRepository = departmentRepository;
_target = target;
}
private void EmployeeEditForm_Load(object sender, EventArgs e)
{
Text = IsNewMode ? "社員 新規登録" : $"社員 編集 (ID:{_target.EmployeeId})";
// 部署プルダウンを初期化
List<Department> departments = _departmentRepository.GetAll();
comboBoxDepartment.DataSource = departments;
comboBoxDepartment.DisplayMember = nameof(Department.DepartmentName);
comboBoxDepartment.ValueMember = nameof(Department.DepartmentId);
// 既存値を画面に流し込む(編集モード)
if (!IsNewMode)
{
textBoxLastName.Text = _target.LastName;
textBoxFirstName.Text = _target.FirstName;
textBoxEmail.Text = _target.Email;
dateTimePickerHireDate.Value = _target.HireDate;
numericUpDownSalary.Value = _target.Salary;
comboBoxDepartment.SelectedValue = _target.DepartmentId;
}
else
{
// 新規モード:今日を入社日のデフォルトに
dateTimePickerHireDate.Value = DateTime.Today;
}
}
private void buttonSave_Click(object sender, EventArgs e)
{
if (!ValidateInput(out string error))
{
MessageBox.Show(error, "入力チェック", MessageBoxButtons.OK, MessageBoxIcon.Warning);
DialogResult = DialogResult.None; // フォームを閉じない
return;
}
Employee employee = new Employee
{
EmployeeId = IsNewMode ? 0 : _target.EmployeeId,
LastName = textBoxLastName.Text.Trim(),
FirstName = textBoxFirstName.Text.Trim(),
Email = textBoxEmail.Text.Trim(),
HireDate = dateTimePickerHireDate.Value.Date,
Salary = numericUpDownSalary.Value,
DepartmentId = (int)comboBoxDepartment.SelectedValue
};
try
{
if (IsNewMode)
{
int newId = _employeeRepository.Insert(employee);
MessageBox.Show($"新規登録しました(ID:{newId})。", "完了", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
else
{
int affected = _employeeRepository.Update(employee);
if (affected == 0)
{
MessageBox.Show("更新対象の社員が見つかりませんでした。", "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning);
DialogResult = DialogResult.None;
return;
}
MessageBox.Show("更新しました。", "完了", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
catch (SqlException ex)
{
MessageBox.Show(
$"SQLServer 関連のエラーが発生しました。\nエラー番号:{ex.Number}\nメッセージ:{ex.Message}",
"DB エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
DialogResult = DialogResult.None;
}
}
private bool ValidateInput(out string error)
{
if (string.IsNullOrWhiteSpace(textBoxLastName.Text))
{
error = "「姓」は必須です。";
return false;
}
if (string.IsNullOrWhiteSpace(textBoxFirstName.Text))
{
error = "「名」は必須です。";
return false;
}
string email = textBoxEmail.Text.Trim();
if (!string.IsNullOrEmpty(email) && !email.Contains('@'))
{
error = "「メール」は @ を含む形式で入力してください。";
return false;
}
if (comboBoxDepartment.SelectedValue == null)
{
error = "「部署」を選択してください。";
return false;
}
error = string.Empty;
return true;
}
}
Form1.cs
namespace EmployeeApp;
using Microsoft.Data.SqlClient;
public partial class Form1 : Form
{
private const string ConnectionString =
"Server=localhost;Database=TrainingDB;Integrated Security=true;TrustServerCertificate=true;";
private const int AllDepartments = -1;
private readonly EmployeeRepository _employeeRepository;
private readonly DepartmentRepository _departmentRepository;
public Form1()
{
InitializeComponent();
_employeeRepository = new EmployeeRepository(ConnectionString);
_departmentRepository = new DepartmentRepository(ConnectionString);
}
private void Form1_Load(object sender, EventArgs e)
{
InitializeDepartmentCombo();
LoadEmployees();
}
private void buttonReload_Click(object sender, EventArgs e)
{
LoadEmployees();
}
private void buttonSearch_Click(object sender, EventArgs e)
{
SearchEmployees();
}
private void buttonClear_Click(object sender, EventArgs e)
{
textBoxKeyword.Text = string.Empty;
comboBoxDepartment.SelectedValue = AllDepartments;
LoadEmployees();
}
private void buttonNew_Click(object sender, EventArgs e)
{
using EmployeeEditForm dialog = new EmployeeEditForm(_employeeRepository, _departmentRepository, target: null);
if (dialog.ShowDialog(this) == DialogResult.OK)
{
LoadEmployees();
}
}
private void buttonEdit_Click(object sender, EventArgs e)
{
Employee selected = GetSelectedEmployee();
if (selected == null)
{
MessageBox.Show("編集する社員を選んでください。", "情報", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
using EmployeeEditForm dialog = new EmployeeEditForm(_employeeRepository, _departmentRepository, target: selected);
if (dialog.ShowDialog(this) == DialogResult.OK)
{
LoadEmployees();
}
}
private void buttonDelete_Click(object sender, EventArgs e)
{
Employee selected = GetSelectedEmployee();
if (selected == null)
{
MessageBox.Show("削除する社員を選んでください。", "情報", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
DialogResult result = MessageBox.Show(
$"社員番号 {selected.EmployeeId}({selected.FullName})を削除します。よろしいですか?",
"削除確認",
MessageBoxButtons.OKCancel,
MessageBoxIcon.Warning);
if (result != DialogResult.OK)
{
return;
}
try
{
int affected = _employeeRepository.Delete(selected.EmployeeId);
if (affected == 0)
{
MessageBox.Show("削除対象の社員が見つかりませんでした。", "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
else
{
MessageBox.Show("削除しました。", "完了", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
LoadEmployees();
}
catch (SqlException ex)
{
ShowError(ex);
}
}
private void dataGridViewEmployees_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex < 0)
{
// 列ヘッダーがダブルクリックされた場合
return;
}
buttonEdit_Click(sender, e);
}
private Employee GetSelectedEmployee()
{
if (dataGridViewEmployees.CurrentRow == null)
{
return null;
}
return dataGridViewEmployees.CurrentRow.DataBoundItem as Employee;
}
private void InitializeDepartmentCombo()
{
try
{
List<Department> departments = _departmentRepository.GetAll();
departments.Insert(0, new Department
{
DepartmentId = AllDepartments,
DepartmentName = "(すべての部署)"
});
comboBoxDepartment.DataSource = departments;
comboBoxDepartment.DisplayMember = nameof(Department.DepartmentName);
comboBoxDepartment.ValueMember = nameof(Department.DepartmentId);
comboBoxDepartment.SelectedValue = AllDepartments;
}
catch (Exception ex)
{
ShowError(ex);
}
}
private void LoadEmployees()
{
try
{
List<Employee> list = _employeeRepository.GetAll();
dataGridViewEmployees.DataSource = list;
}
catch (Exception ex)
{
ShowError(ex);
}
}
private void SearchEmployees()
{
try
{
string keyword = textBoxKeyword.Text.Trim();
int departmentId = (int)comboBoxDepartment.SelectedValue;
List<Employee> list = _employeeRepository.Search(keyword, departmentId);
dataGridViewEmployees.DataSource = list;
}
catch (Exception ex)
{
ShowError(ex);
}
}
private static void ShowError(Exception ex)
{
string title = ex is SqlException ? "DB エラー" : "エラー";
string message = ex is SqlException sqlEx
? $"SQLServer 関連のエラーが発生しました。\nエラー番号:{sqlEx.Number}\nメッセージ:{sqlEx.Message}"
: $"予期しないエラーが発生しました。\n内容:{ex.Message}";
MessageBox.Show(message, title, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}

症状原因対処
InsertSqlException (Number=2627)重複した email(UNIQUE 制約違反)別のメールに変える
InsertSqlException (Number=547)存在しない department_id を指定部署プルダウンの値が正しいか確認
保存しても一覧が変わらない戻った後に LoadEmployees を呼んでいないif (dialog.ShowDialog() == DialogResult.OK) の中で呼ぶ
入力エラーでもダイアログが閉じるDialogResult = DialogResult.None を設定していないSave の検証失敗時に明示的に None を代入
削除ボタンが押せたが効かないWHERE 句の typoSQL を SSMS でコピペ実行して確認
comboBoxDepartment.SelectedValuenull で例外DataSource 設定前に値を読んでいるLoad イベントで先に DataSource を設定
編集後に検索条件が消える編集後の再読み込みで GetAll を呼んでいる検索条件を保持したいなら SearchEmployees を呼ぶ(発展)
行をダブルクリックすると編集と削除が両方走るCellDoubleClick 内で buttonDelete_Click まで呼んでいるbuttonEdit_Click だけ呼ぶ

  • INSERT / UPDATE / DELETE をパラメータ化クエリで書ける
  • OUTPUT INSERTED.列名IDENTITY 採番後の値を取得できる
  • ExecuteNonQuery の戻り値の意味を説明できる
  • ShowDialog でモーダルな別フォームを開ける
  • フォーム間でデータをコンストラクタ + プロパティで受け渡せる
  • DialogResult で OK / Cancel を判定できる
  • 入力チェックを書ける(必須項目・形式・選択チェック)
  • 削除前に確認ダイアログを出せる
  • 戻った後の再読み込みパターン(if (dialog.ShowDialog() == DialogResult.OK) ...)を書ける
  • DataGridView.CurrentRow.DataBoundItem で選択行の元データを取り出せる

  1. ExecuteScalar / ExecuteReader / ExecuteNonQuery の使い分けを 1 つずつ例で説明してください。
  2. OUTPUT INSERTED.employee_id がない場合、Insert の戻り値はどうなりますか?
  3. ShowDialogShow の違いを説明してください。
  4. 編集ダイアログで「保存」を押したのに閉じないことがあります。なぜですか?
  5. DialogResult = DialogResult.None はどんな場面で使いますか?
  6. 削除前に確認ダイアログを出す理由を 2 つ挙げてください。
  7. CRUD の各操作で 影響行数 を確認することの意味は何ですか?

第 25 章も、第 23・24 章と同じ チームで自走するハンズオン形式 で進めます。 チームの役割分担(リーダー / 技術部長 / タイムキーパー)と「自走のすすめ」は 第 17 章「ここからはチームで進める」、ミニ発表の進め方は 第 23 章「演習課題」 を参照してください。本文を手順書として、ペア・チームで確認し合いながら、自分たちのペースでアプリを組み上げます(提供ソースを使うので、できあがる CRUD アプリは全員ほぼ同じ動作になります)。

この章で社員管理アプリは CRUD がそろった、一応の完成形 になります。まずは動かして「自分のアプリができた」を実感してください。そのうえで、第 23・24 章と同じく 必須課題は、できあがったソースを読み解いて「なぜそう書くのか」をコメントとして書き残す 作業です。CRUD・画面遷移・入力チェックという盛りだくさんのコードを、1 つずつ意味を確かめながら読み解きましょう。


  1. チームで本文 25-2〜25-7 の 実装ステップ に沿って、CRUD アプリを組み上げる(= 追加・編集・削除ができる)。25-3・25-4 のステップは自分の手で。詰まったら 25-7 の完成形で答え合わせ
  2. 「25-5 動作確認」のシナリオで、新規登録・編集・削除・入力チェックがひととおり動くことを確かめる
  3. 【必須課題 25-1】 できあがったソースに「なぜ」コメントを付ける
  4. 【発展課題 25-2 / 25-3】 余裕があれば機能を追加する
  5. タイムキーパーの合図で手を止め、チーム内で ミニ発表(下記)を行う

必須課題は、第 23・24 章から続けている EmployeeApp プロジェクト にそのまま書き込みます(「なぜ」コメントも同じプロジェクト)。これで社員管理アプリ(EmployeeApp)が一応の完成形になります。発展課題だけは、同じ KadaiWinFormsApp ソリューション内に 別プロジェクト として作ります。

課題プロジェクト内容
課題 25-1(必須)EmployeeApp(本文の続き=完成)CRUD アプリ本体 + 「なぜ」コメント
課題 25-2(発展)Ext_KeepSearch(新規)編集後も検索条件を保つ
課題 25-3(発展)Ext_BulkSalaryUp(新規)給与の一括引き上げ

社員管理アプリが完成したら、チーム内で一人ずつ ごく簡単に発表 します。最後の仕上げの章なので、完成したアプリを動かして見せる のがメインです。

  1. デモ:社員の 追加 → 編集 → 削除 を実際に動かして見せる(一連の CRUD が動くこと)
  2. 1 問説明:自分が付けた「なぜ」コメントから 1 つ、または上の「ペア確認」から 1 つを選び、自分の言葉で説明する(ShowDialog / DialogResultOUTPUT INSERTED での採番、ExecuteNonQuery の影響行数 など)
  3. 一言:コメントを書いていて一番「なるほど」と思った点、または詰まったポイントを一言

第 23〜25 章をやりきった自分をねぎらう

一覧 → 検索 → 編集と 3 章かけて、DB とつながる業務アプリを 1 つ作り上げました。ミニ発表は、その成果を言葉にして確かめる場です。うまく説明できなかったところは、チームで埋め合わせましょう。



課題 25-1 ソースを読み解いて「なぜ」コメントを付ける

Section titled “課題 25-1 ソースを読み解いて「なぜ」コメントを付ける”

本文 25-2〜25-7 の実装ステップで組み上げた CRUD アプリ(EmployeeApp)の Form1.csEmployeeEditForm.csEmployeeRepository.cs を読み返し、第 23 章の課題 23-1 と同じ要領 で、次の 2 種類のコメントを書き込んでください。

  • (A) メソッドの役割:各メソッドの 上の行 に、何をするメソッドかを 1 行(//)で書く
  • (B) 難所の「なぜ」:下の表の各箇所に、「なぜそう書くのか」「何のためか」前の行 に自分の言葉で書く(言い換えコメントは NG。→ 第 23 章「課題 23-1」・第 7 章「コラム:コメントの書き方」)

CRUD・画面遷移・入力チェックと盛りだくさんなので、全部を完璧に書ききる必要はありません。表の中から、チームで分担したり、自分が「なるほど」と思えた箇所を選んで、確実に自分の言葉にしましょう(目安は 各ファイル 3 箇所以上)。

(B) 「なぜ」コメントを付ける箇所

ファイル箇所説明する観点(= ここに「なぜ」を書く)
EmployeeRepository.csOUTPUT INSERTED.employee_idなぜこれを書くと採番された ID が取れるのか
EmployeeRepository.csInsertExecuteScalar を使うなぜ ExecuteNonQuery ではなく ExecuteScalar
EmployeeRepository.csUpdate/DeleteExecuteNonQuery の戻り値戻り値(影響行数)は何を意味するか
EmployeeRepository.csDELETE ... WHERE employee_id = @employeeIdWHEREなぜ WHERE が必須か(無いとどうなるか)
EmployeeRepository.cs(object)DBNull.Value の三項演算子なぜ null ではなく DBNull.Value を渡すのか
EmployeeEditForm.csIsNewMode => _target == nullなぜ 1 つのフォームで新規と編集を兼ねられるのか
EmployeeEditForm.csDialogResult = DialogResult.Noneなぜこれでフォームが閉じなくなるのか
EmployeeEditForm.csEmployeeId = IsNewMode ? 0 : _target.EmployeeIdなぜ新規のとき 0 でよいのか
Form1.csdialog.ShowDialog(this) == DialogResult.OKなぜ「保存されたとき」だけ再読み込みするのか
Form1.csusing EmployeeEditForm dialog = ...なぜ using を付けるのか
Form1.csCurrentRow.DataBoundItem as Employeeなぜこれで選択行の Employee が取れるのか
Form1.cs削除前の MessageBox(確認ダイアログ)なぜ削除の前に確認を挟むのか

確認すること

  • (A) 各メソッドの上に「役割」を 1 行コメントした
  • (B) 表から各ファイル 3 箇所以上に「なぜ」コメントを前行で書いた
  • とくに WHERE を忘れると全件削除になる 理由を自分の言葉で書けた
  • 言い換えコメントになっていない/本文の解説の丸写しになっていない
  • CRUD アプリが動く(追加・編集・削除・入力チェック)


課題 25-2 編集後も検索条件を保つ

Section titled “課題 25-2 編集後も検索条件を保つ”

KadaiWinFormsApp ソリューションに新しいプロジェクト Ext_KeepSearch を作成し、EmployeeApp のコードをコピーした上で、編集後・削除後・新規登録後に検索条件を保ったまま 一覧を再読み込みするように改修してください。

仕様

  • 編集後、検索キーワード・部署プルダウンが空でなければ、SearchEmployees を呼ぶ
  • 編集後、検索条件がクリア状態なら LoadEmployees(全件)を呼ぶ
  • 新規登録した社員が検索条件に合わないと一覧に出てこないことがある。その場合のヒントを MessageBox で出してもよい(任意)

KadaiWinFormsApp ソリューションに新しいプロジェクト Ext_BulkSalaryUp を作成し(EmployeeApp のコードをコピー)、選択した部署の全社員の給与を 1 万円上げる ボタンを追加してください。

仕様

  • 画面に「[部署一括 1 万円アップ]」ボタンを追加
  • 押すと「comboBoxDepartment で選択された部署」の全社員の salary を 10000 増やす
  • 確認ダイアログ → OK で実行 → 完了メッセージ(影響行数を表示)
  • 「(すべての部署)」が選ばれていたら、確認ダイアログで強く警告(「全社員に適用されます」)
  • パラメータ化クエリで UPDATE employees SET salary = salary + 10000 WHERE ...

ヒント:EmployeeRepositoryBulkSalaryUp(int departmentId) メソッドを追加し、ExecuteNonQuery の戻り値を表示する


  • 全プロジェクトが KadaiWinFormsApp ソリューションに入っている
  • 各プロジェクトで Nullable を disable にしている
  • Microsoft.Data.SqlClient を NuGet で追加した
  • パラメータ化クエリで書いている
  • 入力チェックで必須項目が空のときに保存できないことを確認した
  • 各メソッドの上に「役割」を 1 行コメントした
  • 各ファイル 3 箇所以上に「なぜ」コメントを前行で書いた(WHERE 必須の理由を含む)
  • SqlException をキャッチして MessageBox でエラーを表示する
  • 削除前の確認ダイアログを出している
  • binobj.vs フォルダが Git 管理に入っていない
  • チーム内でミニ発表(CRUD のデモ + 1 問説明 + 一言)を行った

完成したところまでを保存して提出します(タイマーはありません。自分のペースで区切りのよいところまで)。

Terminal window
git status
git add .
git commit -m "Chapter25: CRUDアプリ完成+なぜコメント / <一番なるほどと思った点>"
git push origin main

提出方法:Git が使えないときはサーバへコピー

Git の調子が悪いときは、講師の指示で push の代わりに KadaiWinFormsApp フォルダをサーバ上の自分のフォルダへコピーして提出します。 その場合は、コミットメッセージの代わりに、提出先へエクスプローラーの右クリック →「新規作成」→「テキスト ドキュメント」で 提出メモ.txt を作り、「どこまで完成したか」「詰まったポイント」を書いておいてください。

Git の詳しい操作は、付録 C「Git のインストールと提出ルール」 を参照してください。


  • INSERT / UPDATE / DELETEパラメータ化クエリ で書く(検索と同じ)
  • OUTPUT INSERTED.列名 で IDENTITY 採番後の値を取得できる
  • ExecuteNonQuery の戻り値は影響行数。0 行のときは「対象が見つからなかった」と判断できる
  • 別フォームは ShowDialog でモーダル表示、DialogResult で OK / Cancel を判定する
  • フォーム間のデータ受け渡しは コンストラクタで渡す のが基本
  • 入力チェックに失敗したら DialogResult.None でフォームを閉じない
  • 削除は 必ず確認ダイアログ、影響行数を確認、WHERE 句を絶対に忘れない
  • DataGridView.CurrentRow.DataBoundItem で選択行の元データに直接アクセスできる

これで Windows フォーム社員管理アプリは CRUD が一通り揃った業務アプリの最小完成形 になりました。


ここまで第 18〜25 章で Windows フォームの基本から CRUD まで一気に作りました。 次章からは Web アプリケーション の世界に入ります。

第 26 章 「ASP.NET Core MVC 入門(占いアプリ)」 では、DB を使わない簡単な占いアプリを作りながら MVC(Model-View-Controller)の流れを身に付けます。 名前と生年月日を入れると、ラッキーカラー・ラッキーナンバー・今日の一言が返ってくる、楽しめる題材です。

Windows フォームでは「画面が常に表示されていて、ボタンを押すとイベントが走る」流れでしたが、Web では「ブラウザがリクエストを送ると、サーバーが HTML を返す」流れに切り替わります。 この イベント駆動 vs リクエスト駆動 の違いを意識しながら進めてください。